mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-06-15 20:50:34 +00:00
Compare commits
3050 commits
Author | SHA1 | Date | |
---|---|---|---|
|
343680ffea | ||
|
477855789d | ||
|
93c9821cba | ||
|
0ca4a3778a | ||
|
69c3c2ae46 | ||
|
cd47bf2f04 | ||
|
6538803cf6 | ||
|
4c9ed330c8 | ||
|
7f16fd7736 | ||
|
3e4330686f | ||
|
3b6832724f | ||
|
968e0fddb9 | ||
|
39f466f2c6 | ||
|
4eed615871 | ||
|
3d4d785a2a | ||
|
51f6d3986f | ||
|
00aaecad07 | ||
|
c42040fbb8 | ||
|
9945b702b8 | ||
|
f68655b5e2 | ||
|
aaccc6e7f1 | ||
|
f30cb2b56c | ||
|
7cec628c43 | ||
|
0e85973e94 | ||
|
30252a1b2e | ||
|
1e964233c6 | ||
|
c9ac553cfe | ||
|
260b04a1cf | ||
|
ba70bb1154 | ||
|
85c38107cf | ||
|
8171a00943 | ||
|
ab2f5e3d8d | ||
|
2b68920f82 | ||
|
6597ec84eb | ||
|
6b628485c5 | ||
|
802ff6a67c | ||
|
8fc652f208 | ||
|
568e8533fa | ||
|
91bc39367b | ||
|
04e9e5284c | ||
|
1c54c77840 | ||
|
83f76280f5 | ||
|
712d4757c3 | ||
|
de726ca8d2 | ||
|
ebdcc403cf | ||
|
45800d7636 | ||
|
a7418fb483 | ||
|
df32e1ebfa | ||
|
525179f666 | ||
|
9485265769 | ||
|
1600d3b055 | ||
|
0121d78482 | ||
|
d480c6c2d3 | ||
|
7d5789032a | ||
|
06f40e72cb | ||
|
48e7a2ed06 | ||
|
66306e32f2 | ||
|
327f563e80 | ||
|
74ec83a0ff | ||
|
1865899621 | ||
|
2b4ec75bc5 | ||
|
e09ad990fa | ||
|
1e4a966c92 | ||
|
66c9840ad8 | ||
|
2c86f18a99 | ||
|
27ad26c258 | ||
|
984a9fe5ff | ||
|
b4fd6cf362 | ||
|
4f74cb7958 | ||
|
b6e24668a7 | ||
|
92a1e222f4 | ||
|
de71e9dadd | ||
|
be7da027f8 | ||
|
1e33926dc5 | ||
|
c2f67bd3c9 | ||
|
e7d0e0702a | ||
|
566e6443f4 | ||
|
4259d284bd | ||
|
58e91c154c | ||
|
28bd6f07a2 | ||
|
b1ad123595 | ||
|
c99cabfbc5 | ||
|
f5a7de9dc3 | ||
|
b12da2c543 | ||
|
02cd2c42fd | ||
|
dcc0b47349 | ||
|
0d33077df6 | ||
|
16608d2541 | ||
|
bab3498c6a | ||
|
72006215cb | ||
|
10e0294d5a | ||
|
61523baa7b | ||
|
6f871e6ce2 | ||
|
92c0cf1285 | ||
|
2585639054 | ||
|
0215339c5a | ||
|
539000574b | ||
|
bac5845be1 | ||
|
c282bc1bca | ||
|
9a7f37e2b7 | ||
|
71e9c2bb04 | ||
|
d9aa0731f4 | ||
|
49d3dd17a2 | ||
|
71cd80f204 | ||
|
5549dc7a15 | ||
|
613ed56675 | ||
|
a719cbfcc6 | ||
|
7d75e263f8 | ||
|
aabb011f5a | ||
|
e845e3575c | ||
|
f265c3197b | ||
|
e8e173d0d0 | ||
|
f842aff6df | ||
|
7e09481adc | ||
|
1b48fb7ae7 | ||
|
fe55acb4c9 | ||
|
fe3607bd14 | ||
|
5884c00bd0 | ||
|
13c3db7857 | ||
|
2b7488a4c8 | ||
|
b4576a0074 | ||
|
8861fc493b | ||
|
2bfb6ee016 | ||
|
edd7c258c8 | ||
|
3a3cec96ff | ||
|
80f8664564 | ||
|
be3ae583bc | ||
|
58106a42a9 | ||
|
096538989b | ||
|
150ad7a545 | ||
|
0d2f054c15 | ||
|
18cf5292b7 | ||
|
97d8a79d36 | ||
|
a306b1ce94 | ||
|
22c6a98914 | ||
|
8b64c734e7 | ||
|
befd8d4bd2 | ||
|
ce930eab5f | ||
|
75b25d011f | ||
|
953f6a3fd7 | ||
|
8e675de690 | ||
|
4326c3bfce | ||
|
47b788d44b | ||
|
16b0a4d762 | ||
|
b588ee59bc | ||
|
5466cafc24 | ||
|
812fe0a9bd | ||
|
c55c4ca42a | ||
|
83bd7be92a | ||
|
70397a9f05 | ||
|
a87eaa4b79 | ||
|
88cbc93338 | ||
|
927c3fcdb6 | ||
|
5803904deb | ||
|
c95e07a897 | ||
|
b42bd3d026 | ||
|
3dd800ac77 | ||
|
c92462b240 | ||
|
7573caa8e9 | ||
|
c12585377c | ||
|
17d7997137 | ||
|
66030f36ad | ||
|
b3d3895ae7 | ||
|
d6a855ff1b | ||
|
542030fd82 | ||
|
3fc38be5c4 | ||
|
168af88eda | ||
|
83d70d3471 | ||
|
596a9177ce | ||
|
2341ee6935 | ||
|
61c9cbdc8f | ||
|
00b56ca845 | ||
|
42158cbcb0 | ||
|
4dcc44687a | ||
|
fbce73f6fc | ||
|
f0c38621c1 | ||
|
a3e30b499f | ||
|
2ad452ee89 | ||
|
5d939498f1 | ||
|
f4b086738b | ||
|
6b30266145 | ||
|
c8180e714e | ||
|
0b356ee203 | ||
|
c2ebb3083a | ||
|
921938fd20 | ||
|
fab246f82e | ||
|
7757e06e36 | ||
|
70adedb142 | ||
|
7f6929b98d | ||
|
2575013faa | ||
|
d8fe1c64f1 | ||
|
fea85ff9c8 | ||
|
cc43935036 | ||
|
b5cbc47cf7 | ||
|
35b84d219f | ||
|
0aabbb3186 | ||
|
4dd6b102c4 | ||
|
0dd03da91f | ||
|
e1cd52178e | ||
|
55b4de779c | ||
|
9db4290d2d | ||
|
df30d2fbd3 | ||
|
b0cf7e5c81 | ||
|
756abbf807 | ||
|
5d7e068a8b | ||
|
a870d60621 | ||
|
9f27bde36a | ||
|
e868f81189 | ||
|
bac2e02160 | ||
|
ae7c68dbf8 | ||
|
0b11209674 | ||
|
f97150aa58 | ||
|
7e1ab086de | ||
|
be12c0a5f7 | ||
|
317f46ad97 | ||
|
c5e7e76e4d | ||
|
6556d31ab8 | ||
|
5476e3d759 | ||
|
cc7b7d508d | ||
|
2b9272c7eb | ||
|
cca3ebf520 | ||
|
428f670753 | ||
|
fadb7d0a26 | ||
|
2a88e29454 | ||
|
bfff0f7d87 | ||
|
96337d5234 | ||
|
eb49459937 | ||
|
ce3960f37f | ||
|
9f07ec35e6 | ||
|
f4366f8b2e | ||
|
523a46b4f5 | ||
|
6f8fc5f178 | ||
|
8f997ea4e3 | ||
|
992f8d9a5d | ||
|
9c6a39d692 | ||
|
b29a739fb2 | ||
|
1dea8f60a8 | ||
|
2629719b4e | ||
|
9e6e8c618e | ||
|
995f64513d | ||
|
5b01e43a12 | ||
|
03abb5c681 | ||
|
7a46377627 | ||
|
15e7a63e7b | ||
|
612f863ee9 | ||
|
237f22d131 | ||
|
2839e0078b | ||
|
0414f468c6 | ||
|
8b0731b5a2 | ||
|
7d0397e1ad | ||
|
f6476f1e8f | ||
|
cfebc32b82 | ||
|
721b7e9c8c | ||
|
1e88971ec8 | ||
|
8a6bcb712f | ||
|
002dc36ab9 | ||
|
9c590f4223 | ||
|
f0b408d823 | ||
|
17b2640237 | ||
|
fa006b9fc9 | ||
|
96037fbcc5 | ||
|
730b3459f1 | ||
|
60bb72ddc3 | ||
|
eabf31e6d0 | ||
|
550018c917 | ||
|
0829898d73 | ||
|
ec17c58dee | ||
|
563eff1193 | ||
|
594400a7f5 | ||
|
47ba068966 | ||
|
5df7c01cb5 | ||
|
f7ffa13543 | ||
|
23955d2dbb | ||
|
340d65d7a4 | ||
|
b9195ed309 | ||
|
fc1c017fc6 | ||
|
f563f8334b | ||
|
e09f9e9540 | ||
|
da21dc853d | ||
|
2228f882d8 | ||
|
8f3a6171ac | ||
|
2572afbf15 | ||
|
0faac3b875 | ||
|
cb0cc764ba | ||
|
45f55423fb | ||
|
8ef12a72e8 | ||
|
05884de50c | ||
|
67b7cf9764 | ||
|
9827106961 | ||
|
b2d5ee48cd | ||
|
7274c725a6 | ||
|
66ad059a47 | ||
|
c0d111e2c1 | ||
|
9116853e6d | ||
|
21aa61b69c | ||
|
119d905805 | ||
|
2f964f71bb | ||
|
d8a61edca0 | ||
|
3423d05f77 | ||
|
92891a61e8 | ||
|
76b9836e52 | ||
|
0fe4e0bf0b | ||
|
9971f71e94 | ||
|
803550111a | ||
|
09e9c047df | ||
|
cf5e7f6ed3 | ||
|
7a1cd675c2 | ||
|
e59f3bbe58 | ||
|
3e963e9239 | ||
|
42425abb69 | ||
|
437326ebfd | ||
|
44e49a06a0 | ||
|
975556c06b | ||
|
086ffd7aff | ||
|
612ef91af9 | ||
|
f8572c17dd | ||
|
d7c7784022 | ||
|
77cb344650 | ||
|
bb509bd537 | ||
|
d25a222bf9 | ||
|
0615a16124 | ||
|
91abc62ad0 | ||
|
d7d2d67558 | ||
|
1a55c70114 | ||
|
ffa830ae9b | ||
|
59ef053f50 | ||
|
df2f28bf31 | ||
|
311fda649f | ||
|
916a8b959e | ||
|
972b9e5474 | ||
|
7a72b2fc25 | ||
|
91bfd0f7c3 | ||
|
f54d714afd | ||
|
50e905fe4b | ||
|
f2a7a34abf | ||
|
a82068643d | ||
|
4ad101b53b | ||
|
08af298d11 | ||
|
451d928026 | ||
|
0e86dfa944 | ||
|
ad51c61ac8 | ||
|
4bb867bf52 | ||
|
54f24fe4b0 | ||
|
33a1d8de3d | ||
|
d5740ea844 | ||
|
5b0733d535 | ||
|
6b79ce605d | ||
|
8b5a398135 | ||
|
fcd57e9ac5 | ||
|
95c007953c | ||
|
764143d971 | ||
|
1af18f3028 | ||
|
80b58f3b45 | ||
|
773ebc7854 | ||
|
c616423edb | ||
|
9556abb162 | ||
|
c8bd1293b9 | ||
|
dfa95d8ed3 | ||
|
c85106e700 | ||
|
fecbe01e06 | ||
|
73a53e38c4 | ||
|
fd3675aac0 | ||
|
e70ef7f9e4 | ||
|
8b18ca15b5 | ||
|
06213714c5 | ||
|
fdd33fdeb0 | ||
|
cb78260d22 | ||
|
d36c91d10f | ||
|
2d85048925 | ||
|
63b568f4a0 | ||
|
d9397ef174 | ||
|
17f0b61576 | ||
|
1ef47cb48e | ||
|
79b8610fbe | ||
|
b128d127c2 | ||
|
6686f6415f | ||
|
df1f986239 | ||
|
06d96ec5a2 | ||
|
6d47045a60 | ||
|
410d104ad6 | ||
|
12dbf50ddc | ||
|
a54b2dd39e | ||
|
0a27b9e6d9 | ||
|
763739e3ae | ||
|
68b9dadf07 | ||
|
1faef49b51 | ||
|
9b1c9b1892 | ||
|
99b68d5b63 | ||
|
81dd45c814 | ||
|
caa1451fe8 | ||
|
147df5fd74 | ||
|
2f2bf6ca8f | ||
|
0bae18fe0d | ||
|
181bd13103 | ||
|
308a0c4532 | ||
|
6dfd1c1496 | ||
|
c7f961cc22 | ||
|
f13574d8ed | ||
|
cf1c7600a2 | ||
|
d644bcd79a | ||
|
5739a3d86f | ||
|
ded4261971 | ||
|
fb9b511e15 | ||
|
4333e90220 | ||
|
6c5a0c2795 | ||
|
c3ced8c7e6 | ||
|
9251b1ca26 | ||
|
47d540b7b8 | ||
|
1d9c89e3fe | ||
|
66c62d69b9 | ||
|
43ee6bfc1c | ||
|
ed3aa740be | ||
|
2d3d03b4d3 | ||
|
a0638ec983 | ||
|
3fcab67570 | ||
|
ceff8bd127 | ||
|
dee27e35b7 | ||
|
dd67dc87e3 | ||
|
097de9dbb7 | ||
|
8045e441a2 | ||
|
58723f2a8c | ||
|
8a04a38631 | ||
|
9250c592a7 | ||
|
636c76b03b | ||
|
39155ef81c | ||
|
2afffb39dd | ||
|
99d7cce0d6 | ||
|
eb137ec6dc | ||
|
6c5c09fae9 | ||
|
885928ea17 | ||
|
771741c10c | ||
|
63edc84103 | ||
|
8b37d8ec02 | ||
|
a8205d5b5d | ||
|
e5fd2c3568 | ||
|
5371eb52ad | ||
|
f7745a336f | ||
|
a33f29365a | ||
|
16b917abb1 | ||
|
436b6d8efb | ||
|
16c00ae3f5 | ||
|
855b03a9ea | ||
|
74c04d79c9 | ||
|
b771afe8be | ||
|
557b249e11 | ||
|
e3e58ac0be | ||
|
f1ba498b52 | ||
|
59beade079 | ||
|
f94ecfc7a6 | ||
|
13dae0f0d0 | ||
|
ee93448de7 | ||
|
6633cc4046 | ||
|
4ac7d0415b | ||
|
2f36bd5d77 | ||
|
7c48a299c3 | ||
|
17a2448237 | ||
|
1740a8e363 | ||
|
44f2195674 | ||
|
62791bfb47 | ||
|
d663f708ef | ||
|
6567041a3d | ||
|
0a45f776e0 | ||
|
01386b8451 | ||
|
d27a04e067 | ||
|
a49a5dcb11 | ||
|
bb26e04a55 | ||
|
51129febeb | ||
|
829469d0fe | ||
|
1f5e9a9335 | ||
|
b1894a0acb | ||
|
73ff822d24 | ||
|
a2d7f42138 | ||
|
895ee1d00b | ||
|
235e609eff | ||
|
84b0dd8980 | ||
|
c4a788b97b | ||
|
a946895fad | ||
|
2ce04c6a78 | ||
|
aacfe546d5 | ||
|
d468e1e4a6 | ||
|
50dd519c4f | ||
|
785c9557c8 | ||
|
c021e2b69f | ||
|
42008fb895 | ||
|
05aa9fa431 | ||
|
022afa6375 | ||
|
5b03f7d7b0 | ||
|
3fc6220009 | ||
|
245185d2f6 | ||
|
e4096b5157 | ||
|
a9719cada2 | ||
|
1c4833bc5d | ||
|
3343fd9813 | ||
|
41a6075fb5 | ||
|
5ac3162fca | ||
|
85c46ede5b | ||
|
68c2d27e8d | ||
|
4569b7eca6 | ||
|
d57b83fa08 | ||
|
747d9bfc6e | ||
|
a1ad3379ca | ||
|
450ffbe452 | ||
|
95581d7fbc | ||
|
436798b360 | ||
|
fe4273ca2a | ||
|
980dd74852 | ||
|
6159f299be | ||
|
4d9263f932 | ||
|
90e06dc37b | ||
|
ed4181617a | ||
|
22cc8c4986 | ||
|
1888a2eb82 | ||
|
52117e4b11 | ||
|
7835d78b3d | ||
|
5b563006f9 | ||
|
0fe69cea9f | ||
|
d8546dd140 | ||
|
8e4863e9cd | ||
|
a8d67cc607 | ||
|
c4d371d163 | ||
|
7f16ac3915 | ||
|
559313402e | ||
|
9595c6a1e5 | ||
|
8bbfb10cba | ||
|
a7fe24a294 | ||
|
4be24fdcaf | ||
|
95a7a3c0ec | ||
|
c237cf2c34 | ||
|
b12278e334 | ||
|
225482f7ed | ||
|
1de7754616 | ||
|
18967dadbf | ||
|
029fa9b8dc | ||
|
2381558169 | ||
|
9604dea90a | ||
|
e83238b681 | ||
|
ba9fa989ff | ||
|
67b9622bf5 | ||
|
f47c9ce9c6 | ||
|
146a96686b | ||
|
b0b63e58f8 | ||
|
8d433761d1 | ||
|
de6d2e7f40 | ||
|
905da44958 | ||
|
c1ac9396ee | ||
|
7f6421d977 | ||
|
a06793e25a | ||
|
7cf48db2fb | ||
|
4ff681e0bb | ||
|
7dea39736b | ||
|
1e0621797d | ||
|
f92dc28696 | ||
|
d3da30be6d | ||
|
100333c021 | ||
|
e905299eba | ||
|
6523a07a9f | ||
|
3c1f05cdc3 | ||
|
3000b08ec7 | ||
|
dd005a1e4c | ||
|
e5e3dc6e19 | ||
|
7af2ff0843 | ||
|
d688aeb184 | ||
|
473bc94278 | ||
|
23e1bfa720 | ||
|
3b41f206bc | ||
|
b3826c108d | ||
|
508026d09a | ||
|
5ee46a214c | ||
|
045b524bc6 | ||
|
cac791a6ca | ||
|
2591feb72e | ||
|
5b60ecbb18 | ||
|
d6616fed3f | ||
|
10902c0485 | ||
|
9680805bdb | ||
|
dbeb65da06 | ||
|
516c623df5 | ||
|
d4b3827efa | ||
|
0e26077722 | ||
|
d999c1d3ba | ||
|
31b1cb8ca6 | ||
|
5532ea5d2a | ||
|
3661b4f95b | ||
|
2dc4ce5da5 | ||
|
9707bb89e6 | ||
|
0331522128 | ||
|
ca51cf2509 | ||
|
89002b4562 | ||
|
bbd3d9ffe0 | ||
|
c2201480cf | ||
|
ee4aca3010 | ||
|
957a28f239 | ||
|
1bb06d775c | ||
|
b6bc7fb6d5 | ||
|
1dd13c4812 | ||
|
0fa2c861d6 | ||
|
2abc72b606 | ||
|
61e282af1a | ||
|
dd2d7d9215 | ||
|
2cc98bf410 | ||
|
64151790d0 | ||
|
58adebb325 | ||
|
983f990fe9 | ||
|
4918bf0ab2 | ||
|
08b6251a7a | ||
|
817b60a758 | ||
|
793ee66afa | ||
|
de6abf1439 | ||
|
9d7af671c5 | ||
|
1ed9992775 | ||
|
8417efc630 | ||
|
cc26f06289 | ||
|
1f0ce101eb | ||
|
4555b98aff | ||
|
c350f3c2af | ||
|
8366716456 | ||
|
effe1bacdf | ||
|
bbb88da9e0 | ||
|
f481bb74c4 | ||
|
042a297d1a | ||
|
e58abf0705 | ||
|
d3d78846dc | ||
|
dcb80ac105 | ||
|
2be14b95b3 | ||
|
1119ed6620 | ||
|
52ded6e8cc | ||
|
99fc5f16d2 | ||
|
f85106b86a | ||
|
84a33ca7b9 | ||
|
f00a169081 | ||
|
02c77d2e44 | ||
|
26fd68a37c | ||
|
21df8f8c08 | ||
|
63df653ad2 | ||
|
1200ae0ee6 | ||
|
64056c5527 | ||
|
063871a1eb | ||
|
4683291c1f | ||
|
6ad0db2cdb | ||
|
692d1bfb9e | ||
|
cdd084bbe8 | ||
|
8a7a1f519c | ||
|
81ae675f2d | ||
|
7f78a8428e | ||
|
7447d95f1b | ||
|
de0f7a08fe | ||
|
cd4b90fef4 | ||
|
271b583876 | ||
|
c65b3429ad | ||
|
b7e6e5cbc9 | ||
|
fda5aed89f | ||
|
ab1ec12698 | ||
|
059cdecf7d | ||
|
cf32d9d668 | ||
|
ce42723ad2 | ||
|
f4604e1c58 | ||
|
432de060ea | ||
|
97fa20237f | ||
|
780d9d5b78 | ||
|
6346d5608e | ||
|
2e83107c18 | ||
|
a5fcd66c95 | ||
|
80582923bb | ||
|
dfb261ac9a | ||
|
31b06e52ea | ||
|
4cc2498c24 | ||
|
a20855dfd9 | ||
|
a27be7d054 | ||
|
e62e9f5bd4 | ||
|
e3c46b40a0 | ||
|
44a395f134 | ||
|
e13124a426 | ||
|
ccf076ed1e | ||
|
9a59763df1 | ||
|
b68e2a1ed0 | ||
|
b05c21680d | ||
|
04e35e86d6 | ||
|
e73d7082a6 | ||
|
3406e604cd | ||
|
7ebf2d7a4f | ||
|
067d47f0ec | ||
|
7ba0073052 | ||
|
8e93d294e5 | ||
|
32d59c31d8 | ||
|
773fcd0780 | ||
|
cf21bfabf2 | ||
|
8b2b12e767 | ||
|
680d5221db | ||
|
092ae1fec8 | ||
|
dc5ddd3022 | ||
|
eca269cbf2 | ||
|
de1c8ece43 | ||
|
cb5b527d74 | ||
|
76c8279101 | ||
|
05ee55d617 | ||
|
3d01c9b363 | ||
|
8d6751c88d | ||
|
8e4fd2c167 | ||
|
41ba4b2bc3 | ||
|
5451035215 | ||
|
2bd2e501d9 | ||
|
cc3646640e | ||
|
760e97c7e7 | ||
|
721d17e181 | ||
|
dbdb9bc164 | ||
|
47159ad3c2 | ||
|
aabfb61834 | ||
|
f1fd8d84c3 | ||
|
367b98bfcb | ||
|
371ac83169 | ||
|
dba91bceca | ||
|
8f2273328b | ||
|
5dcdf645d6 | ||
|
011f0d5fee | ||
|
355f925954 | ||
|
f366c20869 | ||
|
408fd2030c | ||
|
4fcbb6ae61 | ||
|
3eca8c60e3 | ||
|
403004a85e | ||
|
a455819871 | ||
|
3368f55a88 | ||
|
58c8c0edc7 | ||
|
0b5cf4e5fd | ||
|
7c30430320 | ||
|
6e36e2ddfd | ||
|
c846147275 | ||
|
c141a82dfb | ||
|
e69b4b7f45 | ||
|
4f4e5f0d75 | ||
|
1c61e46f37 | ||
|
2ac560975c | ||
|
343b659755 | ||
|
b6e78b5f04 | ||
|
8236f3e5e7 | ||
|
538e2e0c9e | ||
|
65c6117962 | ||
|
884a8a8b23 | ||
|
0bc9718e3b | ||
|
762fb86ce7 | ||
|
9cb211470f | ||
|
e549f5c4a9 | ||
|
a8b46f1bf4 | ||
|
c0dc6eb35c | ||
|
9a5e5db271 | ||
|
f17622a1e1 | ||
|
8845f6a4c6 | ||
|
722dba1203 | ||
|
138c318be6 | ||
|
8576af247b | ||
|
4000d60305 | ||
|
c6e1efa0fe | ||
|
15e1844956 | ||
|
23c165dee1 | ||
|
315e53f064 | ||
|
6fe806c2b5 | ||
|
8cb328b6f2 | ||
|
002a70a2a4 | ||
|
7a1b2d97d4 | ||
|
539051c892 | ||
|
002e22510f | ||
|
93f61483b5 | ||
|
82ccba4267 | ||
|
f18f69809a | ||
|
9e3110988c | ||
|
ac52ea4d8e | ||
|
cbc7e16dc0 | ||
|
825fe9a4e2 | ||
|
a38e6ca99b | ||
|
2b32d00589 | ||
|
5a5ca76d9d | ||
|
162db2f3b9 | ||
|
d5d6a4daf9 | ||
|
3b3f0c1a29 | ||
|
299e25ab3c | ||
|
743e97738f | ||
|
9a55fda69c | ||
|
4eccd30ce2 | ||
|
42116b5bce | ||
|
9e00142b40 | ||
|
f88552ee7f | ||
|
f7c8940ff2 | ||
|
e757212741 | ||
|
c1bac30694 | ||
|
04bb7b4db0 | ||
|
a12a8c566d | ||
|
9a50f1f318 | ||
|
584392049c | ||
|
82f1789589 | ||
|
d3b7928b99 | ||
|
b025d068f2 | ||
|
fc5ed15af5 | ||
|
b9cd71d8eb | ||
|
2ea9f147ab | ||
|
f7c02cb3b0 | ||
|
3ef8a48ded | ||
|
93503c0ca9 | ||
|
cd177dee86 | ||
|
cd74d01324 | ||
|
fb528941ea | ||
|
e69f1e0b8c | ||
|
4a988aaeb8 | ||
|
f1a080c94e | ||
|
36ae29d746 | ||
|
00153754bb | ||
|
57f365979c | ||
|
ce3bb2f1d4 | ||
|
0ae637f531 | ||
|
4ec441560b | ||
|
a0f6e84ec2 | ||
|
ff2f7a8505 | ||
|
542c7e12b8 | ||
|
bca4af0c79 | ||
|
6bc72e513c | ||
|
90455a8111 | ||
|
ce1faa6020 | ||
|
f08b65ece1 | ||
|
8aa5125d5b | ||
|
93051dba0e | ||
|
143bf7608e | ||
|
53b6efaa6e | ||
|
9fc1404415 | ||
|
59222f7a35 | ||
|
6b15e772ac | ||
|
487d7fb26b | ||
|
77e99e92fb | ||
|
51d61af863 | ||
|
b0664b3c85 | ||
|
04e101c605 | ||
|
034c0f0fd8 | ||
|
39e0acb55a | ||
|
6420fe43da | ||
|
ac8afc4ac0 | ||
|
1e13dbb99c | ||
|
994c79569e | ||
|
4d9b6c5472 | ||
|
5965ff4364 | ||
|
f55c32ed37 | ||
|
953773a314 | ||
|
c1bfeb4c23 | ||
|
0af7151ae9 | ||
|
f03ee95bf0 | ||
|
c971c4d1d5 | ||
|
165b5f8c50 | ||
|
3c31c98d95 | ||
|
0f383a6545 | ||
|
44405e0cd7 | ||
|
0ed74d0aa4 | ||
|
3a408c0146 | ||
|
5c2de6aeb6 | ||
|
6f26e3bf79 | ||
|
5627bd8d7d | ||
|
4c3ae6f8ce | ||
|
cef6fef079 | ||
|
02ac4b3b04 | ||
|
042a5d0755 | ||
|
6006a0ba36 | ||
|
41aa1e51da | ||
|
5506f8001e | ||
|
194c4e9e9f | ||
|
49350f738f | ||
|
1756d7a516 | ||
|
ed4e9a50d5 | ||
|
d6cb9d72d8 | ||
|
27128a476c | ||
|
ecb26a0b16 | ||
|
560bdc4cb7 | ||
|
33696a8aed | ||
|
1998ecab45 | ||
|
a1cce9b796 | ||
|
2a3d962dc5 | ||
|
c7209dbd4f | ||
|
62bfc545d3 | ||
|
abe4efc4a2 | ||
|
3b4c48d9f5 | ||
|
ad3f1cf534 | ||
|
ee8249eec7 | ||
|
1ceaea844a | ||
|
77d68080e8 | ||
|
e0e63dd4da | ||
|
2c386fb792 | ||
|
037294b077 | ||
|
f62d07633d | ||
|
c5a625ae28 | ||
|
d110977580 | ||
|
4582ae91ab | ||
|
458b2386ed | ||
|
7cfd570c15 | ||
|
b6af64b970 | ||
|
d1196c3e28 | ||
|
03df4f253c | ||
|
b161f56a5c | ||
|
570eb7463a | ||
|
812df78b75 | ||
|
4464bf2eaa | ||
|
6132788b02 | ||
|
0c954135a3 | ||
|
f5e01b9196 | ||
|
1a8abde884 | ||
|
0201f5a456 | ||
|
b96d560f0a | ||
|
bc95a30db6 | ||
|
1a06f5e671 | ||
|
58c21d9868 | ||
|
e9bbf804ba | ||
|
be72fefb18 | ||
|
d44a1a4245 | ||
|
781fd1df9a | ||
|
27435ad82e | ||
|
d846f527af | ||
|
de23ea7f29 | ||
|
0fd63ece7d | ||
|
af9d9c0a5c | ||
|
f59d00b8e6 | ||
|
439ada614c | ||
|
f72540f5c2 | ||
|
9ec06199b1 | ||
|
4ee70913bd | ||
|
cf0ba40115 | ||
|
002e3fa171 | ||
|
f94e422b61 | ||
|
9ee7118bf6 | ||
|
155d621262 | ||
|
06123d74ba | ||
|
4fe0786bbd | ||
|
5f0ff8348f | ||
|
a3d405f670 | ||
|
851c82df85 | ||
|
72fa5fa922 | ||
|
ae9ac872c0 | ||
|
9fa838e366 | ||
|
877a9bd7f3 | ||
|
1ffeb4d44d | ||
|
2bc29c1fd3 | ||
|
ca17c9bc4f | ||
|
1026949b2b | ||
|
52764e140e | ||
|
30e501e7b0 | ||
|
0c8e69ed7c | ||
|
71c268da14 | ||
|
6607a63159 | ||
|
639997d67f | ||
|
0530b324c1 | ||
|
cb45ef2c03 | ||
|
4e444a066c | ||
|
a507f24694 | ||
|
985e3e85d6 | ||
|
620ba6e185 | ||
|
13b6f8fad4 | ||
|
e344585d99 | ||
|
9307acf7fa | ||
|
ed429d570e | ||
|
f8024f072f | ||
|
42385c81be | ||
|
473e7d951b | ||
|
3a8536b45e | ||
|
e5360ff431 | ||
|
3f904553ea | ||
|
289e8a08c3 | ||
|
65efdc8c81 | ||
|
54741b7cc4 | ||
|
a077ecba85 | ||
|
37b0dab0e8 | ||
|
d9de2d3d7b | ||
|
fb42cd8a0f | ||
|
1ffadbc270 | ||
|
08c716d110 | ||
|
99a1e30ab0 | ||
|
9b964db4c9 | ||
|
8452cd9efa | ||
|
9c31344bbc | ||
|
5dc52975ff | ||
|
40680a47ab | ||
|
d18761892e | ||
|
2eba3b321e | ||
|
0b1b8b91b9 | ||
|
db39370701 | ||
|
9fb058d5bc | ||
|
b5daa92c9d | ||
|
cc7419308b | ||
|
b992596236 | ||
|
b427cb6a3d | ||
|
6be5796888 | ||
|
3172bcd095 | ||
|
e6fa7c0b2b | ||
|
aa2abc50bf | ||
|
4604a6368c | ||
|
1c145e2ba9 | ||
|
f9e5a68156 | ||
|
25b0b15989 | ||
|
8c457cfa04 | ||
|
599d3a4d8a | ||
|
975f0141be | ||
|
51c34267a9 | ||
|
ea6c59e5e9 | ||
|
a84eeeb240 | ||
|
821c23e202 | ||
|
b75483e597 | ||
|
12c058bc49 | ||
|
76eeaffbb2 | ||
|
5bbe0eab25 | ||
|
73ce616bd9 | ||
|
72f616fa14 | ||
|
fceacf7081 | ||
|
0e2a00cbc8 | ||
|
1fcbc346bc | ||
|
45fa946e8b | ||
|
79972f8305 | ||
|
6b3bb2c747 | ||
|
e3f645af19 | ||
|
ae4b49c668 | ||
|
fd910cb828 | ||
|
c553ac7402 | ||
|
3003987c3a | ||
|
9903d536b5 | ||
|
aa7fd34097 | ||
|
02fc343642 | ||
|
456fb276d6 | ||
|
6e54d3cea9 | ||
|
1981ffbea0 | ||
|
6b11284e8a | ||
|
2642410702 | ||
|
3abd13e57b | ||
|
bf9f7a747e | ||
|
3094bd96df | ||
|
37cb636140 | ||
|
66e7b314f7 | ||
|
4d310434ab | ||
|
2b4fd40d62 | ||
|
97bb327b2a | ||
|
8d1c7cef3e | ||
|
cc8d84330c | ||
|
e1afa43aa3 | ||
|
29a490f6dc | ||
|
9b96cfc452 | ||
|
4616f0a4a4 | ||
|
f22be3a586 | ||
|
10d8cc21e6 | ||
|
360e4275ed | ||
|
c2f403f998 | ||
|
a5f3197651 | ||
|
f062b7cf0d | ||
|
e87251c7d9 | ||
|
d01779dc6c | ||
|
9aeaac5a96 | ||
|
7ac29827d2 | ||
|
6706f3a4b4 | ||
|
9504e4d540 | ||
|
54c84a7211 | ||
|
ba20fc735e | ||
|
a8250abbf1 | ||
|
31d4ba2eae | ||
|
976ae5707e | ||
|
6ceeadc0f0 | ||
|
7106b0484d | ||
|
938f4e4f1c | ||
|
627bf756b1 | ||
|
a56435801d | ||
|
27fac33c44 | ||
|
cab4cd3b8c | ||
|
ce166b4d8f | ||
|
d067fb2ec8 | ||
|
d46857d3b1 | ||
|
4c7dfceb9a | ||
|
8c8384c711 | ||
|
bf6bdab80c | ||
|
f2223cf2cb | ||
|
b64f951160 | ||
|
6d21231554 | ||
|
5ca033049e | ||
|
554ce7e7d6 | ||
|
43e28e5a6d | ||
|
9a68f6e221 | ||
|
86776be58c | ||
|
fe8e0a8bf8 | ||
|
cb3110df95 | ||
|
f045099fc1 | ||
|
211cd095d6 | ||
|
5d44e0eb3c | ||
|
20ad9175d8 | ||
|
45168639e9 | ||
|
b97a855a51 | ||
|
1929f5872c | ||
|
f058a5e229 | ||
|
f3546819ed | ||
|
68ab01254d | ||
|
6319d104a8 | ||
|
7b5d887c5b | ||
|
09ffeaf04e | ||
|
c6578c8699 | ||
|
5c89c3db69 | ||
|
60223d127e | ||
|
92266cb82c | ||
|
363de0c1c6 | ||
|
cbdd3a7f26 | ||
|
c0bf05d4bb | ||
|
71ed04d89b | ||
|
25bda89ac8 | ||
|
4942a916a8 | ||
|
37c0239aff | ||
|
ad78936365 | ||
|
0f0dec7fa9 | ||
|
5ab7be6124 | ||
|
39c0dcb0d4 | ||
|
b164daf510 | ||
|
87fd49a9bf | ||
|
eb9d0bb824 | ||
|
fec404f87a | ||
|
1db66e5a3b | ||
|
12400b6b87 | ||
|
9ce8e93c63 | ||
|
36861edf9a | ||
|
e0437ae8f6 | ||
|
59daaa62aa | ||
|
953ae18f1d | ||
|
3eaf29b71f | ||
|
24b7cfc841 | ||
|
03b03fe2dd | ||
|
9180d348bf | ||
|
5e7537953c | ||
|
020c7e2900 | ||
|
284ca6e1ac | ||
|
c63307e6d7 | ||
|
8011eadfd2 | ||
|
e66378d254 | ||
|
a5ebefd736 | ||
|
e17688a2da | ||
|
676b061af3 | ||
|
6da4192fe6 | ||
|
b688326383 | ||
|
9b1361b538 | ||
|
0ab965335f | ||
|
f11b0fa5eb | ||
|
862c2af1d9 | ||
|
dbd5a44b90 | ||
|
5f19639d0f | ||
|
b2ddb34258 | ||
|
97e0852156 | ||
|
53b02a82ae | ||
|
0a2e6e47c9 | ||
|
db8037d16c | ||
|
3fe9e4a207 | ||
|
16c036e2cc | ||
|
907910329f | ||
|
047f990c78 | ||
|
a000432b13 | ||
|
fb8192f40b | ||
|
fedd67dcaa | ||
|
95e8deded9 | ||
|
77a5e35081 | ||
|
18cbb587ba | ||
|
1c43a51520 | ||
|
26f843a89f | ||
|
9c10ba87df | ||
|
a3c752830b | ||
|
b82acb9ca9 | ||
|
718734ab18 | ||
|
7a90500fe7 | ||
|
e49138516c | ||
|
9c540d8abb | ||
|
2bffdec691 | ||
|
f4692cb1dd | ||
|
bc5b51687d | ||
|
e110847ede | ||
|
bd14e476f1 | ||
|
b1b707008f | ||
|
3d317b976e | ||
|
6e7ebc30e0 | ||
|
7ee4afacf4 | ||
|
7818ac658b | ||
|
4c57a97d4d | ||
|
9a345f9c6f | ||
|
f82a731b3a | ||
|
0b13bfe9dc | ||
|
9719b055c5 | ||
|
f85321ce9f | ||
|
cd2eb3a22d | ||
|
1d4d3e4cb0 | ||
|
c4f771dbbc | ||
|
a867d6228b | ||
|
9b85dcc03b | ||
|
36ce8bd4f7 | ||
|
f66aafb039 | ||
|
ef7cf4dd98 | ||
|
61c0fdcb5d | ||
|
c5c92d0467 | ||
|
8601562efe | ||
|
9740140798 | ||
|
b63627025e | ||
|
4ba4b00235 | ||
|
0b81ed2e34 | ||
|
5774d9c9ee | ||
|
6772e49712 | ||
|
d6ab55c263 | ||
|
f0b2df49dc | ||
|
7479888200 | ||
|
38753b08ac | ||
|
00411523d4 | ||
|
1f7126a8a1 | ||
|
b6ebad2761 | ||
|
df4bf3214e | ||
|
adb4cb8691 | ||
|
692a063528 | ||
|
c4d2f4a60a | ||
|
86a19e3765 | ||
|
814f279eeb | ||
|
0b7259afac | ||
|
caefa6d33e | ||
|
18f08ae5dc | ||
|
98fc0d5bd6 | ||
|
c32f0ca12e | ||
|
6a10728d94 | ||
|
4928a2badf | ||
|
f19af9f760 | ||
|
60ad532ece | ||
|
ced6bcc246 | ||
|
6f7ac75cfc | ||
|
ef9c818522 | ||
|
7e13ea4ba9 | ||
|
1fa39d0ab4 | ||
|
f2893aae0b | ||
|
49602e1e01 | ||
|
c6d8fec18f | ||
|
28151f2011 | ||
|
21e1756168 | ||
|
dfc11c545b | ||
|
18f3edd3ee | ||
|
3f5020ec83 | ||
|
45962eca1c | ||
|
62f76e1e8b | ||
|
1be30b8ecc | ||
|
ab327be9af | ||
|
d39aabe054 | ||
|
af12bce141 | ||
|
61c62ee1e8 | ||
|
235ded35fd | ||
|
72acbebff0 | ||
|
348a1d0207 | ||
|
2355be1cef | ||
|
cc0ef5290f | ||
|
7edc9e656f | ||
|
f9a8e121e1 | ||
|
8d73b5008a | ||
|
2bf5f0bf67 | ||
|
23c07d3cb3 | ||
|
3313a93ff7 | ||
|
165f3a788b | ||
|
f966b3a573 | ||
|
528bbcf67e | ||
|
419cc03133 | ||
|
72b659b3ea | ||
|
de3972a707 | ||
|
4616e3225c | ||
|
919b60bb6b | ||
|
81ee30771c | ||
|
ef78498dc2 | ||
|
5ebf9913a1 | ||
|
08b79ab8f1 | ||
|
3c6086caeb | ||
|
d8e3894209 | ||
|
a60f4e9ae8 | ||
|
005fbafb30 | ||
|
602c2588dc | ||
|
1a40186485 | ||
|
3924e2e563 | ||
|
46dddaf31c | ||
|
4e7ce210cd | ||
|
462d7aa81a | ||
|
184f862307 | ||
|
d1d7e99978 | ||
|
4ac60165a8 | ||
|
ad48d5e8f2 | ||
|
8f932a7641 | ||
|
934e6c3888 | ||
|
16f9c37c71 | ||
|
664e2b75bd | ||
|
67e9ba8286 | ||
|
827099d22d | ||
|
c3a1536849 | ||
|
cb339c1bf8 | ||
|
420f36251a | ||
|
8605df4b16 | ||
|
5b694988d1 | ||
|
a4f036499e | ||
|
7e59bb519e | ||
|
dbcbfef8c7 | ||
|
f243f5fe5c | ||
|
e391f1ef25 | ||
|
16d804e761 | ||
|
67e651f57c | ||
|
31a53bba8a | ||
|
25465fd9f3 | ||
|
052092cd2e | ||
|
56e7a2f6ab | ||
|
b5d5cf25fe | ||
|
5606111345 | ||
|
84f6484140 | ||
|
2dc42ba0db | ||
|
e0ad7e4c16 | ||
|
2bb071a950 | ||
|
57da8e649d | ||
|
374bb8323f | ||
|
2c99f66ea5 | ||
|
c0c0d42d9a | ||
|
9827406113 | ||
|
be56991b73 | ||
|
23098a98ab | ||
|
3011764da1 | ||
|
21da753607 | ||
|
837126be76 | ||
|
b464e74d41 | ||
|
e4081872c5 | ||
|
5376596557 | ||
|
2b61d51e91 | ||
|
35b42b88d9 | ||
|
ef7ed2d953 | ||
|
2a54d57968 | ||
|
33e601d33e | ||
|
8b54c3fed6 | ||
|
07cbc2f025 | ||
|
bdaa39e267 | ||
|
8ee8ae581a | ||
|
247702b76d | ||
|
fb7929dda6 | ||
|
d4061774a4 | ||
|
625fce3934 | ||
|
0858dfedb4 | ||
|
05207cafea | ||
|
cbb55c2322 | ||
|
d058c96596 | ||
|
8b11aee04d | ||
|
28a62e622e | ||
|
833331ab66 | ||
|
374671cb6f | ||
|
f646cabb3d | ||
|
a9a41a54be | ||
|
7eea27aefc | ||
|
b8767fa18f | ||
|
c399b1f0c6 | ||
|
64f664c859 | ||
|
287e76847a | ||
|
d240bbc4e2 | ||
|
a4893f30c8 | ||
|
c53e5b8bb2 | ||
|
52973d975e | ||
|
7d666ce517 | ||
|
a45443251b | ||
|
5ba1c98ae7 | ||
|
18f85a1543 | ||
|
6a2df92453 | ||
|
fe210a5715 | ||
|
bdd47d69ce | ||
|
14160d1d31 | ||
|
02990f8fcc | ||
|
065bc72bfe | ||
|
320cb73527 | ||
|
2f987b09ee | ||
|
44b05b4285 | ||
|
42b6c32f34 | ||
|
5c5c15d36a | ||
|
a1b87669f2 | ||
|
907d89c998 | ||
|
d6a9106ffa | ||
|
7e826385c7 | ||
|
5720faa808 | ||
|
a1b89c1fb9 | ||
|
885d3de7bb | ||
|
adde6fc4e3 | ||
|
08c6ab29e1 | ||
|
4ff2c8f1bc | ||
|
88bb91a5cb | ||
|
dda125edb5 | ||
|
c6238a6a9f | ||
|
d867ef9dee | ||
|
8e90abfebb | ||
|
e52d1fa317 | ||
|
402500f79c | ||
|
7e2cf613b4 | ||
|
3d7a38f7d4 | ||
|
a1553a8411 | ||
|
191a48ca41 | ||
|
4ef0e26762 | ||
|
0a02d8c096 | ||
|
6edb188899 | ||
|
51c7d0652e | ||
|
a45f944edd | ||
|
06273ed628 | ||
|
a85a647794 | ||
|
e0594ef349 | ||
|
7bc785fba3 | ||
|
cb84206457 | ||
|
5c00db62b7 | ||
|
b3e558bec0 | ||
|
a041943287 | ||
|
d902ecca4f | ||
|
263ccbc64a | ||
|
1ca75c31ae | ||
|
806d101d87 | ||
|
20d2a7d05b | ||
|
5dac064a49 | ||
|
abf872130a | ||
|
dfa5b9d8bb | ||
|
939f37dec5 | ||
|
19f69614a2 | ||
|
511ee766df | ||
|
81437bb1c9 | ||
|
2b0bf218b1 | ||
|
753425507a | ||
|
768fad2445 | ||
|
dccd4c3306 | ||
|
79bce5ddea | ||
|
ec46b706e9 | ||
|
b1b3930e2b | ||
|
87f5d0b31c | ||
|
0b8883e3ca | ||
|
427ebcd759 | ||
|
4072f1df34 | ||
|
d40b804952 | ||
|
29ed333b97 | ||
|
f2f71024b6 | ||
|
3beb712e16 | ||
|
42f2ae7c2b | ||
|
a5dd92f479 | ||
|
90ad761d83 | ||
|
f7f65e5bcb | ||
|
0dd0acd738 | ||
|
f327053811 | ||
|
ab01fc6143 | ||
|
77260a8442 | ||
|
2c855b9cfe | ||
|
68c55ca413 | ||
|
d77929252a | ||
|
351453c132 | ||
|
f6f079f3a8 | ||
|
d18270a391 | ||
|
a0455b5e00 | ||
|
d6fa921822 | ||
|
6bca5a9962 | ||
|
d7bd4c1c93 | ||
|
987e4efc02 | ||
|
ffea0e2d2d | ||
|
e4634ca2fe | ||
|
0206178279 | ||
|
4bc0ae09fa | ||
|
dcad6ffe34 | ||
|
b9bc331ebc | ||
|
50980a0b3e | ||
|
cfb59100d6 | ||
|
e06665b92d | ||
|
31a32a7e2e | ||
|
c09b7b9e41 | ||
|
23cc00ce4b | ||
|
d27e279272 | ||
|
557917b92a | ||
|
2c3514a5a1 | ||
|
7eb67de34c | ||
|
3250675e78 | ||
|
5d01bfcb79 | ||
|
05258756ce | ||
|
7d681c5ce4 | ||
|
ea98a0b596 | ||
|
943a138d49 | ||
|
05ece5560e | ||
|
6b80fdc270 | ||
|
7425b31173 | ||
|
e93544cfc6 | ||
|
a4aa45d27f | ||
|
c02a0d8757 | ||
|
eca3fa9308 | ||
|
f091e90b24 | ||
|
ce50a3003c | ||
|
2f16b5dd3e | ||
|
817231b4d0 | ||
|
92c66be943 | ||
|
a4a5caec53 | ||
|
ee950499d5 | ||
|
075a625305 | ||
|
4a2c93299e | ||
|
3c81afa7b2 | ||
|
e4846d0d53 | ||
|
843875e95d | ||
|
f908cbaffd | ||
|
f104ffd251 | ||
|
3b04a3c06d | ||
|
90c203857a | ||
|
5aa1b7cd3b | ||
|
3e10efa134 | ||
|
b51cdb9de0 | ||
|
ee200326ef | ||
|
b66db922d8 | ||
|
3e5fa04379 | ||
|
5feca3f74b | ||
|
ae32cc7f2b | ||
|
d69b4edfbc | ||
|
03fcc2cb9a | ||
|
f11260aeb5 | ||
|
7ff998f925 | ||
|
385a983e5f | ||
|
b423febfbe | ||
|
03430a9571 | ||
|
9285798210 | ||
|
e82678586f | ||
|
279dd7d053 | ||
|
4aa00c9eca | ||
|
f5260f9b36 | ||
|
0ca0d485a0 | ||
|
d73bce2985 | ||
|
930cdca750 | ||
|
bbb7ced95a | ||
|
ca7cf7dee7 | ||
|
617a2ef49e | ||
|
6ce523a7a8 | ||
|
9e3f713aa9 | ||
|
94b7677318 | ||
|
5004479c6f | ||
|
a4fd144647 | ||
|
930cc41aaa | ||
|
5788837fb6 | ||
|
7233d6936c | ||
|
874063668b | ||
|
28dbbe33db | ||
|
de936f42e9 | ||
|
3de317b3c9 | ||
|
84402f39ef | ||
|
0ba6ebb10f | ||
|
b5a3a99825 | ||
|
12656afe7d | ||
|
5af52f94a8 | ||
|
558656deb5 | ||
|
08379ab389 | ||
|
96c28a5728 | ||
|
452ea76a69 | ||
|
d3d98c73ca | ||
|
7daab76f17 | ||
|
00b3199727 | ||
|
cde5fdf202 | ||
|
ffdcc8167c | ||
|
821ec857e1 | ||
|
69ceaa3a5e | ||
|
0ed72a360d | ||
|
97dba9046b | ||
|
e8f15eb1e8 | ||
|
803e452889 | ||
|
0a3e9c81f9 | ||
|
584daeaa81 | ||
|
f8d162d909 | ||
|
bd2ff494c7 | ||
|
03d4d916f5 | ||
|
bf14939b9b | ||
|
55d30db53b | ||
|
d48d732038 | ||
|
08b52ffd2f | ||
|
5dab4bc502 | ||
|
518e43fc5f | ||
|
59ca466081 | ||
|
1ef9ae6398 | ||
|
8eb8ea0e7d | ||
|
c1615d01e6 | ||
|
97985d6442 | ||
|
ab96219c19 | ||
|
f6eb967cbe | ||
|
e0d05353e8 | ||
|
71dd5182f3 | ||
|
b80cf1fb8e | ||
|
983fcf2fbd | ||
|
b5443c5966 | ||
|
88edc93a8a | ||
|
70e6227c9a | ||
|
bd560fa9f9 | ||
|
c6feb31207 | ||
|
9ebbae9d27 | ||
|
4874c7eb1f | ||
|
09a697faef | ||
|
94c5cbbfb8 | ||
|
5fe95afe87 | ||
|
22bb6ec74a | ||
|
bd88395859 | ||
|
4a54001aed | ||
|
e973d41bac | ||
|
a3db85d869 | ||
|
dd028ce97e | ||
|
22585a0383 | ||
|
6cf7d28481 | ||
|
aac7e52d87 | ||
|
9e6fc2983f | ||
|
ecf03d0e5c | ||
|
8389bff7d8 | ||
|
7b109785be | ||
|
31864c9a9d | ||
|
526f34d98b | ||
|
35d9247487 | ||
|
73cfb357c6 | ||
|
7f000ea42b | ||
|
3a5e05f2ab | ||
|
83797e9df3 | ||
|
b38f6cc731 | ||
|
d9f3e8e9e7 | ||
|
b682833cca | ||
|
04648546d1 | ||
|
6ffe9d3744 | ||
|
2a4f9fbd54 | ||
|
1d5089dfa7 | ||
|
de2ea8a1b2 | ||
|
a1e2debde4 | ||
|
c2aafa4f46 | ||
|
ab344a469a | ||
|
52f5dc0185 | ||
|
72d9d3dc58 | ||
|
2cf84d5ce8 | ||
|
e0e890fd8d | ||
|
422ea740ca | ||
|
8263e19313 | ||
|
7b81945ca1 | ||
|
03787ef857 | ||
|
001fdd4d03 | ||
|
c052752bfb | ||
|
9e7406cec0 | ||
|
51c2f35909 | ||
|
22205c235d | ||
|
794df19122 | ||
|
4e0e8d1cc3 | ||
|
36eb00121c | ||
|
93bfa4d37a | ||
|
22ae31bfcb | ||
|
1b8ee90f83 | ||
|
d3d483d0b5 | ||
|
7654567201 | ||
|
1451e267cc | ||
|
12f2b9282a | ||
|
5a0f7f6976 | ||
|
c879b4ee2e | ||
|
858865b731 | ||
|
7c872bb92b | ||
|
cf46d9439b | ||
|
240fa0b066 | ||
|
5a4c8b9fc8 | ||
|
2c511fca50 | ||
|
772fbaa073 | ||
|
6ce3029e07 | ||
|
b795c0c392 | ||
|
6e28a17280 | ||
|
c35d1cdc0c | ||
|
2c6ab9dd1f | ||
|
e419c18a87 | ||
|
bbe0b068e5 | ||
|
42f9f99690 | ||
|
3abbfbb51a | ||
|
1983041770 | ||
|
f44b86cd30 | ||
|
ddb3bde942 | ||
|
c221e9e870 | ||
|
b185819140 | ||
|
65fcd55160 | ||
|
763ad0cb18 | ||
|
b2d0172422 | ||
|
75d348709c | ||
|
eb8dfb28f1 | ||
|
0dd6e303ce | ||
|
51f8e963d6 | ||
|
326449b3e6 | ||
|
0c7764fa40 | ||
|
ab14c50d1c | ||
|
3cc2b32756 | ||
|
f78b97ba4a | ||
|
81f5f0f60c | ||
|
0bc7697600 | ||
|
16d35a789a | ||
|
a07edc7cf4 | ||
|
4576bea10c | ||
|
f0d64a9fe8 | ||
|
64dd588734 | ||
|
1573522520 | ||
|
689bd93055 | ||
|
7b66b21f47 | ||
|
e57c2eb101 | ||
|
cf176bdd93 | ||
|
300adda41b | ||
|
85fd7175de | ||
|
8bab034bc8 | ||
|
0615a94cae | ||
|
53cef60f2c | ||
|
faee72bf5b | ||
|
e2ecd77654 | ||
|
9858eeeb00 | ||
|
a43e21a414 | ||
|
61cdf47462 | ||
|
d6ba009742 | ||
|
d15c32e3e6 | ||
|
c5cba3fec5 | ||
|
5e4fc8b138 | ||
|
21d41ca244 | ||
|
6163589ac7 | ||
|
db9c38aa93 | ||
|
c5ef83d5b0 | ||
|
cd0773662f | ||
|
da0a934307 | ||
|
bba26a9cf5 | ||
|
b7c08933aa | ||
|
9783d01a35 | ||
|
4a5815cc97 | ||
|
8128c14fa9 | ||
|
114fd3c5f6 | ||
|
7c909e59a9 | ||
|
a1c89dd17b | ||
|
82d969190f | ||
|
d7677a6337 | ||
|
7b908e0165 | ||
|
2acb83da8f | ||
|
a4df10b112 | ||
|
d230345d90 | ||
|
1826111278 | ||
|
9ae8f0d330 | ||
|
86021d637b | ||
|
6db599e5ae | ||
|
7bd7c4e960 | ||
|
b12d91775d | ||
|
5904e1ccb4 | ||
|
099a3f2114 | ||
|
3ed9e291c2 | ||
|
80e3acda93 | ||
|
57f459f43b | ||
|
66e987c174 | ||
|
febbd5c2c9 | ||
|
423fa0d0a9 | ||
|
13923051a0 | ||
|
f9fb18ae1b | ||
|
5f98e61c91 | ||
|
921ca7fbab | ||
|
c46901d150 | ||
|
f02322bd80 | ||
|
f9a39b1138 | ||
|
40a4b745e3 | ||
|
ee64d53cf4 | ||
|
97e6a89cac | ||
|
3cf2ad3b77 | ||
|
3052884bdc | ||
|
7ce0ef5b88 | ||
|
4668da45ef | ||
|
0b348406ef | ||
|
11bef9066c | ||
|
2c2cd8c2be | ||
|
86f422592b | ||
|
e7a9c2b054 | ||
|
651ea7de5f | ||
|
c68f6b2631 | ||
|
d88786a824 | ||
|
ac60e7b101 | ||
|
55aad51141 | ||
|
288acaa7cc | ||
|
63337c7df1 | ||
|
444ee40f09 | ||
|
ac0f9f41df | ||
|
5285fab8b3 | ||
|
8722206be8 | ||
|
29d310a2ab | ||
|
31172bb011 | ||
|
8d9a7fefa1 | ||
|
53bfb58751 | ||
|
88ecd79090 | ||
|
41a37db2c7 | ||
|
f817f6e9b9 | ||
|
58322bcc96 | ||
|
e87a7afe3e | ||
|
b738d5933d | ||
|
e3fbf2078d | ||
|
e81047b8a2 | ||
|
78d7cbd7dd | ||
|
c05f2bca27 | ||
|
4a8848e5e9 | ||
|
1818a5a7a2 | ||
|
4b00c7d40b | ||
|
8c6ff24052 | ||
|
44b4a4eb7e | ||
|
d568d85c4b | ||
|
82be7b3ac5 | ||
|
d9bda62a47 | ||
|
3acaaa50f4 | ||
|
c99b7785f9 | ||
|
32ef12e738 | ||
|
0a7d1639e7 | ||
|
5ae1f72162 | ||
|
79fb66f338 | ||
|
da4122a721 | ||
|
b9541b2ca4 | ||
|
db30cfcc74 | ||
|
acce0ab4fc | ||
|
97f45a0f77 | ||
|
6111d0f4ed | ||
|
27b9f0d868 | ||
|
bd8a7e8df7 | ||
|
b0cebbedbc | ||
|
70f0aa9758 | ||
|
54c8f5b3ab | ||
|
c5d3a2efce | ||
|
01a551f2ac | ||
|
fe0075ad15 | ||
|
cecacf5430 | ||
|
76a33e8f47 | ||
|
cf637d0288 | ||
|
d17c3483c1 | ||
|
6ec98ec5e4 | ||
|
6f7a8f36de | ||
|
30796fbe07 | ||
|
1a830c7c78 | ||
|
21e8c6dcd1 | ||
|
cdfa63f341 | ||
|
8585ef1e66 | ||
|
a132fdb8be | ||
|
f6aa03b58a | ||
|
74b47f016f | ||
|
f890abe5cb | ||
|
db6b9531ca | ||
|
b98efea5aa | ||
|
ce45f5a673 | ||
|
8cf682d72b | ||
|
7483a66b66 | ||
|
c8f12b8c3b | ||
|
c98b626f69 | ||
|
3260651671 | ||
|
bc587a09f8 | ||
|
e642d6a4c1 | ||
|
0911775142 | ||
|
9a53bcd405 | ||
|
0c89e0819f | ||
|
febb2fb035 | ||
|
7c3e50c629 | ||
|
ed90b338f8 | ||
|
b3184b45bc | ||
|
01cc9e23d8 | ||
|
b1bd3020fa | ||
|
69bb09f7ad | ||
|
502b336361 | ||
|
3ce6a5f403 | ||
|
0f88b3df68 | ||
|
7708b65e97 | ||
|
afd736dfec | ||
|
a03b52a975 | ||
|
ea2a313d01 | ||
|
2878e4ce7c | ||
|
600d217e7d | ||
|
08229402cd | ||
|
fd1c0a7518 | ||
|
4c8c398aa4 | ||
|
f4613bfc07 | ||
|
291d951b01 | ||
|
484cad00ce | ||
|
ea394fb06e | ||
|
c515ee5a01 | ||
|
e61bfa1ea9 | ||
|
b9864acad6 | ||
|
c8fc082968 | ||
|
bb3949aeda | ||
|
426dc4c54d | ||
|
610f98dc2d | ||
|
96d86eaa06 | ||
|
626df03961 | ||
|
434fe8e56f | ||
|
715418c005 | ||
|
338e334c8a | ||
|
1a4e6d58f4 | ||
|
6f14ebdfee | ||
|
cfbc062743 | ||
|
4a870af19c | ||
|
a8a3a6ec3e | ||
|
4894e7b3ee | ||
|
6b9f915286 | ||
|
160571e251 | ||
|
f5cc0e50fe | ||
|
fced787a63 | ||
|
848b296390 | ||
|
120f4ab84b | ||
|
19dcb8159a | ||
|
b7d1c178a3 | ||
|
7604a0c596 | ||
|
dc28274f65 | ||
|
76198f4395 | ||
|
052365ba1a | ||
|
67f566dd28 | ||
|
ca892cf116 | ||
|
a051127cb1 | ||
|
6e639dbfee | ||
|
1123648aae | ||
|
54d8c5f6a9 | ||
|
5397f4bfaf | ||
|
f733f90333 | ||
|
24ec79cd1a | ||
|
1c3ae0f89a | ||
|
5a640f9f3d | ||
|
b372c35f35 | ||
|
82df25694a | ||
|
36b346e733 | ||
|
d38a84ab79 | ||
|
ef41adf776 | ||
|
29052b1acb | ||
|
0b08f855c5 | ||
|
a5a80281f3 | ||
|
3cdc5870a1 | ||
|
3561c0ee98 | ||
|
b5375d2f21 | ||
|
23219c3c09 | ||
|
9415c50200 | ||
|
77c59f4f13 | ||
|
3a2d16f00c | ||
|
0592e46e65 | ||
|
94ae345701 | ||
|
640ce43fee | ||
|
d6f6f1a777 | ||
|
d15e97efb8 | ||
|
ada328df01 | ||
|
5439f14e57 | ||
|
8f8f5bdff7 | ||
|
e16cad7c8f | ||
|
c2de0649a7 | ||
|
b8ad30610b | ||
|
88dfd97df6 | ||
|
2c4c35deba | ||
|
8f81cb8812 | ||
|
17feaa8c71 | ||
|
8dfc872544 | ||
|
91c8fd146d | ||
|
94f75c29a1 | ||
|
1ec1352c88 | ||
|
e9f0a3b8ac | ||
|
6b5e536ca6 | ||
|
3b37c87182 | ||
|
04a60b8f46 | ||
|
97b6a9099f | ||
|
ee0b2e79da | ||
|
5660df7f70 | ||
|
f4a3881dcf | ||
|
23bcfb8b1c | ||
|
9349b86b27 | ||
|
3d24f1da22 | ||
|
e1ea71fec7 | ||
|
bf5e231e5b | ||
|
8bda233d02 | ||
|
78d9fb521d | ||
|
75959e8c63 | ||
|
15cf738616 | ||
|
40765f7c53 | ||
|
e9a08214bb | ||
|
8f74314e96 | ||
|
7923e26545 | ||
|
95cdd43f4f | ||
|
3203f57748 | ||
|
67c5871957 | ||
|
27bc5c89ca | ||
|
e0b577fe1d | ||
|
61214b5788 | ||
|
b92360db37 | ||
|
70d3eecbc5 | ||
|
f95b5ee666 | ||
|
8d0d438615 | ||
|
c81213b83c | ||
|
e573f0ba16 | ||
|
7d17f88941 | ||
|
06accc8d98 | ||
|
c3fb55f235 | ||
|
5be7ebd480 | ||
|
2bd4fc4728 | ||
|
3dda2aebe9 | ||
|
195f22c2e3 | ||
|
81f9d334e8 | ||
|
65d625a4eb | ||
|
bb8931c39b | ||
|
9acacbb320 | ||
|
5e89d345d8 | ||
|
f8adb42f7b | ||
|
9d359d9341 | ||
|
3a9a937bfd | ||
|
b919d226b1 | ||
|
d9e727050c | ||
|
359c07203e | ||
|
a500166082 | ||
|
0335893559 | ||
|
50548c8e6a | ||
|
11238579a5 | ||
|
b57c50bdb9 | ||
|
2cada57efc | ||
|
aa354058f5 | ||
|
39a75632c8 | ||
|
9721b3e762 | ||
|
dc0c5f7611 | ||
|
2345c455c1 | ||
|
e7b3b87757 | ||
|
83962cbb8c | ||
|
84e4fe7f59 | ||
|
a02fe56871 | ||
|
5dd0a23986 | ||
|
4e93604fe9 | ||
|
bb1e1b1529 | ||
|
0aef5152a7 | ||
|
eee4a65d19 | ||
|
0616c18703 | ||
|
7c61fd9e7a | ||
|
77bf0c945e | ||
|
a7df50e68f | ||
|
cc402f769c | ||
|
3c982a3de0 | ||
|
19611f0ebe | ||
|
20607fe346 | ||
|
126df546c0 | ||
|
2cde437bee | ||
|
75170e5162 | ||
|
ab251ec573 | ||
|
1cd5d5ef45 | ||
|
bd7cc85e72 | ||
|
cbda137fbf | ||
|
f63c4284c1 | ||
|
b649e9b076 | ||
|
1a826caf75 | ||
|
78c0ea6a4c | ||
|
d4ce1a33f2 | ||
|
875c3efb91 | ||
|
13efa59252 | ||
|
42b4defb5c | ||
|
cbf1266a8c | ||
|
b062f63ec3 | ||
|
6267e00c20 | ||
|
a7180e3995 | ||
|
a3dafea688 | ||
|
084c9d1447 | ||
|
72700fb07e | ||
|
27b2de1d05 | ||
|
fa4cd38f75 | ||
|
d28fda6bc3 | ||
|
84896e6468 | ||
|
65c9c33f88 | ||
|
e3aa368d94 | ||
|
c380a3ea3d | ||
|
2b7cebb02a | ||
|
9d8fe31a5b | ||
|
0754c98f9d | ||
|
595616fe2d | ||
|
fdc3ea68e8 | ||
|
9167e5e561 | ||
|
32d511684e | ||
|
dfdbd370f9 | ||
|
8bc2e5ebb8 | ||
|
fc1bae347e | ||
|
a2b766d568 | ||
|
1a7c4c14cc | ||
|
61270a337b | ||
|
d36218fe35 | ||
|
817a4710f3 | ||
|
0f4383a8a0 | ||
|
c09dc96c2c | ||
|
cdca6c9372 | ||
|
514f6ea7ec | ||
|
078bf81b85 | ||
|
3b739530bf | ||
|
02310d8720 | ||
|
a88fe95fee | ||
|
38ecd43074 | ||
|
7e1181ab84 | ||
|
640d8ef904 | ||
|
005c62425b | ||
|
348b865187 | ||
|
0d93910a39 | ||
|
3d617371af | ||
|
ea6c05e16c | ||
|
708c6aa57e | ||
|
b80a607737 | ||
|
f57ce41e89 | ||
|
fddf33d339 | ||
|
34a2dd80a2 | ||
|
74f3de5674 | ||
|
d5b648921c | ||
|
c7fe08bf6d | ||
|
b5796f5773 | ||
|
09561686b8 | ||
|
89b72b53d1 | ||
|
2131d5bfda | ||
|
46651e31c4 | ||
|
6aed1ed926 | ||
|
58786fa0b5 | ||
|
8b8380992f | ||
|
b81bdd823a | ||
|
87d7b44dd2 | ||
|
f81d7b61b5 | ||
|
767ed3afae | ||
|
7dcd51e2f1 | ||
|
c3cd12d08b | ||
|
810f27886b | ||
|
3c9f1c0d1d | ||
|
1c9c22df0c | ||
|
684f52b7d4 | ||
|
5f11c0d603 | ||
|
d7044589f4 | ||
|
a97d1da3ab | ||
|
ea95c31939 | ||
|
cbe54c0827 | ||
|
82c8a7b7e8 | ||
|
0ff11b2cc1 | ||
|
ffae72cb0f | ||
|
d56ae71e0e | ||
|
df6a229f58 | ||
|
d921ee2245 | ||
|
b664055e3c | ||
|
3e211415c3 | ||
|
fd26dfecca | ||
|
7e64652fa7 | ||
|
b9f8ce9995 | ||
|
717477fd36 | ||
|
b021a8bf10 | ||
|
4829e31191 | ||
|
dbf108d9a4 | ||
|
d7404a7e1c | ||
|
af0337c26c | ||
|
f54f9f977e | ||
|
f43436056e | ||
|
178c7155a3 | ||
|
9c48de75d8 | ||
|
469ab53377 | ||
|
49198f9c11 | ||
|
8e4cca6981 | ||
|
4c4ed6eed2 | ||
|
41a660ba4f | ||
|
c62bdb171a | ||
|
53b7bdd4d5 | ||
|
1f446f6b64 | ||
|
6390d85b5f | ||
|
d16e7d1213 | ||
|
ad90efc508 | ||
|
1b546bb562 | ||
|
46af97219c | ||
|
4eacce80a5 | ||
|
7c3e69bb4a | ||
|
bbc18d6349 | ||
|
9d394d39ed | ||
|
eced2006e3 | ||
|
83e64104bc | ||
|
88bf03bedf | ||
|
48b9a5400b | ||
|
1e661e6d5b | ||
|
6dc71affca | ||
|
ca3347fb3d | ||
|
6559287bea | ||
|
5cba2b002b | ||
|
643544876b | ||
|
4561b0bc3c | ||
|
331374fabe | ||
|
bf82e750f4 | ||
|
fab361d32d | ||
|
b81c7e2fd7 | ||
|
34ee0790aa | ||
|
6de1ca7ed3 | ||
|
3b5d9b7392 | ||
|
dc8b722f72 | ||
|
8c1906eb6c | ||
|
c1d9c0e958 | ||
|
3c5c06c9f8 | ||
|
3ea203825a | ||
|
eaaf4bcb21 | ||
|
71e0c4813b | ||
|
87025d7a02 | ||
|
0f89576235 | ||
|
c716957a0d | ||
|
60656c713d | ||
|
b8f9e0efc8 | ||
|
e1784ea01b | ||
|
b4f6e7186a | ||
|
89346fa945 | ||
|
98b618cc9d | ||
|
a91e8aadb2 | ||
|
4b171ec7ff | ||
|
a2c0d70930 | ||
|
9ebcfbf333 | ||
|
0ce36c0f76 | ||
|
42a3cf8bb2 | ||
|
30c711886e | ||
|
34fab8786f | ||
|
7a2c8768ad | ||
|
a022bbe260 | ||
|
7255c1d204 | ||
|
4d19fbb58d | ||
|
04e98f9306 | ||
|
5fa3b689f0 | ||
|
3189ab6e81 | ||
|
786ee001b3 | ||
|
c556c1f164 | ||
|
0eb777cf5a | ||
|
1730de6cea | ||
|
ad34160083 | ||
|
6061d22fad | ||
|
54c5612ac8 | ||
|
e9b61b733d | ||
|
5e1ce7f9a7 | ||
|
2b892ec01a | ||
|
45f547c4be | ||
|
879767599e | ||
|
55f3349b39 | ||
|
daa6cfbb6a | ||
|
2903d4a66c | ||
|
7945de32eb | ||
|
44ad0a2f52 | ||
|
f7fc5bb0a3 | ||
|
2192c9d3b4 | ||
|
bde998ce50 | ||
|
a28455f0ce | ||
|
e2f27e77ce | ||
|
48c20471d5 | ||
|
11d17b8de9 | ||
|
d03c6cb26a | ||
|
86e983ef11 | ||
|
f62c11f851 | ||
|
392bfc0b1e | ||
|
2e13e4ce73 | ||
|
e40267e95d | ||
|
fe274ac6e7 | ||
|
fc20df294e | ||
|
9bb3e75fb9 | ||
|
c917e77687 | ||
|
ecac8002d9 | ||
|
d9257c2219 | ||
|
f4e7f127d9 | ||
|
60321edb8c | ||
|
61f76548bc | ||
|
e85799b9d6 | ||
|
ed4fa7fde4 | ||
|
321f418518 | ||
|
737bd459e4 | ||
|
84a2d8b513 | ||
|
0674826376 | ||
|
0bb626c653 | ||
|
f27173e2b9 | ||
|
3d26d2f27b | ||
|
dfaf59a59b | ||
|
13d3fd1cc8 | ||
|
83b0596242 | ||
|
7bf43241e5 | ||
|
815aa80789 | ||
|
08da51744b | ||
|
2e283b855a | ||
|
7dc2de0c6c | ||
|
d398d4a7dc | ||
|
65333d85ab | ||
|
04f85f6dbd | ||
|
244f6dd6f7 | ||
|
725eb0a093 | ||
|
f0793587f6 | ||
|
5c9bbc6818 | ||
|
1bea2ad279 | ||
|
b3138ad041 | ||
|
621907556d | ||
|
ba161a146d | ||
|
c91e480f09 | ||
|
d50abd74a1 | ||
|
026bbde403 | ||
|
09c30d73f1 | ||
|
ed3ef5f741 | ||
|
d130b29146 | ||
|
36bcd54306 | ||
|
a4a0a669bd | ||
|
e1c3491915 | ||
|
56962b1273 | ||
|
d4e9a56e54 | ||
|
b180517b29 | ||
|
6c6917077d | ||
|
896bda12f9 | ||
|
3d2792a1c3 | ||
|
4cba9d1e53 | ||
|
8e38d861b4 | ||
|
26634f591a | ||
|
c414f78248 | ||
|
ee71a4ab8a | ||
|
9efe4168b7 | ||
|
871f91d87f | ||
|
c58c6bf7fd | ||
|
3d1df3ca25 | ||
|
f34673b704 | ||
|
54586c6ecc | ||
|
957aac94ae | ||
|
dae38eb0a3 | ||
|
36f032ef15 | ||
|
2019cdb8cb | ||
|
a63991e325 | ||
|
5a7fcfad7f | ||
|
44d4a2a832 | ||
|
510c1cf2df | ||
|
5c82e6fe6c | ||
|
fbb2022b25 | ||
|
de796c95f0 | ||
|
bdfb24abbd | ||
|
d08268627e | ||
|
15d863a26c | ||
|
575d3534b1 | ||
|
ea74d87510 | ||
|
7701850586 | ||
|
92163c46b2 | ||
|
bdb0e72cc7 | ||
|
2d6a220c00 | ||
|
adfc25b123 | ||
|
e10f88da50 | ||
|
f1e3212477 | ||
|
70ef91fb4a | ||
|
5435ea1b7c | ||
|
8370fb8a11 | ||
|
660e325bbc | ||
|
7c79f73a4c | ||
|
a31b3c5c83 | ||
|
a2b3b70f3b | ||
|
e7d49c45da | ||
|
8e2b79a90c | ||
|
5ad93604ef | ||
|
3c226a05d5 | ||
|
163e8cf1a0 | ||
|
bd90d1d9a6 | ||
|
02935b7005 | ||
|
281aad2243 | ||
|
53194a64f3 | ||
|
ccdb704ca8 | ||
|
577c980a6d | ||
|
f6120fcf98 | ||
|
cb7741919d | ||
|
4206b53c09 | ||
|
7173790da2 | ||
|
acbd3066e8 | ||
|
39245734f3 | ||
|
bc5d05f5e8 | ||
|
9c0416b56d | ||
|
dc3c8fd049 | ||
|
205b6040fb | ||
|
5d992692f0 | ||
|
0e2635221d | ||
|
ded03ee9a9 | ||
|
2341f73106 | ||
|
6607377496 | ||
|
0e42c6b9b1 | ||
|
bbdf90d67c | ||
|
9ec6a02e13 | ||
|
a9b43da6cd | ||
|
bbf800f17f | ||
|
9cdf09d106 | ||
|
3f05e448d7 | ||
|
ba7186aa6e | ||
|
65dc7cc0eb | ||
|
6c4aadee4e | ||
|
666ec7d54d | ||
|
4bfd1f76f3 | ||
|
e0695b637f | ||
|
888f98e2f0 | ||
|
20a9eba4c8 | ||
|
89682aa6a8 | ||
|
bb6ec76951 | ||
|
800d3435eb | ||
|
cf59318ab4 | ||
|
116cf9bd3c | ||
|
3c82f7d82e | ||
|
f271ac8f75 | ||
|
96cfcff4cf | ||
|
09f332f31f | ||
|
5dd80e0706 | ||
|
cd2957679d | ||
|
dcf2befe52 | ||
|
4c0ba5c374 | ||
|
c01ef0e774 | ||
|
ad4597a40e | ||
|
625798c5db | ||
|
6c4108671f | ||
|
0b240b829e | ||
|
575efcab7e | ||
|
b3becb01c3 | ||
|
cbfd80d75f | ||
|
56895f35bc | ||
|
12dcca3f17 | ||
|
fa7cc9826d | ||
|
d2ad227a2f | ||
|
85cbbf5240 | ||
|
ac574cd112 | ||
|
9ddcae4ed2 | ||
|
bfedca6cfe | ||
|
5f9e923a04 | ||
|
776708bee6 | ||
|
95b2641056 | ||
|
ded3af31c1 | ||
|
5f5f0fe866 | ||
|
d9cd48287e | ||
|
f5eb91ebe2 | ||
|
4c584fd162 | ||
|
e729324cce | ||
|
3ea465907d | ||
|
03fda100d4 | ||
|
bcbf862ded | ||
|
b8f8705c47 | ||
|
160ae4d11c | ||
|
382b9f118c | ||
|
4c27d560a9 | ||
|
9202e8fddd | ||
|
af56755aaa | ||
|
90b9499909 | ||
|
9e665ae807 | ||
|
e5e29b18d1 | ||
|
1bfa20a656 | ||
|
c60c6f0dc6 | ||
|
1c7f9041e0 | ||
|
750b29b76c | ||
|
4dac77bb93 | ||
|
d58cdc41b7 | ||
|
49f73fbf61 | ||
|
bcae2423f5 | ||
|
53e948b8da | ||
|
4abb389269 | ||
|
9d4e427e14 | ||
|
21d7298843 | ||
|
90cb42fdf9 | ||
|
1b84dd1f6b | ||
|
a43ad8f2dd | ||
|
87f2c3b025 | ||
|
c25a3e13e3 | ||
|
1e778e9827 | ||
|
e2add3f2c8 | ||
|
c50aa09034 | ||
|
decc1e2029 | ||
|
92941d7f92 | ||
|
d1cc8eaec8 | ||
|
0aa5b15564 | ||
|
1823ca525e | ||
|
4d620cd737 | ||
|
8d2c025e47 | ||
|
f2a4699d13 | ||
|
5047ed480d | ||
|
1ad4e39cc9 | ||
|
7d3ab342d2 | ||
|
b84831c94f | ||
|
676ae87aed | ||
|
d98e76529d | ||
|
7a4fea8669 | ||
|
a15d60105b | ||
|
3eed2f69d9 | ||
|
a6adc81eb4 | ||
|
afc301de3a | ||
|
ce004a5bac | ||
|
119afdde2a | ||
|
28e3e3199c | ||
|
accd10dfea | ||
|
aa40eae581 | ||
|
e73e27cda4 | ||
|
13d4029f70 | ||
|
46dda60db9 | ||
|
e158f4ef88 | ||
|
e2a2a17f09 | ||
|
9cade5bbe0 | ||
|
05b807aff0 | ||
|
cbb1912e99 | ||
|
db8b2ad08b | ||
|
9b33c3627d | ||
|
6d2ab03f56 | ||
|
b1e85c7fa0 | ||
|
672cb730a8 | ||
|
9d659fbd00 | ||
|
84c40b872d | ||
|
317679fec3 | ||
|
edcb10820d | ||
|
e877da0ba3 | ||
|
9772adbfbf | ||
|
bc94cc999e | ||
|
dea2cb769d | ||
|
688357a474 | ||
|
dfbc0d3975 | ||
|
0221524a10 | ||
|
e8f5191ee7 | ||
|
bdadf25f5c | ||
|
ff470e9799 | ||
|
8eec141517 | ||
|
6aea804c6c | ||
|
5923adf333 | ||
|
a91d0d929c | ||
|
9390295281 | ||
|
ccfb8246be | ||
|
ef8adbf113 | ||
|
916c382171 | ||
|
5ca81246f1 | ||
|
064cc827a3 | ||
|
0f99994d9e | ||
|
23ed11e52f | ||
|
f638b0eef7 | ||
|
486e0e3420 | ||
|
0eab103066 | ||
|
78eebf7b15 | ||
|
9f75a1cecc | ||
|
7ba1e6f60d | ||
|
e1b96960b2 | ||
|
1895c72ca6 | ||
|
a5d49f57da | ||
|
b89a0d18dc | ||
|
88cb29b455 | ||
|
a049d9fada | ||
|
e5244fc36a | ||
|
1ae57967ae | ||
|
93756c392f | ||
|
dd5b24fcc9 | ||
|
d18dbb85d8 | ||
|
bc34fbd2eb | ||
|
12058a4c9a | ||
|
82841555f2 | ||
|
5def817f75 | ||
|
99836709fa | ||
|
120f85ac71 | ||
|
c4e51d3e8c | ||
|
f9e5d67478 | ||
|
98127948af | ||
|
2a6ee4b959 | ||
|
cecc0804d5 | ||
|
e90099bad3 | ||
|
69aefb15f7 | ||
|
ce2b148107 | ||
|
04bc1a6b65 | ||
|
074a82d8ad | ||
|
9897336896 | ||
|
8a4d517dec | ||
|
60a19826e6 | ||
|
b84388b0f3 | ||
|
429e3027b3 | ||
|
22982614d7 | ||
|
6888b08db2 | ||
|
fecf4bac2e | ||
|
e3a92edd45 | ||
|
5976c9c1e4 | ||
|
c5d5cdcd9d | ||
|
e708e42dcb | ||
|
a6812d852f | ||
|
50ee0c1a1d | ||
|
b7dba68ab9 | ||
|
9c051083b0 | ||
|
05d379bd3d | ||
|
187cd9f8b1 | ||
|
7e25b4fd84 | ||
|
3af5552e10 | ||
|
f2264b8a1e | ||
|
d0978473d3 | ||
|
63eab12132 | ||
|
ec56fee368 | ||
|
7d88c014f8 | ||
|
e7053bc046 | ||
|
5f9d649021 | ||
|
b05fb902f9 | ||
|
1084178e95 | ||
|
83cc8104a1 | ||
|
2c7775125a | ||
|
9d7e304fc6 | ||
|
202ff408e7 | ||
|
5496067925 | ||
|
33cb599464 | ||
|
b7e55836c1 | ||
|
120481269b | ||
|
5f020ad5b8 | ||
|
d48b3f18fc | ||
|
732100f293 | ||
|
9581a3695d | ||
|
6aa09149b9 | ||
|
5e10693aa6 | ||
|
183f197d32 | ||
|
b401b2f243 | ||
|
064cb52d0b | ||
|
06e5d7cb95 | ||
|
b17773a574 | ||
|
9119349c08 | ||
|
0e11ac87d3 | ||
|
da2332d814 | ||
|
892c812669 | ||
|
f2dca12de6 | ||
|
ccd607707d | ||
|
923f6e3a5c | ||
|
1e71767b40 | ||
|
8e3c6c39b7 | ||
|
842603482b | ||
|
7246b1b147 | ||
|
324b205926 | ||
|
0fcc0cb2d3 | ||
|
a844d765da | ||
|
7137d3d389 | ||
|
892e54ac4d | ||
|
b0b42f8f17 | ||
|
43eee4740b | ||
|
4b4a2798b6 | ||
|
367ca9b996 | ||
|
542f68dd07 | ||
|
59f004de24 | ||
|
c888094f68 | ||
|
1a20ec4c2c | ||
|
57d928ad96 | ||
|
060190d59a | ||
|
a0c5b24fb8 | ||
|
cd741f37be | ||
|
3ac1a6b288 | ||
|
bbdfae8cdd | ||
|
98c290602c | ||
|
d352a0c20d | ||
|
5e5007091e | ||
|
fa9f788190 | ||
|
33370e42ad | ||
|
66d4fd1d90 | ||
|
19d25d20a7 | ||
|
f27c2507c5 | ||
|
cd0726b037 | ||
|
34858762f7 | ||
|
8b00e80316 | ||
|
a4890d3295 | ||
|
fabcc65460 | ||
|
a500b5297b | ||
|
b91d23521f | ||
|
cd8bca11aa | ||
|
4ebcd78acc | ||
|
75eb959c80 | ||
|
5022ff412c | ||
|
4443e03cd2 | ||
|
2ad413773e | ||
|
32289047a5 | ||
|
9afe1d74b8 | ||
|
25d7c9e74f | ||
|
71d8f7108c | ||
|
861d216eed | ||
|
48f8c498fa | ||
|
a0c918de71 | ||
|
f149f8f1b5 | ||
|
13ce2da387 | ||
|
3742f8e776 | ||
|
cc63b187b9 | ||
|
a355220254 | ||
|
33fbc1c508 | ||
|
fbbd70950a | ||
|
f7fd1e3f99 | ||
|
20c02c4b38 | ||
|
caeff6f968 | ||
|
70385a4833 | ||
|
c483270f27 | ||
|
b41d5839d4 | ||
|
7b8070c55d | ||
|
bd1f8cb7d1 | ||
|
38e39d181e | ||
|
3acd848b5f | ||
|
8b4f0f91a9 | ||
|
d0e5868880 | ||
|
107183ad57 | ||
|
7ab4aff180 | ||
|
50730574ba | ||
|
7e93c20b38 | ||
|
924b89a880 | ||
|
acc4b6a7e2 | ||
|
91adc3c416 | ||
|
ce1ed81922 | ||
|
e20a1ce947 | ||
|
b4efa42d8f | ||
|
63a840e0e7 | ||
|
0a008354a4 | ||
|
2df6a5e049 | ||
|
e8d6a7cbd4 | ||
|
b1061fe90a | ||
|
d6f169866a | ||
|
ecbf7097de | ||
|
11bd909bf1 | ||
|
1f2d549a1e | ||
|
833b16def6 | ||
|
d6a28ead84 | ||
|
6936ce11a7 | ||
|
def42b5514 | ||
|
aff2b8ffc5 | ||
|
42d4d10b43 | ||
|
7679becc66 | ||
|
28265f1151 | ||
|
7948b266c5 | ||
|
294a888978 | ||
|
4c08971486 | ||
|
99aca7469e | ||
|
4327ad84e9 | ||
|
9af2823f58 | ||
|
288908edc0 | ||
|
50037325ff | ||
|
d12950a434 | ||
|
53e530f7db | ||
|
ea14f36a0d | ||
|
4eac148045 | ||
|
0a9536046b | ||
|
79cef6cbf7 | ||
|
dc45b7f8ac | ||
|
0f15b58453 | ||
|
a11550a59d | ||
|
f867673853 | ||
|
3e01736caf | ||
|
aa12b6bdc2 | ||
|
06bb9e1da7 | ||
|
a710cd0751 | ||
|
2fc89ced15 | ||
|
fbee00caab | ||
|
e70937a8d7 | ||
|
857800b7b6 | ||
|
2af9057200 | ||
|
a7d24506c2 | ||
|
c46ec64b03 | ||
|
150f672fde | ||
|
fda1233163 | ||
|
9bccc50add | ||
|
d7125aee68 | ||
|
cfe660ae82 | ||
|
2802a6fc97 | ||
|
81cde09641 | ||
|
8c03237949 | ||
|
6cc722af25 | ||
|
075cb97b3f | ||
|
8b49953a7a | ||
|
b6cd1cf6a9 | ||
|
8486fb0b41 | ||
|
c1eecbc5c4 | ||
|
34c7a541f6 | ||
|
83c53c6802 | ||
|
129f6a28b0 | ||
|
f5b88d37c5 | ||
|
6dc39b2dea | ||
|
fb56cdc87c | ||
|
50b9654af5 | ||
|
2794366186 | ||
|
2f3139dea2 | ||
|
16139c4565 | ||
|
547df0f042 | ||
|
dc2591ab45 | ||
|
7c98b2e830 | ||
|
761d7ae7ef | ||
|
a407346053 | ||
|
1bacf8237b | ||
|
f74b0b4088 | ||
|
c1f0b8e941 | ||
|
cdbd5c3c91 | ||
|
afe8e8e32b | ||
|
ff6e54c690 | ||
|
aa325ea98d | ||
|
5e80f2fab4 | ||
|
186ee43f8e | ||
|
09d7555653 | ||
|
aebbe8c39b | ||
|
e8c5884931 | ||
|
ca012cd4f0 | ||
|
7c1ca3ef55 | ||
|
c4885c6aab | ||
|
02655a9dce | ||
|
23071c1e0e | ||
|
01911b0ca7 | ||
|
a2582afed3 | ||
|
6286e496ba | ||
|
e1df2e2de5 | ||
|
288596b7a2 | ||
|
9db02cb55e | ||
|
2a656a98b6 | ||
|
3596870751 | ||
|
4c618394b7 | ||
|
b8d19920a8 | ||
|
45ebb4c629 | ||
|
ccc3652a1a | ||
|
d7fc5ca272 | ||
|
8c671ed7dc | ||
|
f5a398b21e | ||
|
873d177322 | ||
|
e1c32ecd61 | ||
|
45c765fb57 | ||
|
774110ec0a | ||
|
e930133bdf | ||
|
abe87fae12 | ||
|
4b4ae6d52c | ||
|
1e39927037 | ||
|
8ad7643ec3 | ||
|
7e68f84a43 | ||
|
bab4efb234 | ||
|
e77c23352a | ||
|
bddc4f325e | ||
|
04955f61d7 | ||
|
f374d7fedf | ||
|
a8215ad711 | ||
|
1f8465af66 | ||
|
5b0d79bc73 | ||
|
fb741f26f3 | ||
|
7d1f6b0bd4 | ||
|
d770cbf839 | ||
|
0792fe4f1a | ||
|
6bd4931120 | ||
|
10da397d9b | ||
|
44da5074bd | ||
|
127dbca2f5 | ||
|
b85fcdf649 | ||
|
d3ba5efff8 | ||
|
c12f216b59 | ||
|
6116729c5d | ||
|
3e38042d7f | ||
|
9a2b647443 | ||
|
678c5876de | ||
|
4812884453 | ||
|
e64a9b4a1a | ||
|
f06ac8c035 | ||
|
b0b3316616 | ||
|
9df0a01a0c | ||
|
e68bb9b4aa | ||
|
192253ab05 | ||
|
4d87c11293 | ||
|
3026e56cfb | ||
|
9750195caa | ||
|
4ac6863eed | ||
|
485839a2a9 | ||
|
38ca1ef3cb | ||
|
40426a2cf6 | ||
|
ab59e88809 | ||
|
fae723a238 | ||
|
32265412f3 | ||
|
8881548652 | ||
|
8081c319c2 | ||
|
66366ce024 | ||
|
8b7f0b40ea | ||
|
bcc0bb0d7d | ||
|
941b83a1d6 | ||
|
ab08cbd412 | ||
|
4fb18382c2 | ||
|
73a7be5ef5 | ||
|
6c32b702f0 | ||
|
20bec35c68 | ||
|
132986cf71 | ||
|
4e8c6fd293 | ||
|
fbc0a04cff | ||
|
55f9b84008 | ||
|
20149c7293 | ||
|
6827b9509e | ||
|
df86b67117 | ||
|
1ac85c91e5 | ||
|
ec3e0875a1 | ||
|
987f78de42 | ||
|
23b25b210b | ||
|
8cdb47e61e | ||
|
9adf663073 | ||
|
c5d901609f | ||
|
7ac9534322 | ||
|
51aa06d013 | ||
|
3a8ce35e60 | ||
|
135ec5ee7d | ||
|
0b54cdb8ea | ||
|
be0403ce24 | ||
|
8f9a42f486 | ||
|
da8390ef7b | ||
|
2dfca38977 | ||
|
b56e1a9873 | ||
|
7939f95861 | ||
|
eab6537094 | ||
|
3e82b5a14e | ||
|
34acbcc6a1 | ||
|
30b2df753c | ||
|
099093e9be | ||
|
a9d979a988 | ||
|
96bc778f72 | ||
|
88933790e7 | ||
|
28100d3a63 | ||
|
82ab78fa3d | ||
|
d52f1d49dd | ||
|
f5b3e9481f | ||
|
ab22d81f12 | ||
|
82cc63551c | ||
|
13e706f678 | ||
|
6ebc8988b2 | ||
|
b4d1145490 | ||
|
c26299277e | ||
|
811893ccf9 | ||
|
e03c27814b | ||
|
1e26ca6365 | ||
|
21f905739f | ||
|
7cd2945268 | ||
|
212b00ef2f | ||
|
613706d446 | ||
|
e269e51524 | ||
|
f53efc6e6f | ||
|
6aa9f642ba | ||
|
b939607693 | ||
|
299c69185e | ||
|
0fe6fbc859 | ||
|
1593c06595 | ||
|
a8e16d4815 | ||
|
e4acf61af3 | ||
|
cee861f341 | ||
|
46f80da72e | ||
|
87f94ddcc8 | ||
|
9a4aaa4e43 | ||
|
712ea76d7d | ||
|
97cc1c8458 | ||
|
ee3fc37f4c | ||
|
25501233ec | ||
|
20910b2415 | ||
|
c5c3726668 | ||
|
252b8498ff | ||
|
628f5099eb | ||
|
f2deb2264b | ||
|
162311d2f1 | ||
|
227080e32d | ||
|
cb1ea2195f | ||
|
8a81ca526b | ||
|
c93f7d41dc | ||
|
d1cec10925 | ||
|
00df152210 | ||
|
a8c9d78bff | ||
|
1a4f146318 | ||
|
6ff3cb8e74 | ||
|
b2ec1da345 | ||
|
22c7240bad | ||
|
f80a8ce18e | ||
|
90b5fa8937 | ||
|
72bd822525 | ||
|
86cdb6e5a2 | ||
|
c94727c872 | ||
|
533cc1148b | ||
|
56befc7f08 | ||
|
02f3b84bb3 | ||
|
d1ded51a96 | ||
|
104509fb79 | ||
|
a178945d80 | ||
|
440a353f93 | ||
|
a41f33ae08 | ||
|
8ce45f4f0d | ||
|
cf8b7db9bd | ||
|
6db4929e08 | ||
|
c669a99043 | ||
|
47453f380d | ||
|
45e2ef54fb | ||
|
00992c1825 | ||
|
c92d63ee88 | ||
|
1aaf429f9e | ||
|
1cabad0092 | ||
|
79fc47b41f | ||
|
046a0af6a1 | ||
|
1379e880a7 | ||
|
5fd1f2bcbe | ||
|
17193e3308 | ||
|
5cac22d1c0 | ||
|
2864ef4d99 | ||
|
85d0c75f9b | ||
|
aaaa593a72 | ||
|
4338068b3c | ||
|
3b4ae6c00e | ||
|
07a8b8a274 | ||
|
ba45931830 | ||
|
8bf4f8f935 | ||
|
5e55ed6b6c | ||
|
bf63a823cd | ||
|
d1f90d0fea | ||
|
0826906704 | ||
|
484c5186cd | ||
|
dcb3683232 | ||
|
bdfc95e6e8 | ||
|
6e33313b78 | ||
|
b3089ca047 | ||
|
0baf7ee839 | ||
|
98cd1bbc8a | ||
|
7cbf8e57a8 | ||
|
6ba47cc085 | ||
|
8399da4f7c | ||
|
570abeff49 | ||
|
75bba9f0e0 | ||
|
43db3e4477 | ||
|
893c217212 | ||
|
e5405a6657 | ||
|
a9960f1fc0 | ||
|
2e9a0242af | ||
|
e0a3fb393e | ||
|
5302c67f97 | ||
|
1ea558641e | ||
|
24f2cd57ad | ||
|
3e9b1a85ca | ||
|
e8ee6fd806 | ||
|
754373ba6c | ||
|
a89334a500 | ||
|
a1fc0efe3d | ||
|
c744353583 | ||
|
385351e5ad | ||
|
25af5afb2b | ||
|
48ce43ce06 | ||
|
389e25ae03 | ||
|
57dc9451cc | ||
|
3703999895 | ||
|
2c0e7a9192 | ||
|
88596f933f | ||
|
318656ea9b | ||
|
1f596d414d | ||
|
6803cb5fac | ||
|
80793cd766 | ||
|
9559742e19 | ||
|
51b21ef977 | ||
|
8bc3f12061 | ||
|
d74995ee7e | ||
|
a401427a0c | ||
|
31a48c4baa | ||
|
d667536ec6 | ||
|
bc8950303c | ||
|
53c692fac5 | ||
|
bf25388216 | ||
|
45fcccbd31 | ||
|
db5493d110 | ||
|
2d9feaa462 | ||
|
d813fa5681 | ||
|
ca4e498a0b | ||
|
491276bfbf | ||
|
1377916b3b | ||
|
892eb29c3b | ||
|
c25ad01561 | ||
|
2b09a48a5c | ||
|
227d750305 | ||
|
b0f9b7fae9 | ||
|
2aed66ba0f | ||
|
a3067f0dd5 | ||
|
8229082cc1 | ||
|
1d33719744 | ||
|
755fda63d3 | ||
|
98db99924e | ||
|
20b6ae5ff6 | ||
|
8d99d47f81 | ||
|
6239c2b6b9 | ||
|
3a28e7a9fb | ||
|
337337854b | ||
|
6c19cc8d9f | ||
|
f2f18ebb27 | ||
|
80dcf3c90c | ||
|
683253e8e7 | ||
|
c7103765db | ||
|
40f243dfc3 | ||
|
941a500c5f | ||
|
ef508c39d1 | ||
|
864e9b4520 | ||
|
7bffce7a6f | ||
|
d25b5006cb | ||
|
3db1c92663 | ||
|
58c1762e7d | ||
|
ceab3bae8a | ||
|
f3714f6fe7 | ||
|
bcb4617856 | ||
|
24e5c435a7 | ||
|
da433f92af | ||
|
e7b12d87f9 | ||
|
17270741e7 | ||
|
94555afbf4 | ||
|
75248c7f07 | ||
|
b7c06e0203 | ||
|
a0a01f3013 | ||
|
2996eaf287 | ||
|
d98c666bd9 | ||
|
9375503296 | ||
|
3f8adff757 | ||
|
e7cd12bf8a | ||
|
714301d584 | ||
|
20743b29c0 | ||
|
fc5e2ae03a | ||
|
b9f6d9dc34 | ||
|
8a41656f40 | ||
|
8df8b68555 | ||
|
3aa3d826c3 | ||
|
43d38d0951 | ||
|
3181631bb7 | ||
|
67edf375f3 | ||
|
09ffbb3872 | ||
|
000da42b5c |
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,7 @@
|
|||
target
|
||||
*~
|
||||
*.bk
|
||||
*.swp
|
||||
.vscode
|
||||
builddir
|
||||
.meson-subproject-wrap-hash.txt
|
||||
|
|
426
.gitlab-ci.yml
Normal file
426
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,426 @@
|
|||
.templates_sha: &templates_sha fddab8aa63e89a8e65214f59860d9c0f030360c9
|
||||
|
||||
include:
|
||||
- project: 'freedesktop/ci-templates'
|
||||
ref: *templates_sha
|
||||
file: '/templates/debian.yml'
|
||||
|
||||
- project: 'gstreamer/gstreamer-rs'
|
||||
ref: main
|
||||
file: '/ci/images_template.yml'
|
||||
|
||||
- project: 'gstreamer/gstreamer'
|
||||
ref: main
|
||||
file: '/.gitlab-image-tags.yml'
|
||||
|
||||
variables:
|
||||
FDO_UPSTREAM_REPO: gstreamer/gstreamer-rs
|
||||
|
||||
# We use GStreamer image to build the documentation as it is the simplest way
|
||||
# to ensure that we are testing against the same thing as GStreamer itself.
|
||||
# The tag name is included above from the main repo.
|
||||
GSTREAMER_DOC_IMAGE: "registry.freedesktop.org/gstreamer/gstreamer/amd64/fedora:$FEDORA_TAG-main"
|
||||
# Use the gstreamer image to trigger the cerbero job, same as the monorepo
|
||||
CERBERO_TRIGGER_IMAGE: "registry.freedesktop.org/gstreamer/gstreamer/amd64/fedora:$FEDORA_TAG-main"
|
||||
WINDOWS_BASE: "registry.freedesktop.org/gstreamer/gstreamer-rs/windows"
|
||||
WINDOWS_RUST_MINIMUM_IMAGE: "$WINDOWS_BASE:$GST_RS_IMG_TAG-main-$GST_RS_MSRV"
|
||||
WINDOWS_RUST_STABLE_IMAGE: "$WINDOWS_BASE:$GST_RS_IMG_TAG-main-$GST_RS_STABLE"
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_IID
|
||||
# don't create a pipeline if its a commit pipeline, on a branch and that branch has
|
||||
# open merge requests (bc we will get a MR build instead)
|
||||
- if: $CI_OPEN_MERGE_REQUESTS
|
||||
when: never
|
||||
- if: $CI_COMMIT_TAG
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
default:
|
||||
interruptible: true
|
||||
|
||||
stages:
|
||||
- "trigger"
|
||||
- "lint"
|
||||
- "test"
|
||||
- "extras"
|
||||
- "integration"
|
||||
|
||||
# This is an empty job that is used to trigger the pipeline.
|
||||
trigger:
|
||||
image: alpine:latest
|
||||
stage: 'trigger'
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
tags: [ 'placeholder-job' ]
|
||||
script:
|
||||
- echo "Trigger job done, now running the pipeline."
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
# If the MR is assigned to the Merge bot, trigger the pipeline automatically
|
||||
- if: '$CI_MERGE_REQUEST_ASSIGNEES == "gstreamer-merge-bot"'
|
||||
# Require explicit action to trigger tests post merge
|
||||
- if: '$CI_PROJECT_NAMESPACE == "gstreamer" && $CI_COMMIT_BRANCH == "main"'
|
||||
when: 'manual'
|
||||
# When the assignee isn't the merge bot, require an explicit action to trigger the pipeline
|
||||
# to avoid wasting CI resources
|
||||
- if: '$CI_MERGE_REQUEST_ASSIGNEES != "gstreamer-merge-bot"'
|
||||
when: 'manual'
|
||||
allow_failure: false
|
||||
|
||||
.debian:12:
|
||||
variables:
|
||||
SODIUM_USE_PKG_CONFIG: "true"
|
||||
after_script:
|
||||
- rm -rf target
|
||||
before_script:
|
||||
- source ./ci/env.sh
|
||||
- mkdir .cargo && echo -e "[net]\ngit-fetch-with-cli = true" > .cargo/config
|
||||
|
||||
.debian:12-stable:
|
||||
extends: .debian:12
|
||||
image: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bookworm-slim:$GST_RS_STABLE-$GST_RS_IMG_TAG"
|
||||
|
||||
.debian:12-msrv:
|
||||
extends: .debian:12
|
||||
image: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bookworm-slim:$GST_RS_MSRV-$GST_RS_IMG_TAG"
|
||||
|
||||
.debian:12-nightly:
|
||||
extends: .debian:12
|
||||
image: "registry.freedesktop.org/gstreamer/gstreamer-rs/debian/bookworm-slim:nightly-$GST_RS_IMG_TAG"
|
||||
|
||||
.cargo test:
|
||||
stage: "test"
|
||||
variables:
|
||||
# csound-sys only looks at /usr/lib and /usr/local top levels
|
||||
CSOUND_LIB_DIR: '/usr/lib/x86_64-linux-gnu/'
|
||||
RUST_BACKTRACE: 'full'
|
||||
script:
|
||||
- rustc --version
|
||||
|
||||
- cargo build --locked --color=always --workspace --all-targets
|
||||
- RUST_BACKTRACE=1 G_DEBUG=fatal_warnings cargo test --locked --color=always --workspace --all-targets
|
||||
- cargo build --locked --color=always --workspace --all-targets --all-features --exclude gst-plugin-gtk4
|
||||
- RUST_BACKTRACE=1 G_DEBUG=fatal_warnings cargo test --locked --color=always --workspace --all-targets --all-features --exclude gst-plugin-gtk4
|
||||
- cargo build --locked --color=always --workspace --all-targets --no-default-features
|
||||
- RUST_BACKTRACE=1 G_DEBUG=fatal_warnings cargo test --locked --color=always --workspace --all-targets --no-default-features
|
||||
|
||||
test msrv:
|
||||
extends:
|
||||
- '.cargo test'
|
||||
- '.debian:12-msrv'
|
||||
needs: [ "trigger" ]
|
||||
|
||||
test stable:
|
||||
extends:
|
||||
- '.cargo test'
|
||||
- '.debian:12-stable'
|
||||
needs: [ "trigger" ]
|
||||
|
||||
test nightly:
|
||||
allow_failure: true
|
||||
extends:
|
||||
- '.cargo test'
|
||||
- '.debian:12-nightly'
|
||||
needs: [ "trigger" ]
|
||||
|
||||
.meson:
|
||||
extends: .debian:12-stable
|
||||
variables:
|
||||
# csound-sys only looks at /usr/lib and /usr/local top levels
|
||||
CSOUND_LIB_DIR: '/usr/lib/x86_64-linux-gnu/'
|
||||
|
||||
meson shared:
|
||||
extends: .meson
|
||||
needs: [ "trigger" ]
|
||||
variables:
|
||||
CI_ARTIFACTS_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/raw/"
|
||||
script:
|
||||
- meson build --default-library=shared --prefix=$(pwd)/install --fatal-meson-warnings
|
||||
- ninja -C build install
|
||||
- ./ci/check-installed.py install
|
||||
- ninja -C build docs/gst_plugins_cache.json
|
||||
- ci/check-documentation-diff.py
|
||||
artifacts:
|
||||
when: always
|
||||
expire_in: "7 days"
|
||||
paths:
|
||||
- plugins-cache-diffs/
|
||||
- 'build/meson-logs/'
|
||||
|
||||
meson static:
|
||||
extends: .meson
|
||||
needs: [ "trigger" ]
|
||||
script:
|
||||
- meson build --default-library=static --prefix=$(pwd)/install -Dsodium-source=built-in
|
||||
- ninja -C build install
|
||||
- ./ci/generate-static-test.py test-static-link-all
|
||||
- cd test-static-link-all
|
||||
- PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$(pwd)/../install/lib/x86_64-linux-gnu/pkgconfig meson build
|
||||
- ninja -C build
|
||||
- ./build/test-gst-static
|
||||
artifacts:
|
||||
when: always
|
||||
expire_in: "7 days"
|
||||
paths:
|
||||
- 'build/meson-logs/'
|
||||
|
||||
# Check that the gstreamer documentation keeps working
|
||||
documentation:
|
||||
image: $GSTREAMER_DOC_IMAGE
|
||||
stage: 'integration'
|
||||
variables:
|
||||
MESON_ARGS: >
|
||||
-Ddoc=enabled
|
||||
-Dpython=disabled
|
||||
-Dlibav=disabled
|
||||
-Dlibnice=disabled
|
||||
-Ddevtools=disabled
|
||||
-Dges=disabled
|
||||
-Dsharp=disabled
|
||||
-Dgst-examples=disabled
|
||||
-Drs=enabled
|
||||
-Dgst-plugins-rs:sodium-source=system
|
||||
-Dgst-docs:fatal_warnings=true
|
||||
-Dorc=disabled
|
||||
script:
|
||||
- export PATH=/usr/local/cargo/bin/:/usr/local/bin/:$PATH
|
||||
- export RUSTUP_HOME='/usr/local/rustup'
|
||||
- P=$(pwd)
|
||||
- cd ..
|
||||
- rm -rf gstreamer
|
||||
- git clone --depth 1 https://gitlab.freedesktop.org/gstreamer/gstreamer.git --branch main
|
||||
- cd gstreamer
|
||||
- ln -s $P subprojects/gst-plugins-rs
|
||||
- meson build $MESON_ARGS
|
||||
- ./gst-env.py ninja -C build subprojects/gst-docs/GStreamer-doc
|
||||
- mv build/subprojects/gst-docs/GStreamer-doc/html $P/documentation/
|
||||
artifacts:
|
||||
expire_in: '7 days'
|
||||
when: always
|
||||
paths:
|
||||
- documentation/
|
||||
needs: []
|
||||
rules:
|
||||
# Run job if the MR is assigned to the Merge bot or it a post-merge pipeline on main branch
|
||||
- if: '$CI_MERGE_REQUEST_ASSIGNEES == "gstreamer-merge-bot"'
|
||||
when: 'always'
|
||||
- if: '$CI_PROJECT_NAMESPACE == "gstreamer" && $CI_COMMIT_BRANCH == "main"'
|
||||
when: 'always'
|
||||
# Require explicit action to trigger otherwise
|
||||
- if: '$CI_PROJECT_NAMESPACE != "gstreamer" || $CI_COMMIT_BRANCH != "main"'
|
||||
when: 'manual'
|
||||
|
||||
# build gst-plugins-rs as a gst-build subproject
|
||||
# Disabled because of https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/262
|
||||
#gst-build:
|
||||
# extends: .meson
|
||||
# rules:
|
||||
# - if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||
# - if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
# when: 'manual'
|
||||
# allow_failure: true
|
||||
# variables:
|
||||
# MESON_ARGS: >
|
||||
# -Domx=disabled
|
||||
# -Dpython=disabled
|
||||
# -Dlibav=disabled
|
||||
# -Dlibnice=disabled
|
||||
# -Dugly=disabled
|
||||
# -Dbad=disabled
|
||||
# -Ddevtools=disabled
|
||||
# -Dges=disabled
|
||||
# -Drtsp_server=disabled
|
||||
# -Dvaapi=disabled
|
||||
# -Dsharp=disabled
|
||||
# -Dgst-examples=disabled
|
||||
# -Drs=enabled
|
||||
# -Dgst-plugins-rs:sodium-source=system
|
||||
# script:
|
||||
# - P=$(pwd)
|
||||
# - cd ..
|
||||
# - rm -rf gstreamer
|
||||
# - git clone --depth 1 https://gitlab.freedesktop.org/gstreamer/gstreamer.git --branch main
|
||||
# - cd gstreamer
|
||||
# - ln -s $P subprojects/gst-plugins-rs
|
||||
# - meson build $MESON_ARGS
|
||||
# - ninja -C build
|
||||
# # Check static Rust plugins can be linked into gst-full
|
||||
# - meson build-gst-full --default-library=static $MESON_ARGS
|
||||
# - ninja -C build-gst-full
|
||||
# - meson devenv -C build-gst-full ./gst-inspect-1.0 rsaudiofx
|
||||
# artifacts:
|
||||
# expire_in: '7 days'
|
||||
# when: always
|
||||
# paths:
|
||||
# - 'build/meson-logs/'
|
||||
# - 'build-gst-full/meson-logs/'
|
||||
|
||||
.msvc2019 build:
|
||||
stage: 'test'
|
||||
needs:
|
||||
- 'trigger'
|
||||
tags:
|
||||
- 'docker'
|
||||
- 'windows'
|
||||
- '2022'
|
||||
script:
|
||||
# Set the code page to UTF-8
|
||||
- chcp 65001
|
||||
|
||||
# We need to build each crate separately to choose that can build on windows
|
||||
- cmd.exe /C "C:\BuildTools\Common7\Tools\VsDevCmd.bat -host_arch=amd64 -arch=amd64 &&
|
||||
powershell ./ci/run_windows_tests.ps1"
|
||||
|
||||
- |
|
||||
if (!$?) {
|
||||
Write-Host "Tests Failed!"
|
||||
Exit 1
|
||||
}
|
||||
|
||||
test windows msrv:
|
||||
extends: '.msvc2019 build'
|
||||
image: $WINDOWS_RUST_MINIMUM_IMAGE
|
||||
when: 'manual'
|
||||
|
||||
test windows stable:
|
||||
extends: '.msvc2019 build'
|
||||
image: "$WINDOWS_RUST_STABLE_IMAGE"
|
||||
|
||||
rustfmt:
|
||||
extends: '.debian:12-stable'
|
||||
stage: "lint"
|
||||
tags: [ 'placeholder-job' ]
|
||||
needs: []
|
||||
script:
|
||||
- cargo fmt --version
|
||||
- cargo fmt -- --color=always --check
|
||||
|
||||
typos:
|
||||
extends: '.debian:12-stable'
|
||||
stage: "lint"
|
||||
tags: [ 'placeholder-job' ]
|
||||
needs: []
|
||||
script:
|
||||
- typos
|
||||
|
||||
gstwebrtc-api lint:
|
||||
image: node:lts
|
||||
stage: "lint"
|
||||
tags: [ 'placeholder-job' ]
|
||||
needs: []
|
||||
script:
|
||||
- cd net/webrtc/gstwebrtc-api
|
||||
- npm install
|
||||
- npm run check
|
||||
|
||||
check commits:
|
||||
extends: '.debian:12-stable'
|
||||
stage: "lint"
|
||||
tags: [ 'placeholder-job' ]
|
||||
needs: []
|
||||
script:
|
||||
- ci-fairy check-commits --textwidth 0 --no-signed-off-by
|
||||
- ci/check-for-symlinks.sh
|
||||
- ci/check-meson-version.sh
|
||||
|
||||
clippy:
|
||||
extends: '.debian:12-stable'
|
||||
needs:
|
||||
- "trigger"
|
||||
- "test stable"
|
||||
stage: 'extras'
|
||||
variables:
|
||||
# csound-sys only looks at /usr/lib and /usr/local top levels
|
||||
CSOUND_LIB_DIR: '/usr/lib/x86_64-linux-gnu/'
|
||||
script:
|
||||
- cargo clippy --locked --color=always --all --all-targets -- -D warnings -A unknown-lints
|
||||
- cargo clippy --locked --color=always --all --all-features --all-targets --exclude gst-plugin-gtk4 -- -D warnings -A unknown-lints
|
||||
- cargo clippy --locked --color=always --all --all-targets --no-default-features -- -D warnings -A unknown-lints
|
||||
|
||||
deny:
|
||||
extends: .debian:12-stable
|
||||
stage: 'extras'
|
||||
needs:
|
||||
- "trigger"
|
||||
- "test stable"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||
script:
|
||||
- cargo update --color=always
|
||||
- cargo deny --color=always --workspace --all-features check all
|
||||
|
||||
outdated:
|
||||
extends: '.debian:12-stable'
|
||||
allow_failure: true
|
||||
needs:
|
||||
- "trigger"
|
||||
- "test stable"
|
||||
stage: 'extras'
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||
script:
|
||||
- cargo update --color=always
|
||||
# env_logger is ignored because it requires Rust >= 1.71
|
||||
- cargo outdated --color=always --root-deps-only --ignore env_logger --exit-code 1 -v
|
||||
|
||||
coverage:
|
||||
allow_failure: true
|
||||
extends:
|
||||
- '.debian:12-stable'
|
||||
needs:
|
||||
- "trigger"
|
||||
- "test stable"
|
||||
stage: 'extras'
|
||||
variables:
|
||||
RUSTFLAGS: "-Cinstrument-coverage"
|
||||
LLVM_PROFILE_FILE: "gst-plugins-rs-%p-%m.profraw"
|
||||
# csound-sys only looks at /usr/lib and /usr/local top levels
|
||||
CSOUND_LIB_DIR: '/usr/lib/x86_64-linux-gnu/'
|
||||
script:
|
||||
- cargo test --locked --color=always --all --all-features --exclude gst-plugin-gtk4
|
||||
# generate html report
|
||||
- grcov . --binary-path ./target/debug/ -s . -t html --branch --ignore-not-existing --ignore "*target*" --ignore "*/build.rs" -o ./coverage/
|
||||
# generate cobertura report for gitlab integration
|
||||
- grcov . --binary-path ./target/debug/ -s . -t cobertura --branch --ignore-not-existing --ignore "*target*" --ignore "*/build.rs" -o coverage.xml
|
||||
# output coverage summary for gitlab parsing.
|
||||
# TODO: use grcov once https://github.com/mozilla/grcov/issues/556 is fixed
|
||||
- grep "%" coverage/index.html | head -1 || true
|
||||
artifacts:
|
||||
paths:
|
||||
- 'coverage'
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
cerbero trigger:
|
||||
image: $CERBERO_TRIGGER_IMAGE
|
||||
needs: [ "trigger" ]
|
||||
variables:
|
||||
# We will build this cerbero branch in the cerbero trigger CI
|
||||
CERBERO_UPSTREAM_BRANCH: 'main'
|
||||
script:
|
||||
- ci/cerbero/trigger_cerbero_pipeline.py
|
||||
rules:
|
||||
# Never run post merge
|
||||
- if: '$CI_PROJECT_NAMESPACE == "gstreamer"'
|
||||
when: never
|
||||
# Don't run if the only changes are files that cargo-c does not read
|
||||
- if:
|
||||
changes:
|
||||
- "CHANGELOG.md"
|
||||
- "README.md"
|
||||
- "deny.toml"
|
||||
- "rustfmt.toml"
|
||||
- "typos.toml"
|
||||
- "*.py"
|
||||
- "*.sh"
|
||||
- "Makefile"
|
||||
- "meson.build"
|
||||
- "meson_options.txt"
|
||||
- "**/meson.build"
|
||||
- "ci/*.sh"
|
||||
- "ci/*.py"
|
||||
when: never
|
||||
- when: always
|
33
.gitlab/issue_templates/Bug.md
Normal file
33
.gitlab/issue_templates/Bug.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
### Describe your issue
|
||||
<!-- a clear and concise summary of the bug. -->
|
||||
<!-- For any GStreamer usage question, please contact the community using the #gstreamer channel on IRC https://www.oftc.net/ or the mailing list on https://gstreamer.freedesktop.org/lists/ -->
|
||||
|
||||
#### Expected Behavior
|
||||
<!-- What did you expect to happen -->
|
||||
|
||||
#### Observed Behavior
|
||||
<!-- What actually happened -->
|
||||
|
||||
#### Setup
|
||||
- **Operating System:**
|
||||
- **Device:** Computer / Tablet / Mobile / Virtual Machine <!-- Delete as appropriate !-->
|
||||
- **gst-plugins-rs Version:**
|
||||
- **GStreamer Version:**
|
||||
- **Command line:**
|
||||
|
||||
### Steps to reproduce the bug
|
||||
<!-- please fill in exact steps which reproduce the bug on your system, for example: -->
|
||||
1. open terminal
|
||||
2. type `command`
|
||||
|
||||
### How reproducible is the bug?
|
||||
<!-- The reproducibility of the bug is Always/Intermittent/Only once after doing a very specific set of steps-->
|
||||
|
||||
### Screenshots if relevant
|
||||
|
||||
### Solutions you have tried
|
||||
|
||||
### Related non-duplicate issues
|
||||
|
||||
### Additional Information
|
||||
<!-- Any other information such as logs. Make use of <details> for long output -->
|
34
.travis.yml
34
.travis.yml
|
@ -1,34 +0,0 @@
|
|||
dist: trusty
|
||||
sudo: required
|
||||
language: rust
|
||||
cache:
|
||||
cargo: true
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- liborc-0.4-dev
|
||||
- libglib2.0-dev
|
||||
- libxml2-dev
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
|
||||
script:
|
||||
- rustc --version
|
||||
- cargo build --all
|
||||
- cargo test --all
|
||||
|
||||
before_install:
|
||||
- curl -L https://people.freedesktop.org/~slomo/gstreamer.tar.gz | tar xz
|
||||
- sed -i "s;prefix=/root/gstreamer;prefix=$PWD/gstreamer;g" $PWD/gstreamer/lib/pkgconfig/*.pc
|
||||
- export PKG_CONFIG_PATH=$PWD/gstreamer/lib/pkgconfig
|
||||
- export GST_PLUGIN_SYSTEM_PATH=$PWD/gstreamer/lib/gstreamer-1.0
|
||||
- export GST_PLUGIN_SCANNER=$PWD/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner
|
||||
- export PATH=$PATH:$PWD/gstreamer/bin
|
||||
- export LD_LIBRARY_PATH=$PWD/gstreamer/lib:$LD_LIBRARY_PATH
|
457
CHANGELOG.md
Normal file
457
CHANGELOG.md
Normal file
|
@ -0,0 +1,457 @@
|
|||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html),
|
||||
specifically the [variant used by Rust](http://doc.crates.io/manifest.html#the-version-field).
|
||||
|
||||
## [0.12.6] - 2024-05-23
|
||||
### Fixed
|
||||
- Various Rust 1.78 clippy warnings.
|
||||
- gtk4paintablesink: Fix plugin description.
|
||||
|
||||
### Added
|
||||
- fmp4mux / mp4mux: Add support for adding AV1 header OBUs into the MP4
|
||||
headers.
|
||||
- fmp4mux / mp4mux: Take track language from the tags if provided.
|
||||
- gtk4paintablesink: Add GST_GTK4_WINDOW_FULLSCREEN environment variable to
|
||||
create a fullscreen window for debugging purposes.
|
||||
- gtk4paintablesink: Also create a window automatically when called from
|
||||
gst-play-1.0.
|
||||
- webrtc: Add support for insecure TLS connections.
|
||||
- webrtcsink: Add VP9 parser after the encoder.
|
||||
|
||||
### Changed
|
||||
- webrtcsink: Improve error when no discovery pipeline runs.
|
||||
- rtpgccbwe: Improve debug output in various places.
|
||||
|
||||
## [0.12.5] - 2024-04-29
|
||||
### Fixed
|
||||
- hrtfrender: Use a bitmask instead of an int in the caps for the channel-mask.
|
||||
- rtpgccbwe: Don't log an error when pushing a buffer list fails while stopping.
|
||||
- webrtcsink: Don't panic in bitrate handling with unsupported encoders.
|
||||
- webrtcsink: Don't panic if unsupported input caps are used.
|
||||
- webrtcsrc: Allow a `None` producer-id in `request-encoded-filter` signal.
|
||||
|
||||
### Added
|
||||
- aws: New property to support path-style addressing.
|
||||
- fmp4mux / mp4mux: Support FLAC instead (f)MP4.
|
||||
- gtk4: Support directly importing dmabufs with GTK 4.14.
|
||||
- gtk4: Add force-aspect-ratio property similar to other video sinks.
|
||||
|
||||
## [0.12.4] - 2024-04-08
|
||||
### Fixed
|
||||
- aws: Use fixed behaviour version to ensure that updates to the AWS SDK don't
|
||||
change any defaults configurations in unexpected ways.
|
||||
- onvifmetadataparse: Fix possible deadlock on shutdown.
|
||||
- webrtcsink: Set `perfect-timestamp=true` on audio encoders to work around
|
||||
bugs in Chrome's audio decoders.
|
||||
- Various clippy warnings.
|
||||
|
||||
### Changed
|
||||
- reqwest: Update to reqwest 0.12.
|
||||
- webrtchttp: Update to reqwest 0.12.
|
||||
|
||||
## [0.12.3] - 2024-03-21
|
||||
### Fixed
|
||||
- gtk4paintablesink: Fix scaling of texture position.
|
||||
- janusvrwebrtcsink: Handle 64 bit numerical room ids.
|
||||
- janusvrwebrtcsink: Don't include deprecated audio/video fields in publish
|
||||
messages.
|
||||
- janusvrwebrtcsink: Handle various other messages to avoid printing errors.
|
||||
- livekitwebrtc: Fix shutdown behaviour.
|
||||
- rtpgccbwe: Don't forward buffer lists with buffers from different SSRCs to
|
||||
avoid breaking assumptions in rtpsession.
|
||||
- sccparse: Ignore invalid timecodes during seeking.
|
||||
- webrtcsink: Don't try parsing audio caps as video caps.
|
||||
|
||||
### Changed
|
||||
- webrtc: Allow resolution and framerate changes.
|
||||
- webrtcsrc: Make produce-peer-id optional.
|
||||
|
||||
### Added
|
||||
- livekitwebrtcsrc: Add new LiveKit source element.
|
||||
- regex: Add support for configuring regex behaviour.
|
||||
- spotifyaudiosrc: Document how to use with non-Facebook accounts.
|
||||
- webrtcsrc: Add `do-retransmission` property.
|
||||
|
||||
## [0.12.2] - 2024-02-26
|
||||
### Fixed
|
||||
- rtpgccbwe: Don't reset PTS/DTS to `None` as otherwise `rtpsession` won't be
|
||||
able to generate valid RTCP.
|
||||
- webrtcsink: Fix usage with 1.22.
|
||||
|
||||
### Added
|
||||
- janusvrwebrtcsink: Add `secret-key` property.
|
||||
- janusvrwebrtcsink: Allow for string room ids and add `string-ids` property.
|
||||
- textwrap: Don't split on all whitespaces, especially not on non-breaking
|
||||
whitespace.
|
||||
|
||||
## [0.12.1] - 2024-02-13
|
||||
### Added
|
||||
- gtk4: Create a window for testing purposes when running in `gst-launch-1.0`
|
||||
or if `GST_GTK4_WINDOW=1` is set.
|
||||
- webrtcsink: Add `msid` property.
|
||||
|
||||
## [0.12.0] - 2024-02-08
|
||||
### Changed
|
||||
- ndi: `ndisrc` passes received data downstream without an additional copy, if
|
||||
possible.
|
||||
- webrtc: Cleanups to webrtcsrc/sink default signalling protocol, JavaScript
|
||||
implementation and server implementation.
|
||||
- webrtc: `whipwebrtcsink` is renamed to `whipclientsink` and deprecate old
|
||||
`whipsink`.
|
||||
|
||||
### Fixed
|
||||
- gtk4: Fix Windows build when using EGL.
|
||||
- gtk4: Fix ARGB pre-multiplication with GTK 4.14. This requires building with
|
||||
the `gtk_v4_10` or even better `gtk_v4_14` feature.
|
||||
- gtk4: Fix segfault if GTK3 is used in the same process.
|
||||
- gtk4: Always draw background behind the video frame and not only when
|
||||
borders have to be added to avoid glitches.
|
||||
- livekitwebrtcsink: Add high-quality layer for video streams.
|
||||
- webrtc: Fix potential hang and fd leak in signalling server.
|
||||
- webrtc: Fix closing of WebSockets.
|
||||
- webrtchttp: Allow setting `None` for audio/video caps for WHEP.
|
||||
|
||||
### Added
|
||||
- New `awss3putobjectsink` that works similar to `awss3sink` but with a
|
||||
different upload strategy.
|
||||
- New `hlscmafsink` element for writing HLS streams with CMAF/ISOBMFF
|
||||
fragments.
|
||||
- New `inter` plugin with `intersink` / `intersrc` elements that allow to
|
||||
connect different pipelines in the same process.
|
||||
- New `janusvrwebrtcsink` element for the Janus VideoRoom API.
|
||||
- New `rtspsrc2` element.
|
||||
- New `whipserversrc` element.
|
||||
- gtk4: New `background-color` property for setting the color of the
|
||||
background of the frame and the borders, if any.
|
||||
- gtk4: New `scale-filter` property for defining how to scale the frames.
|
||||
- livesync: Add support for image formats.
|
||||
- ndi: Closed Caption support in `ndisrc` / `ndisink`.
|
||||
- textwrap: Add support for gaps.
|
||||
- tracers: Optionally only show late buffers in `buffer-lateness` tracer.
|
||||
- webrtc: Add support for custom headers.
|
||||
- webrtcsink: New `payloader-setup` signal to configure payloader elements.
|
||||
- webrtcsrc: Support for navigation events.
|
||||
|
||||
## [0.11.3] - 2023-12-18
|
||||
### Fixed
|
||||
- ndi: Mark a private type as such and remove a wrong `Clone` impl of internal types.
|
||||
- uriplaylistbin: Fix a minor clippy warning.
|
||||
- fallbacksrc: Fix error during badly timed timeout scheduling.
|
||||
- webrtcsink: Fail gracefully if webrtcbin pads can't be requested instead of
|
||||
panicking.
|
||||
- threadshare: Fix deadlock in `ts-udpsrc` `notify::used-socket` signal
|
||||
emission.
|
||||
|
||||
### Changed
|
||||
- Update to AWS SDK 1.0.
|
||||
- Update to windows-sys 0.52.
|
||||
- Update to async-tungstenite 0.24.
|
||||
- Update to bitstream-io 2.0.
|
||||
- tttocea608: De-duplicate some functions.
|
||||
- gtk4: Use async-channel instead of deprecated GLib main context channel.
|
||||
|
||||
## [0.11.2] - 2023-11-11
|
||||
### Fixed
|
||||
- filesink / s3sink: Set `sync=false` to allow processing faster than
|
||||
real-time.
|
||||
- hlssink3: Various minor bugfixes and cleanups.
|
||||
- livesync: Various minor bugfixes and cleanups that should make the element
|
||||
work more reliable.
|
||||
- s3sink: Fix handling of non-ASCII characters in URIs and keys.
|
||||
- sccparse: Parse SCC files that are incorrectly created by CCExtractor.
|
||||
- ndisrc: Assume > 8 channels are unpositioned.
|
||||
- rtpav1depay: Skip unexpected leading fragments instead of repeatedly warning
|
||||
about the stream possibly being corrupted.
|
||||
- rtpav1depay: Don't push stale temporal delimiters downstream but wait until
|
||||
a complete OBU is collected.
|
||||
- whipwebrtcsink: Use correct URL during redirects.
|
||||
- webrtcsink: Make sure to not miss any ICE candidates.
|
||||
- webrtcsink: Fix deadlock when calling `set-local-description`.
|
||||
- webrtcsrc: Fix reference cycles that prevented the element from being freed.
|
||||
- webrtcsrc: Define signaller property as `CONSTRUCT_ONLY` to make it actually
|
||||
possible to set different signallers.
|
||||
- webrtc: Update livekit signaller to livekit 0.2.
|
||||
- meson: Various fixes to the meson-based build system.
|
||||
|
||||
### Added
|
||||
- audiornnoise: Attach audio level meta to output buffers.
|
||||
- hlssink3: Allow adding `EXT-X-PROGRAM-DATE-TIME` tag to the manifest.
|
||||
- webrtcsrc: Add `turn-servers` property.
|
||||
|
||||
### Changed
|
||||
- aws/webrtc: Update to AWS SDK 0.57/0.35.
|
||||
|
||||
## [0.11.1] - 2023-10-04
|
||||
### Fixed
|
||||
- fallbackswitch: Fix various deadlocks.
|
||||
- webrtcsink: Gracefully fail if adding the TWCC RTP header extension fails.
|
||||
- webrtcsink: Fix codec selection discovery.
|
||||
- webrtcsink: Add support for D3D11 memory and qsvh264enc.
|
||||
- onvifmetadataparse: Skip metadata frames with unrepresentable UTC times.
|
||||
- gtk4paintablesink: Pre-multiply alpha when creating GL textures with alpha.
|
||||
- gtk4paintablesink: Only support RGBA/RGB in the GL code path.
|
||||
- webrtchttp: Respect HTTP redirects.
|
||||
- fmp4mux: Specify unit of fragment-duration property.
|
||||
|
||||
### Changed
|
||||
- threadshare: Port to polling 3.1.
|
||||
|
||||
## [0.11.0] - 2023-08-10
|
||||
### Changed
|
||||
- Updated MSRV to 1.70.
|
||||
- Compatible with gtk-rs 0.18 and gstreamer-rs 0.21.
|
||||
- awstranscriber: Move to HTTP2-based API via the aws-sdk-transcribestreaming
|
||||
crate instead of our own implementation around the WebSocket API.
|
||||
|
||||
### Added
|
||||
- webrtcsink: Add AWS KVS signaller and corresponding aws-kvs-webrtcsink
|
||||
element.
|
||||
- awstranscriber / transcriberbin: Add support for translations and outputting
|
||||
transcriptions from a single audio stream in multiple languages at once.
|
||||
- gstwebrtc-api: JavaScript API for interacting with the default signalling
|
||||
protocol used by webrtcsink / webrtcsrc.
|
||||
- cea608to708: New element for converting CEA608 to CEA708 closed captions.
|
||||
- webrtcsink: Expose the signaller as property and allow implementing a
|
||||
custom signaller by connecting signal handlers to the default signaller.
|
||||
- webrtcsink: Add support for pre-encoded streams.
|
||||
- togglerecord: Add support for non-live input streams.
|
||||
- webrtcsink: New whipwebrtcsink that implements WHIP around webrtcsink.
|
||||
The existing whipsink still exists but will sooner or later be deprecated.
|
||||
- webrtcsink: Add LiveKit signaller and corresponding livekitwebrtcsink
|
||||
element.
|
||||
|
||||
## [0.10.11] - 2023-07-20
|
||||
### Fixed
|
||||
- fallbackswitch: Fix pad health calculation and notifies.
|
||||
- fallbackswitch: Change the threshold for trailing buffers.
|
||||
- webrtcsink: Fix pipeline when input caps contain a max-framerate field.
|
||||
- webrtcsink: Set VP8/VP9 payloader properties based on payloader element
|
||||
factory name.
|
||||
- webrtcsink: Set config-interval=-1 and aggregate-mode=zero-latency for
|
||||
H264/5 payloaders.
|
||||
- webrtcsink: Translate force-keyunit events to custom force-IDR API of NVIDIA
|
||||
encoders.
|
||||
- webrtcsink: Configure only 4 threads instead of 12 for x264enc for Chrome
|
||||
compatibility.
|
||||
- fmp4mux: Fix draining in chunk mode if keyframes are after the desired
|
||||
fragment end.
|
||||
|
||||
## [0.10.10] - 2023-07-05
|
||||
### Fixed
|
||||
- livesync: Improve EOS handling to be in sync with `queue`'s behaviour.
|
||||
- livesync: Wait for the end timestamp of the previous buffer before looking
|
||||
at queue to actually make use of the available latency.
|
||||
- webrtcsink: Avoid panic on unprepare from an async tokio context.
|
||||
- webrtc/signalling: Fix race condition in message ordering.
|
||||
- webrtcsink: Use the correct property types when configuring `nvvideoconvert`.
|
||||
- videofx: Minimize dependencies of the image crate.
|
||||
- togglerecord: Fix segment clipping to actually work as intended.
|
||||
|
||||
### Added
|
||||
- gtk4paintablesink: Support for WGL/EGL on Windows.
|
||||
- gtk4paintablesink: Add Python example application to the repository.
|
||||
|
||||
## [0.10.9] - 2023-06-19
|
||||
### Fixed
|
||||
- mp4mux/fmp4mux: Fix byte order in Opus extension box.
|
||||
- webrtcsrc: Add twcc extension to the codec-preferences when present.
|
||||
- webrtcsink: Don't try using cudaconvert if it is not present.
|
||||
- mccparse: Don't offset the first timecode to a zero PTS.
|
||||
- Correctly use MPL as license specifier instead of MPL-2 for plugins that
|
||||
compile with GStreamer < 1.20.
|
||||
|
||||
### Added
|
||||
- fallbackswitch: Add `stop-on-eos` property.
|
||||
|
||||
## [0.10.8] - 2023-06-07
|
||||
### Fixed
|
||||
- fmp4mux: Use updated start PTS when checking if a stream is filled instead
|
||||
of a stale one.
|
||||
- fmp4mux: Fix various issues with stream gaps, especially in the beginning.
|
||||
- fmp4mux: Fix waiting in live pipelines.
|
||||
- uriplaylistbin: Prevent deadlocks during property notifications.
|
||||
- webrtcsink: Fix panics during `twcc-stats` callback and related issues.
|
||||
- awstranscriber: Handle stream disconts correctly.
|
||||
- roundedcorners: Fix caps negotiation to not use I420 if a border radius is
|
||||
configured.
|
||||
- whipsink: Use the correct pad template to request pads from the internal
|
||||
webrtcbin.
|
||||
- fallbacksrc: Don't apply fallback audio caps to the main stream.
|
||||
- webrtcsrc: Fix caps handling during transceiver creation.
|
||||
|
||||
### Changed
|
||||
- rtpgccbwe: Improve packet handling.
|
||||
|
||||
## [0.10.7] - 2023-05-09
|
||||
### Fixed
|
||||
- ffv1dec: Drop rank until the implementation is feature-complete.
|
||||
- spotifyaudiosrc: Check cached credentials before use and fix usage of
|
||||
credentials cache.
|
||||
- tttocea608: Specify raw CEA608 field.
|
||||
- gtk4paintablesink: Fix compilation on non-Linux UNIX systems.
|
||||
- webrtcsrc: Don't set stun-server to the empty string if none was set.
|
||||
- webrtcsink: Abort statistics collection before stopping the signaller.
|
||||
- rtpgccbwe: Don't process empty lists.
|
||||
|
||||
### Changed
|
||||
- ndi: Update to libloading 0.8.
|
||||
- aws: Update to AWS SDK 0.55/0.27.
|
||||
- webrtcsink: Order pads by serial number.
|
||||
- Update to async-tungstenite 0.22.
|
||||
|
||||
### Added
|
||||
- webrtcsink/webrtcsrc: Add `request-encoded-filter` signal to add support for
|
||||
inserting custom filters between encoder/payloader or depayloader/decoder.
|
||||
This allows interacting with the "insertable streams" API from Chrome.
|
||||
|
||||
## [0.10.6] - 2023-04-06
|
||||
### Fixed
|
||||
- webrtcsink: Fix max/min-bitrate property blurb/nick.
|
||||
- uriplaylistbin: Add missing queues to example.
|
||||
- tttocea608: Fix pushing of caps events that sometimes contained unfixed caps.
|
||||
- tttocea608: Fix disappearing text after special character in non-popon mode.
|
||||
- transcriberbin: Fix deadlock on construction.
|
||||
- transcriberbin: Fix initial bin setup.
|
||||
- fallbacksrc: Handle incompatible downstream caps without panicking.
|
||||
- ndisrc: Fix copying of raw video frames with different NDI/GStreamer strides.
|
||||
- livesync: Correctly assume zero upstream latency if latency query fails.
|
||||
|
||||
### Added
|
||||
- webrtcsink: Add `ice-transport-policy` property that proxies the same
|
||||
`webrtcbin` property.
|
||||
|
||||
## [0.10.5] - 2023-03-19
|
||||
### Fixed
|
||||
- gtk4: Fix build with OpenGL support on macOS.
|
||||
- threadshare: Fix symbol conflicts when statically linking the plugin.
|
||||
|
||||
## [0.10.4] - 2023-03-14
|
||||
### Fixed
|
||||
- fmp4mux: Return a running time from `AggregatorImpl::next_time()` to fix
|
||||
waiting in live pipelines.
|
||||
- fmp4mux: Fix `hls_live` example to set properties on the right element.
|
||||
- uriplaylistbin: Reset element when switching back to `NULL` state.
|
||||
- livesync: Handle variable framerates correctly in fallback buffer duration
|
||||
calculation.
|
||||
- meson: Fix GStreamer version feature detection.
|
||||
|
||||
### Added
|
||||
- webrtc: New `webrtc` element.
|
||||
|
||||
## [0.10.3] - 2023-03-02
|
||||
### Added
|
||||
- tracers: `queue_levels` tracer now also supports printing the `appsrc` levels.
|
||||
- webrtc: `webrtcsink` can use `nvvidconv` if `nvvideoconvert` does not exist
|
||||
on an NVIDIA platform.
|
||||
|
||||
### Fixed
|
||||
- gtk4: Set the sync point on the video frame after mapping it as otherwise
|
||||
the frame might not be ready yet for further usage.
|
||||
- livesync: Correctly calculate the fallback buffer duration from the video
|
||||
framerate.
|
||||
- ndi: Handle caps changes correctly in `ndisinkcombiner`.
|
||||
|
||||
### Changed
|
||||
- webrtc: Minor cleanup.
|
||||
|
||||
## [0.10.2] - 2023-02-23
|
||||
### Fixed
|
||||
- hlssink3: Allow signal handlers to return `None`
|
||||
- gtk4: Make GL context sharing more reliable in pipelines with multiple
|
||||
`gtk4paintablesinks`
|
||||
- gtk4: Attach channel receiver to the main context from the correct thread to
|
||||
make it possible to start the sink from a different thread than the main
|
||||
thread without having retrieved the paintable from the main thread before.
|
||||
- fmp4mux/mp4mux: Ignore caps changes if only the framerate changes.
|
||||
|
||||
### Changed
|
||||
- gtk4: Simplify and refactor GL context sharing. Apart from being more
|
||||
reliable this reduces GL resource usage.
|
||||
|
||||
## [0.10.1] - 2023-02-13
|
||||
### Fixed
|
||||
- rtpav1pay: Fix calculation of Leb128 size size to work correctly with
|
||||
streams from certain encoders.
|
||||
|
||||
## [0.10.0] - 2023-02-10
|
||||
### Fixed
|
||||
- audiornnoise: Use correct value range for the samples
|
||||
- awss3sink: Treat stopping without EOS as an error for multipart upload
|
||||
- awss3hlssink: Fix the name of the hlssink child element
|
||||
- awss3hlssink: Fix deadlock on EOS
|
||||
- dav1d: Various fixes to improve performance, to handle decoding errors more
|
||||
gracefully and to make sure all frames are output in the end
|
||||
- fmp4mux: Various fixes to fragment splitting behaviour, output formatting
|
||||
and header generation
|
||||
- gtk4: Various stability and rendering fixes
|
||||
- meson: Various fixes and improvements to the meson-based build system
|
||||
- ndi: provide non-Linux/macOS UNIX fallback for the soname
|
||||
- ndisrc: Use default channel mask for audio output to allow >2 channels to
|
||||
work better
|
||||
- rav1e: Correctly enable threading support
|
||||
- rtpav1: Various fixes to the payloader and depayloader to handle streams
|
||||
more correctly and to handle errors more cleanly
|
||||
- rtpav1depay: Set caps on the source pad
|
||||
- spotify: fix "start a runtime from within a runtime" with static link
|
||||
- textahead: fix previous buffers
|
||||
- textwrap: Don't panic on empty buffers
|
||||
- tttocea608: Don't fail if a GAP event contains no duration
|
||||
- webrtchttp: whipsink: construct TURN URL correctly
|
||||
- webrtcsink: fix panic on pre-bwe request error
|
||||
- whipsink: Send ICE candidates together with the offer
|
||||
- whipsink: Various cleanups and minor fixes
|
||||
|
||||
### Added
|
||||
- audiornnoise: Add voice detection threshold property
|
||||
- awss3hlssink: Add `stats` property
|
||||
- awss3sink: Add properties to set Content-Type and Content-Disposition
|
||||
- fmp4mux: add 'offset-to-zero' property
|
||||
- fmp4mux/mp4mux: add support for muxing Opus, VP8, VP9 and AV1 streams
|
||||
- fmp4mux/mp4mux: Make media/track timescales configurable
|
||||
- fmp4mux: Add support for CMAF-style chunking, e.g. low-latency / LL HLS and DASH
|
||||
- gtk4: Support for rendering GL textures on X11/EGL, X11/GLX, Wayland and macOS
|
||||
- hlssink3: Allow generating i-frame-only playlist
|
||||
- livesync: New element that allows maintaining a contiguous live stream
|
||||
without gaps from a potentially unstable source.
|
||||
- mp4mux: New non-fragmented MP4 muxer element
|
||||
- spotifyaudiosrc: Support configurable bitrate
|
||||
- textahead: add settings to display previous buffers
|
||||
- threadshare: Introduce new ts-audiotestsrc
|
||||
- webrtcsink: Support nvv4l2vp9enc
|
||||
- whepsource: Add a WebRTC WHEP source element
|
||||
|
||||
### Changed
|
||||
- audiofx: Derive from AudioFilter where possible
|
||||
- dav1ddec: Lower rank to primary to allow usage of hardware decoders with
|
||||
higher ranks
|
||||
- fmp4mux: Only push `fragment_offset` if `write-mfra` is true to reduce memory usage
|
||||
- webrtcsink: Make the `turn-server` property a `turn-servers` list
|
||||
- webrtcsink: Move from async-std to tokio
|
||||
|
||||
[Unreleased]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.12.6...HEAD
|
||||
[0.12.6]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.12.5...0.12.6
|
||||
[0.12.5]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.12.4...0.12.5
|
||||
[0.12.4]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.12.3...0.12.4
|
||||
[0.12.3]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.12.2...0.12.3
|
||||
[0.12.2]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.12.1...0.12.2
|
||||
[0.12.1]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.12.0...0.12.1
|
||||
[0.12.0]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.11.3...0.12.0
|
||||
[0.11.3]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.11.2...0.11.3
|
||||
[0.11.2]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.11.1...0.11.2
|
||||
[0.11.1]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.11.0...0.11.1
|
||||
[0.11.0]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.11...0.11.0
|
||||
[0.10.11]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.10...0.10.11
|
||||
[0.10.10]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.9...0.10.10
|
||||
[0.10.9]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.8...0.10.9
|
||||
[0.10.8]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.7...0.10.8
|
||||
[0.10.7]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.6...0.10.7
|
||||
[0.10.6]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.5...0.10.6
|
||||
[0.10.5]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.4...0.10.5
|
||||
[0.10.4]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.3...0.10.4
|
||||
[0.10.3]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.2...0.10.3
|
||||
[0.10.2]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.1...0.10.2
|
||||
[0.10.1]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.10.0...0.10.1
|
||||
[0.10.0]: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/compare/0.9.0...0.10.0
|
7674
Cargo.lock
generated
Normal file
7674
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
152
Cargo.toml
152
Cargo.toml
|
@ -1,13 +1,112 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"gst-plugin",
|
||||
"gst-plugin-simple",
|
||||
"gst-plugin-file",
|
||||
"gst-plugin-http",
|
||||
"gst-plugin-flv",
|
||||
"gst-plugin-audiofx",
|
||||
"gst-plugin-togglerecord",
|
||||
"tutorial",
|
||||
"version-helper",
|
||||
|
||||
"audio/audiofx",
|
||||
"audio/claxon",
|
||||
"audio/csound",
|
||||
"audio/lewton",
|
||||
"audio/spotify",
|
||||
|
||||
"generic/file",
|
||||
"generic/originalbuffer",
|
||||
"generic/sodium",
|
||||
"generic/threadshare",
|
||||
"generic/inter",
|
||||
"generic/gopbuffer",
|
||||
|
||||
"mux/flavors",
|
||||
"mux/fmp4",
|
||||
"mux/mp4",
|
||||
|
||||
"net/aws",
|
||||
"net/hlssink3",
|
||||
"net/ndi",
|
||||
"net/onvif",
|
||||
"net/raptorq",
|
||||
"net/reqwest",
|
||||
"net/rtp",
|
||||
"net/rtsp",
|
||||
"net/webrtchttp",
|
||||
"net/webrtc",
|
||||
"net/webrtc/protocol",
|
||||
"net/webrtc/signalling",
|
||||
"net/quinn",
|
||||
|
||||
"text/ahead",
|
||||
"text/json",
|
||||
"text/regex",
|
||||
"text/wrap",
|
||||
|
||||
"utils/fallbackswitch",
|
||||
"utils/livesync",
|
||||
"utils/togglerecord",
|
||||
"utils/tracers",
|
||||
"utils/uriplaylistbin",
|
||||
|
||||
"video/cdg",
|
||||
"video/closedcaption",
|
||||
"video/dav1d",
|
||||
"video/ffv1",
|
||||
"video/gif",
|
||||
"video/gtk4",
|
||||
"video/hsv",
|
||||
"video/png",
|
||||
"video/rav1e",
|
||||
"video/videofx",
|
||||
"video/webp",
|
||||
]
|
||||
|
||||
# Only plugins without external dependencies
|
||||
default-members = [
|
||||
"version-helper",
|
||||
|
||||
"audio/audiofx",
|
||||
"audio/claxon",
|
||||
"audio/lewton",
|
||||
|
||||
"generic/originalbuffer",
|
||||
"generic/threadshare",
|
||||
"generic/inter",
|
||||
"generic/gopbuffer",
|
||||
|
||||
"mux/fmp4",
|
||||
"mux/mp4",
|
||||
|
||||
"net/aws",
|
||||
"net/hlssink3",
|
||||
"net/onvif",
|
||||
"net/raptorq",
|
||||
"net/reqwest",
|
||||
"net/rtp",
|
||||
"net/rtsp",
|
||||
"net/webrtchttp",
|
||||
"net/webrtc",
|
||||
"net/webrtc/protocol",
|
||||
"net/webrtc/signalling",
|
||||
"net/ndi",
|
||||
"net/quinn",
|
||||
|
||||
"text/ahead",
|
||||
"text/json",
|
||||
"text/regex",
|
||||
"text/wrap",
|
||||
|
||||
"utils/fallbackswitch",
|
||||
"utils/livesync",
|
||||
"utils/togglerecord",
|
||||
"utils/tracers",
|
||||
"utils/uriplaylistbin",
|
||||
|
||||
"video/cdg",
|
||||
"video/ffv1",
|
||||
"video/gif",
|
||||
"video/hsv",
|
||||
"video/png",
|
||||
"video/rav1e",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
@ -15,3 +114,42 @@ lto = true
|
|||
opt-level = 3
|
||||
debug = true
|
||||
panic = 'unwind'
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[workspace.package]
|
||||
version = "0.13.0-alpha.1"
|
||||
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
||||
[workspace.dependencies]
|
||||
once_cell = "1"
|
||||
glib = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
|
||||
gio = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
|
||||
cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master", features=["use_glib"] }
|
||||
pango = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
|
||||
pangocairo = { git = "https://github.com/gtk-rs/gtk-rs-core", branch = "master" }
|
||||
gtk = { package = "gtk4", git = "https://github.com/gtk-rs/gtk4-rs", branch = "master"}
|
||||
gdk-wayland = { package = "gdk4-wayland", git = "https://github.com/gtk-rs/gtk4-rs", branch = "master"}
|
||||
gdk-x11 = { package = "gdk4-x11", git = "https://github.com/gtk-rs/gtk4-rs", branch = "master"}
|
||||
gdk-win32 = { package = "gdk4-win32", git = "https://github.com/gtk-rs/gtk4-rs", branch = "master"}
|
||||
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-allocators = { package = "gstreamer-allocators", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-gl = { package = "gstreamer-gl", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-gl-egl = { package = "gstreamer-gl-egl", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-gl-wayland = { package = "gstreamer-gl-wayland", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-gl-x11 = { package = "gstreamer-gl-x11", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-net = { package = "gstreamer-net", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-pbutils = { package = "gstreamer-pbutils", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-plugin-version-helper = { path="./version-helper" }
|
||||
gst-rtp = { package = "gstreamer-rtp", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-sdp = { package = "gstreamer-sdp", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-utils = { package = "gstreamer-utils", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-video = { package = "gstreamer-video", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
gst-webrtc = { package = "gstreamer-webrtc", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main" }
|
||||
|
|
373
LICENSE-MPL-2.0
Normal file
373
LICENSE-MPL-2.0
Normal file
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
32
Makefile
32
Makefile
|
@ -1,5 +1,33 @@
|
|||
all:
|
||||
cargo build --all
|
||||
PLUGINS_DIR ?= $(shell pkg-config --variable=pluginsdir gstreamer-1.0)
|
||||
|
||||
OS=$(shell uname -s)
|
||||
ifeq ($(OS),Linux)
|
||||
SO_SUFFIX=so
|
||||
else
|
||||
ifeq ($(OS),Darwin)
|
||||
SO_SUFFIX=dylib
|
||||
else
|
||||
# FIXME: Bad hack, how to know we're on Windows?
|
||||
SO_SUFFIX=dll
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(DEBUG),1)
|
||||
CARGO_FLAGS=
|
||||
BUILD_DIR=target/debug
|
||||
else
|
||||
CARGO_FLAGS=--release
|
||||
BUILD_DIR=target/release
|
||||
endif
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
cargo build --all $(CARGO_FLAGS)
|
||||
|
||||
install: build
|
||||
install -d $(DESTDIR)$(PLUGINS_DIR)
|
||||
install -m 755 $(BUILD_DIR)/*.$(SO_SUFFIX) $(DESTDIR)$(PLUGINS_DIR)
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
|
226
README.md
226
README.md
|
@ -1,52 +1,208 @@
|
|||
# gst-plugin-rs [![crates.io](https://img.shields.io/crates/v/gst-plugin.svg)](https://crates.io/crates/gst-plugin) [![Build Status](https://travis-ci.org/sdroege/gst-plugin-rs.svg?branch=master)](https://travis-ci.org/sdroege/gst-plugin-rs)
|
||||
# gst-plugins-rs [![crates.io](https://img.shields.io/crates/v/gst-plugin.svg)](https://crates.io/crates/gst-plugin) [![pipeline status](https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/badges/main/pipeline.svg)](https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/commits/main)
|
||||
|
||||
Infrastructure for writing [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
plugins and elements in the [Rust programming
|
||||
language](https://www.rust-lang.org/), and a collection of various GStreamer
|
||||
plugins.
|
||||
Repository containing various [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
plugins and elements written in the [Rust programming
|
||||
language](https://www.rust-lang.org/).
|
||||
|
||||
Documentation for the crate containing the infrastructure for writing
|
||||
GStreamer plugins in Rust, [`gst-plugin`](gst-plugin), can be found
|
||||
[here](https://sdroege.github.io/rustdoc/gst-plugin/gst_plugin/). The whole
|
||||
API builds upon the [application-side GStreamer bindings](https://github.com/sdroege/gstreamer-rs).
|
||||
The plugins build upon the [GStreamer Rust bindings](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs).
|
||||
Check the README.md of that repository also for details about how to set-up
|
||||
your development environment.
|
||||
|
||||
Various example plugins can be found in the [GIT repository](https://github.com/sdroege/gst-plugin-rs/).
|
||||
## Plugins
|
||||
|
||||
For background and motivation, see the [announcement
|
||||
blogpost](https://coaxion.net/blog/2016/05/writing-gstreamer-plugins-and-elements-in-rust/)
|
||||
and the follow-up blogposts
|
||||
[1](https://coaxion.net/blog/2016/09/writing-gstreamer-elements-in-rust-part-2-dont-panic-we-have-better-assertions-now-and-other-updates/),
|
||||
[2](https://coaxion.net/blog/2016/11/writing-gstreamer-elements-in-rust-part-3-parsing-data-from-untrusted-sources-like-its-2016/),
|
||||
[3](https://coaxion.net/blog/2017/03/writing-gstreamer-elements-in-rust-part-4-logging-cows-and-plugins/).
|
||||
Note that the overall implementation has changed completely since those
|
||||
blogposts were written.
|
||||
You will find the following plugins in this repository:
|
||||
|
||||
* `generic`
|
||||
- `file`: A Rust implementation of the standard `filesrc` and `filesink` elements
|
||||
|
||||
- `sodium`: Elements to perform encryption and decryption using [libsodium](https://libsodium.org).
|
||||
|
||||
- `threadshare`: Some popular threaded elements reimplemented using common thread-sharing infrastructure.
|
||||
|
||||
* `net`
|
||||
|
||||
- `aws`: Various elements for Amazon AWS services using the [AWS SDK](https://awslabs.github.io/aws-sdk-rust/) library
|
||||
- `s3src`/`s3sink`: A source and sink element to talk to the Amazon S3 object storage system.
|
||||
- `s3hlssink`: A sink element to store HLS streams on Amazon S3.
|
||||
- `awstranscriber`: an element wrapping the AWS Transcriber service.
|
||||
- `awstranscribeparse`: an element parsing the packets of the AWS Transcriber service.
|
||||
|
||||
- `hlssink3`: An element for generating MPEG-TS HLS streams.
|
||||
|
||||
- `ndi`: An [NDI](https://www.newtek.com/ndi/) plugin containing a source, sink and device provider.
|
||||
|
||||
- `onvif`: Various elements for parsing, RTP (de)payloading, overlaying of ONVIF timed metadata.
|
||||
|
||||
- `quinn`: Transfer data over the network using QUIC
|
||||
- `quinnquicsink`/`quinnquicsrc`: Send and receive data using QUIC
|
||||
|
||||
- `raptorq`: Encoder/decoder element for RaptorQ RTP FEC mechanism.
|
||||
|
||||
- `reqwest`: An HTTP source element based on the [reqwest](https://github.com/seanmonstar/reqwest) library.
|
||||
|
||||
- `rtp`:
|
||||
- `rtpav1pay` / `rtpav1depay`: RTP (de)payloader for the AV1 video codec.
|
||||
|
||||
- `rtpgccbwe`: RTP bandwidth estimator based on the Google Congestion Control algorithm.
|
||||
|
||||
- `webrtc`: WebRTC elements, with batteries included Sink elements for specific signalling protocols.
|
||||
|
||||
- `webrtchttp`: Simple WebRTC HTTP elements (WHIP/WHEP).
|
||||
|
||||
* `audio`
|
||||
- `audiofx`: Elements to apply audio effects to a stream
|
||||
- `rsaudioecho`: a simple echo/reverb filter.
|
||||
- `audioloudnorm`: [audio normalization](http://k.ylo.ph/2016/04/04/loudnorm.html) filter.
|
||||
- `audiornnoise`: Filter for [removing noise](https://jmvalin.ca/demo/rnnoise/).
|
||||
- `ebur128level`: Filter for measuring audio loudness according to EBU R-128.
|
||||
- `hrtfrender`: Filter for rendering audio according to a [head-related transfer
|
||||
function](https://en.wikipedia.org/wiki/Head-related_transfer_function).
|
||||
|
||||
- `claxon`: A FLAC decoder based on the [Claxon](https://github.com/ruuda/claxon) library.
|
||||
|
||||
- `csound`: A plugin to implement audio effects using the [Csound](https://csound.com/) library.
|
||||
|
||||
- `lewton`: A Vorbis decoder based on the [lewton](https://github.com/RustAudio/lewton) library.
|
||||
|
||||
- `spotify`: A plugin to access content from [Spotify](https://www.spotify.com/) based on the [librespot](https://github.com/librespot-org/) library.
|
||||
|
||||
* `video`
|
||||
- `cdg`: A parser and renderer for [CD+G karaoke data](https://docs.rs/cdg/0.1.0/cdg/).
|
||||
|
||||
- `closedcaption`: Plugin to deal with closed caption streams
|
||||
- `ccdetect`: Detects if a stream contains active Closed Captions.
|
||||
- `cea608overlay`: Overlay CEA-608 / EIA-608 closed captions over a
|
||||
video stream.
|
||||
- `cea608tojson`: Convert CEA-608 / EIA-608 closed captions to a JSON
|
||||
stream.
|
||||
- `cea608tott`: Convert CEA-608 / EIA-608 closed captions to timed text.
|
||||
- `jsontovtt`: Convert JSON to timed text.
|
||||
- `mccenc`: Convert CEA-608 / EIA-608 and CEA-708 / EIA-708 closed captions to the MCC format.
|
||||
- `mccparse`: Parse CEA-608 / EIA-608 and CEA-708 / EIA-708 closed captions from the MCC format.
|
||||
- `sccenc`: Convert CEA-608 / EIA-608 closed captions to the MCC format.
|
||||
- `sccparse`: Parse CEA-608 / EIA-608 closed captions from the MCC format.
|
||||
- `transcriberbin`: Convenience bin around transcriber elements like `aws_transcriber`.
|
||||
- `tttocea608`: Convert timed text to CEA-608 / EIA-608 closed captions.
|
||||
- `tttojson`: Convert timed text to JSON.
|
||||
|
||||
- `dav1d`: AV1 decoder based on the [dav1d](https://code.videolan.org/videolan/dav1d) library.
|
||||
|
||||
- `ffv1`: FFV1 decoder based on the [ffv1](https://github.com/rust-av/ffv1) library.
|
||||
|
||||
- `gif`: A GIF encoder based on the [gif](https://github.com/image-rs/image-gif) library.
|
||||
|
||||
- `gtk4`: A [GTK4](https://www.gtk.org) video sink that provides a `GdkPaintable` for UI integration.
|
||||
|
||||
- `hsv`: Plugin with various elements to work with video data in hue, saturation, value format
|
||||
- `hsvdetector`: Mark pixels that are close to a configured color in HSV format.
|
||||
- `hsvfilter`: Apply various transformations in the HSV colorspace.
|
||||
|
||||
- `png`: PNG encoder based on the [png](https://github.com/image-rs/image-png) library.
|
||||
|
||||
- `rav1e`: AV1 encoder based on the [rav1e](https://github.com/xiph/rav1e) library.
|
||||
|
||||
- `videofx`: Plugin with various video filters.
|
||||
- `roundedcorners`: Element to make the corners of a video rounded via the alpha channel.
|
||||
- `colordetect`: A pass-through filter able to detect the dominant color(s) on incoming frames, using [color-thief](https://github.com/RazrFalcon/color-thief-rs).
|
||||
- `videocompare`: Compare similarity of video frames. The element can use different hashing algorithms like [Blockhash](https://github.com/commonsmachinery/blockhash-rfc), [DSSIM](https://kornel.ski/dssim), and others.
|
||||
|
||||
- `webp`: WebP decoder based on the [libwebp-sys-2](https://github.com/qnighy/libwebp-sys2-rs) library.
|
||||
|
||||
* `mux`
|
||||
- `flavors`: FLV demuxer based on the [flavors](https://github.com/rust-av/flavors) library.
|
||||
|
||||
- `fmp4`: A fragmented MP4/ISOBMFF/CMAF muxer for generating e.g. DASH/HLS media fragments.
|
||||
|
||||
- `mp4`: A non-fragmented MP4 muxer for generating MP4 files.
|
||||
|
||||
* `text`
|
||||
- `ahead`: A plugin to display upcoming text buffers ahead.
|
||||
|
||||
- `json`: A plugin to convert a stream of JSON objects to a higher level wrapped NDJSON output.
|
||||
|
||||
- `regex`: A regular expression text filter plugin.
|
||||
|
||||
- `wrap`: A plugin to perform text wrapping with hyphenation.
|
||||
|
||||
* `utils`
|
||||
- `fallbackswitch`:
|
||||
- `fallbackswitch`: An element that allows falling back to different
|
||||
sink pads after a timeout based on the sink pads' priorities.
|
||||
- `fallbacksrc`: Element similar to `urisourcebin` that allows
|
||||
configuring a fallback audio/video if there are problems with the main
|
||||
source.
|
||||
|
||||
- `livesync`: Element to maintain a continuous live stream from a
|
||||
potentially unstable source.
|
||||
|
||||
- `togglerecord`: Element to enable starting and stopping multiple streams together.
|
||||
|
||||
- `tracers`: Plugin with multiple tracers:
|
||||
- `buffer-lateness`: Records lateness of buffers and the reported
|
||||
latency for each pad in a CSV file. Contains a script for
|
||||
visualization.
|
||||
- `pipeline-snapshot`: Creates a .dot file of all pipelines in the
|
||||
application whenever requested.
|
||||
- `queue-levels`: Records queue levels for each queue in a CSV file.
|
||||
Contains a script for visualization.
|
||||
|
||||
- `uriplaylistbin`: Helper bin to gaplessly play a list of URIs.
|
||||
|
||||
## Building
|
||||
|
||||
gst-plugins-rs relies on [cargo-c](https://github.com/lu-zero/cargo-c/) to
|
||||
generate shared and static C libraries. It can be installed using:
|
||||
|
||||
```
|
||||
$ cargo install cargo-c
|
||||
```
|
||||
|
||||
Then you can easily build and test a specific plugin:
|
||||
|
||||
```
|
||||
$ cargo cbuild -p gst-plugin-cdg
|
||||
$ GST_PLUGIN_PATH="target/x86_64-unknown-linux-gnu/debug:$GST_PLUGIN_PATH" gst-inspect-1.0 cdgdec
|
||||
```
|
||||
|
||||
Replace `x86_64-unknown-linux-gnu` with your system's Rust target triple (`rustc -vV`).
|
||||
|
||||
The plugin can also be installed system-wide:
|
||||
|
||||
```
|
||||
$ cargo cbuild -p gst-plugin-cdg --prefix=/usr
|
||||
$ cargo cinstall -p gst-plugin-cdg --prefix=/usr
|
||||
```
|
||||
|
||||
This will install the plugin to `/usr/lib/gstreamer-1.0`.
|
||||
You can use `--libdir` to pass a custom `lib` directory
|
||||
such as `/usr/lib/x86_64-linux-gnu` for example.
|
||||
|
||||
Note that you can also just use `cargo` directly to build Rust static libraries
|
||||
and shared C libraries. `cargo-c` is mostly useful to build static C libraries
|
||||
and generate `pkg-config` files.
|
||||
|
||||
In case cargo complains about dependency versions after a `git pull`, `cargo update` may
|
||||
be able to resolve those.
|
||||
|
||||
## LICENSE
|
||||
|
||||
gst-plugin-rs and all crates contained in here that are not listed below are
|
||||
licensed under either of
|
||||
gst-plugins-rs and all crates contained in here are licensed under one of the
|
||||
following licenses
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
gst-plugin-togglerecord is licensed under the Lesser General Public License
|
||||
([LICENSE-LGPLv2](LICENSE-LGPLv2)) version 2.1 or (at your option) any later
|
||||
version.
|
||||
* Mozilla Public License Version 2.0 ([LICENSE-MPL-2.0](LICENSE-MPL-2.0) or http://opensource.org/licenses/MPL-2.0)
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
* Lesser General Public License ([LICENSE-LGPLv2](LICENSE-LGPLv2)) version 2.1 or (at your option) any later version
|
||||
|
||||
GStreamer itself is licensed under the Lesser General Public License version
|
||||
2.1 or (at your option) any later version:
|
||||
https://www.gnu.org/licenses/lgpl-2.1.html
|
||||
2.1 or (at your option) any later version: https://www.gnu.org/licenses/lgpl-2.1.html
|
||||
|
||||
## Contribution
|
||||
|
||||
Any kinds of contributions are welcome as a pull request.
|
||||
Any kinds of contributions are welcome as a merge request.
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in gst-plugin-rs by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
for inclusion in gst-plugins-rs by you shall be licensed under the license of
|
||||
the plugin it is added to.
|
||||
|
||||
For new plugins the MPL-2 license is preferred.
|
||||
|
|
55
audio/audiofx/Cargo.toml
Normal file
55
audio/audiofx/Cargo.toml
Normal file
|
@ -0,0 +1,55 @@
|
|||
[package]
|
||||
name = "gst-plugin-audiofx"
|
||||
version.workspace = true
|
||||
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
|
||||
repository.workspace = true
|
||||
license = "MPL-2.0"
|
||||
description = "GStreamer Rust Audio Effects Plugin"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gst = { workspace = true, features = ["v1_20"] }
|
||||
gst-base = { workspace = true, features = ["v1_20"] }
|
||||
gst-audio = { workspace = true, features = ["v1_20"] }
|
||||
anyhow = "1"
|
||||
byte-slice-cast = "1.0"
|
||||
num-traits = "0.2"
|
||||
ebur128 = "0.1"
|
||||
hrtf = "0.8"
|
||||
nnnoiseless = { version = "0.5", default-features = false }
|
||||
smallvec = "1"
|
||||
atomic_refcell = "0.1"
|
||||
rayon = "1.5"
|
||||
once_cell.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "gstrsaudiofx"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
gst-check = { workspace = true, features = ["v1_18"] }
|
||||
gst-app.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper.workspace = true
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
doc = ["gst/v1_18"]
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.9.21"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
import_library = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0"
|
373
audio/audiofx/LICENSE-MPL-2.0
Normal file
373
audio/audiofx/LICENSE-MPL-2.0
Normal file
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
3
audio/audiofx/build.rs
Normal file
3
audio/audiofx/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
146
audio/audiofx/examples/hrtfrender.rs
Normal file
146
audio/audiofx/examples/hrtfrender.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
// Copyright (C) 2021 Tomasz Andrzejak <andreiltd@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::{env, thread, time};
|
||||
|
||||
// Rotation in radians to apply to object position every 100 ms
|
||||
const ROTATION: f32 = 2.0 / 180.0 * std::f32::consts::PI;
|
||||
|
||||
fn run() -> Result<(), Error> {
|
||||
gst::init()?;
|
||||
gstrsaudiofx::plugin_register_static()?;
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
// Ircam binaries that the hrtf plugin is using can be downloaded from:
|
||||
// https://github.com/mrDIMAS/hrir_sphere_builder/tree/master/hrtf_base/IRCAM
|
||||
//
|
||||
// Ideally provide a mono input as this example is moving a single object. Otherwise
|
||||
// the input will be downmixed to mono with the audioconvert element.
|
||||
//
|
||||
// e.g.: hrtfrender 'file:///path/to/my/awesome/mono/wav/awesome.wav' IRC_1002_C.bin
|
||||
if args.len() != 3 {
|
||||
bail!("Usage: {} URI HRIR", args[0].clone());
|
||||
}
|
||||
|
||||
let uri = &args[1];
|
||||
let hrir = &args[2];
|
||||
|
||||
let pipeline = gst::parse::launch(&format!(
|
||||
"uridecodebin uri={uri} ! audioconvert ! audio/x-raw,channels=1 !
|
||||
hrtfrender hrir-file={hrir} name=hrtf ! audioresample ! autoaudiosink"
|
||||
))?
|
||||
.downcast::<gst::Pipeline>()
|
||||
.expect("type error");
|
||||
|
||||
let hrtf = pipeline.by_name("hrtf").expect("hrtf element not found");
|
||||
|
||||
// At the beginning put an object in front of listener
|
||||
let objs = [gst::Structure::builder("application/spatial-object")
|
||||
.field("x", 0f32)
|
||||
.field("y", 0f32)
|
||||
.field("z", 1f32)
|
||||
.field("distance-gain", 1f32)
|
||||
.build()];
|
||||
|
||||
hrtf.set_property("spatial-objects", gst::Array::new(objs));
|
||||
|
||||
let state_cond = Arc::new((Mutex::new(gst::State::Null), Condvar::new()));
|
||||
let state_cond_clone = Arc::clone(&state_cond);
|
||||
|
||||
thread::spawn(move || {
|
||||
// Wait for the pipeline to start up
|
||||
{
|
||||
let (lock, cvar) = &*state_cond_clone;
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
while *state != gst::State::Playing {
|
||||
state = cvar.wait(state).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
// get current object position and rotate it clockwise
|
||||
let s = hrtf.property::<gst::Array>("spatial-objects")[0]
|
||||
.get::<gst::Structure>()
|
||||
.expect("type error");
|
||||
|
||||
// positive values are on the right side of a listener
|
||||
let x = s.get::<f32>("x").expect("type error");
|
||||
// elevation, positive value is up
|
||||
let y = s.get::<f32>("y").expect("type error");
|
||||
// positive values are in front of a listener
|
||||
let z = s.get::<f32>("z").expect("type error");
|
||||
// gain
|
||||
let gain = s.get::<f32>("distance-gain").expect("type error");
|
||||
|
||||
// rotate clockwise: https://en.wikipedia.org/wiki/Rotation_matrix
|
||||
let new_x = x * f32::cos(ROTATION) + z * f32::sin(ROTATION);
|
||||
let new_z = -x * f32::sin(ROTATION) + z * f32::cos(ROTATION);
|
||||
|
||||
let objs = [gst::Structure::builder("application/spatial-object")
|
||||
.field("x", new_x)
|
||||
.field("y", y)
|
||||
.field("z", new_z)
|
||||
.field("distance-gain", gain)
|
||||
.build()];
|
||||
|
||||
hrtf.set_property("spatial-objects", gst::Array::new(objs));
|
||||
|
||||
thread::sleep(time::Duration::from_millis(100));
|
||||
}
|
||||
});
|
||||
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
let bus = pipeline.bus().unwrap();
|
||||
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||
use gst::MessageView;
|
||||
|
||||
match msg.view() {
|
||||
MessageView::StateChanged(state_changed) => {
|
||||
if state_changed.src().map(|s| s == &pipeline).unwrap_or(false)
|
||||
&& state_changed.current() == gst::State::Playing
|
||||
{
|
||||
let (lock, cvar) = &*state_cond;
|
||||
let mut state = lock.lock().unwrap();
|
||||
|
||||
*state = gst::State::Playing;
|
||||
cvar.notify_one();
|
||||
}
|
||||
}
|
||||
MessageView::Eos(..) => break,
|
||||
MessageView::Error(err) => {
|
||||
println!(
|
||||
"Error from {:?}: {} ({:?})",
|
||||
msg.src().map(|s| s.path_string()),
|
||||
err.error(),
|
||||
err.debug()
|
||||
);
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pipeline.set_state(gst::State::Null)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match run() {
|
||||
Ok(r) => r,
|
||||
Err(e) => eprintln!("Error! {e}"),
|
||||
}
|
||||
}
|
260
audio/audiofx/src/audioecho/imp.rs
Normal file
260
audio/audiofx/src/audioecho/imp.rs
Normal file
|
@ -0,0 +1,260 @@
|
|||
// Copyright (C) 2017,2018 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_audio::subclass::prelude::*;
|
||||
|
||||
use std::cmp;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
use num_traits::cast::{FromPrimitive, ToPrimitive};
|
||||
use num_traits::float::Float;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
static _CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"rsaudioecho",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("Rust Audio Echo Filter"),
|
||||
)
|
||||
});
|
||||
|
||||
use super::ring_buffer::RingBuffer;
|
||||
|
||||
const DEFAULT_MAX_DELAY: gst::ClockTime = gst::ClockTime::SECOND;
|
||||
const DEFAULT_DELAY: gst::ClockTime = gst::ClockTime::from_seconds(500);
|
||||
const DEFAULT_INTENSITY: f64 = 0.5;
|
||||
const DEFAULT_FEEDBACK: f64 = 0.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Settings {
|
||||
pub max_delay: gst::ClockTime,
|
||||
pub delay: gst::ClockTime,
|
||||
pub intensity: f64,
|
||||
pub feedback: f64,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
max_delay: DEFAULT_MAX_DELAY,
|
||||
delay: DEFAULT_DELAY,
|
||||
intensity: DEFAULT_INTENSITY,
|
||||
feedback: DEFAULT_FEEDBACK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
info: gst_audio::AudioInfo,
|
||||
buffer: RingBuffer,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AudioEcho {
|
||||
settings: Mutex<Settings>,
|
||||
state: Mutex<Option<State>>,
|
||||
}
|
||||
|
||||
impl AudioEcho {
|
||||
fn process<F: Float + ToPrimitive + FromPrimitive>(
|
||||
data: &mut [F],
|
||||
state: &mut State,
|
||||
settings: &Settings,
|
||||
) {
|
||||
let delay_frames = (settings.delay
|
||||
* (state.info.channels() as u64)
|
||||
* (state.info.rate() as u64))
|
||||
.seconds() as usize;
|
||||
|
||||
for (i, (o, e)) in data.iter_mut().zip(state.buffer.iter(delay_frames)) {
|
||||
let inp = (*i).to_f64().unwrap();
|
||||
let out = inp + settings.intensity * e;
|
||||
*o = inp + settings.feedback * e;
|
||||
*i = FromPrimitive::from_f64(out).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for AudioEcho {
|
||||
const NAME: &'static str = "GstRsAudioEcho";
|
||||
type Type = super::AudioEcho;
|
||||
type ParentType = gst_audio::AudioFilter;
|
||||
}
|
||||
|
||||
impl ObjectImpl for AudioEcho {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecUInt64::builder("max-delay")
|
||||
.nick("Maximum Delay")
|
||||
.blurb("Maximum delay of the echo in nanoseconds (can't be changed in PLAYING or PAUSED state)")
|
||||
.maximum(u64::MAX - 1)
|
||||
.default_value(DEFAULT_MAX_DELAY.nseconds())
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecUInt64::builder("delay")
|
||||
.nick("Delay")
|
||||
.blurb("Delay of the echo in nanoseconds")
|
||||
.maximum(u64::MAX - 1)
|
||||
.default_value(DEFAULT_DELAY.nseconds())
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("intensity")
|
||||
.nick("Intensity")
|
||||
.blurb("Intensity of the echo")
|
||||
.minimum(0.0)
|
||||
.maximum(1.0)
|
||||
.default_value(DEFAULT_INTENSITY)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("feedback")
|
||||
.nick("Feedback")
|
||||
.blurb("Amount of feedback")
|
||||
.minimum(0.0)
|
||||
.maximum(1.0)
|
||||
.default_value(DEFAULT_FEEDBACK)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"max-delay" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
if self.state.lock().unwrap().is_none() {
|
||||
settings.max_delay = value.get::<u64>().unwrap().nseconds();
|
||||
}
|
||||
}
|
||||
"delay" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.delay = value.get::<u64>().unwrap().nseconds();
|
||||
}
|
||||
"intensity" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.intensity = value.get().expect("type checked upstream");
|
||||
}
|
||||
"feedback" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.feedback = value.get().expect("type checked upstream");
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"max-delay" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.max_delay.to_value()
|
||||
}
|
||||
"delay" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.delay.to_value()
|
||||
}
|
||||
"intensity" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.intensity.to_value()
|
||||
}
|
||||
"feedback" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.feedback.to_value()
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for AudioEcho {}
|
||||
|
||||
impl ElementImpl for AudioEcho {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Audio echo",
|
||||
"Filter/Effect/Audio",
|
||||
"Adds an echo or reverb effect to an audio stream",
|
||||
"Sebastian Dröge <sebastian@centricular.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseTransformImpl for AudioEcho {
|
||||
const MODE: gst_base::subclass::BaseTransformMode =
|
||||
gst_base::subclass::BaseTransformMode::AlwaysInPlace;
|
||||
const PASSTHROUGH_ON_SAME_CAPS: bool = false;
|
||||
const TRANSFORM_IP_ON_PASSTHROUGH: bool = false;
|
||||
|
||||
fn transform_ip(&self, buf: &mut gst::BufferRef) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let mut settings = *self.settings.lock().unwrap();
|
||||
settings.delay = cmp::min(settings.max_delay, settings.delay);
|
||||
|
||||
let mut state_guard = self.state.lock().unwrap();
|
||||
let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?;
|
||||
|
||||
let mut map = buf.map_writable().map_err(|_| gst::FlowError::Error)?;
|
||||
|
||||
match state.info.format() {
|
||||
gst_audio::AUDIO_FORMAT_F64 => {
|
||||
let data = map.as_mut_slice_of::<f64>().unwrap();
|
||||
Self::process(data, state, &settings);
|
||||
}
|
||||
gst_audio::AUDIO_FORMAT_F32 => {
|
||||
let data = map.as_mut_slice_of::<f32>().unwrap();
|
||||
Self::process(data, state, &settings);
|
||||
}
|
||||
_ => return Err(gst::FlowError::NotNegotiated),
|
||||
}
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
// Drop state
|
||||
let _ = self.state.lock().unwrap().take();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFilterImpl for AudioEcho {
|
||||
fn allowed_caps() -> &'static gst::Caps {
|
||||
static CAPS: Lazy<gst::Caps> = Lazy::new(|| {
|
||||
gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format_list([gst_audio::AUDIO_FORMAT_F32, gst_audio::AUDIO_FORMAT_F64])
|
||||
.build()
|
||||
});
|
||||
|
||||
&CAPS
|
||||
}
|
||||
|
||||
fn setup(&self, info: &gst_audio::AudioInfo) -> Result<(), gst::LoggableError> {
|
||||
let max_delay = self.settings.lock().unwrap().max_delay;
|
||||
let size = (max_delay * (info.rate() as u64)).seconds() as usize;
|
||||
let buffer_size = size * (info.channels() as usize);
|
||||
|
||||
*self.state.lock().unwrap() = Some(State {
|
||||
info: info.clone(),
|
||||
buffer: RingBuffer::new(buffer_size),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
26
audio/audiofx/src/audioecho/mod.rs
Normal file
26
audio/audiofx/src/audioecho/mod.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright (C) 2017,2018 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
mod ring_buffer;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct AudioEcho(ObjectSubclass<imp::AudioEcho>) @extends gst_base::BaseTransform, gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"rsaudioecho",
|
||||
gst::Rank::NONE,
|
||||
AudioEcho::static_type(),
|
||||
)
|
||||
}
|
84
audio/audiofx/src/audioecho/ring_buffer.rs
Normal file
84
audio/audiofx/src/audioecho/ring_buffer.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Copyright (C) 2017,2018 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::iter;
|
||||
|
||||
pub struct RingBuffer {
|
||||
buffer: Box<[f64]>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl RingBuffer {
|
||||
pub fn new(size: usize) -> Self {
|
||||
let mut buffer = Vec::with_capacity(size);
|
||||
buffer.extend(iter::repeat(0.0).take(size));
|
||||
|
||||
Self {
|
||||
buffer: buffer.into_boxed_slice(),
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&mut self, delay: usize) -> RingBufferIter {
|
||||
RingBufferIter::new(self, delay)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RingBufferIter<'a> {
|
||||
buffer: &'a mut [f64],
|
||||
buffer_pos: &'a mut usize,
|
||||
read_pos: usize,
|
||||
write_pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> RingBufferIter<'a> {
|
||||
fn new(buffer: &'a mut RingBuffer, delay: usize) -> RingBufferIter<'a> {
|
||||
let size = buffer.buffer.len();
|
||||
|
||||
assert!(size >= delay);
|
||||
assert_ne!(size, 0);
|
||||
|
||||
let read_pos = (size - delay + buffer.pos) % size;
|
||||
let write_pos = buffer.pos % size;
|
||||
|
||||
let buffer_pos = &mut buffer.pos;
|
||||
let buffer = &mut buffer.buffer;
|
||||
|
||||
RingBufferIter {
|
||||
buffer,
|
||||
buffer_pos,
|
||||
read_pos,
|
||||
write_pos,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RingBufferIter<'a> {
|
||||
type Item = (&'a mut f64, f64);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let res = unsafe {
|
||||
let r = *self.buffer.get_unchecked(self.read_pos);
|
||||
let w = self.buffer.get_unchecked_mut(self.write_pos);
|
||||
// Cast needed to get from &mut f64 to &'a mut f64
|
||||
(&mut *(w as *mut f64), r)
|
||||
};
|
||||
|
||||
let size = self.buffer.len();
|
||||
self.write_pos = (self.write_pos + 1) % size;
|
||||
self.read_pos = (self.read_pos + 1) % size;
|
||||
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for RingBufferIter<'a> {
|
||||
fn drop(&mut self) {
|
||||
*self.buffer_pos = self.write_pos;
|
||||
}
|
||||
}
|
1902
audio/audiofx/src/audioloudnorm/imp.rs
Normal file
1902
audio/audiofx/src/audioloudnorm/imp.rs
Normal file
File diff suppressed because it is too large
Load diff
25
audio/audiofx/src/audioloudnorm/mod.rs
Normal file
25
audio/audiofx/src/audioloudnorm/mod.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright (C) 2019-2020 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct AudioLoudNorm(ObjectSubclass<imp::AudioLoudNorm>) @extends gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"audioloudnorm",
|
||||
gst::Rank::NONE,
|
||||
AudioLoudNorm::static_type(),
|
||||
)
|
||||
}
|
429
audio/audiofx/src/audiornnoise/imp.rs
Normal file
429
audio/audiofx/src/audiornnoise/imp.rs
Normal file
|
@ -0,0 +1,429 @@
|
|||
// Copyright (C) 2020 Philippe Normand <philn@igalia.com>
|
||||
// Copyright (C) 2020 Natanael Mojica <neithanmo@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_audio::subclass::prelude::*;
|
||||
use gst_base::prelude::*;
|
||||
use gst_base::subclass::base_transform::BaseTransformImplExt;
|
||||
use gst_base::subclass::base_transform::GenerateOutputSuccess;
|
||||
|
||||
use nnnoiseless::DenoiseState;
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use atomic_refcell::AtomicRefCell;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"audiornnoise",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("Rust Audio Denoise Filter"),
|
||||
)
|
||||
});
|
||||
|
||||
const DEFAULT_VOICE_ACTIVITY_THRESHOLD: f32 = 0.0;
|
||||
const FRAME_SIZE: usize = DenoiseState::FRAME_SIZE;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Settings {
|
||||
vad_threshold: f32,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
vad_threshold: DEFAULT_VOICE_ACTIVITY_THRESHOLD,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelDenoiser {
|
||||
denoiser: Box<DenoiseState<'static>>,
|
||||
frame_chunk: Box<[f32; FRAME_SIZE]>,
|
||||
out_chunk: Box<[f32; FRAME_SIZE]>,
|
||||
}
|
||||
|
||||
struct State {
|
||||
in_info: gst_audio::AudioInfo,
|
||||
denoisers: Vec<ChannelDenoiser>,
|
||||
adapter: gst_base::UniqueAdapter,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AudioRNNoise {
|
||||
settings: Mutex<Settings>,
|
||||
state: AtomicRefCell<Option<State>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
// The following three functions are copied from the csound filter.
|
||||
fn buffer_duration(&self, buffer_size: u64) -> Option<gst::ClockTime> {
|
||||
let samples = buffer_size / self.in_info.bpf() as u64;
|
||||
self.samples_to_time(samples)
|
||||
}
|
||||
|
||||
fn samples_to_time(&self, samples: u64) -> Option<gst::ClockTime> {
|
||||
samples
|
||||
.mul_div_round(*gst::ClockTime::SECOND, self.in_info.rate() as u64)
|
||||
.map(gst::ClockTime::from_nseconds)
|
||||
}
|
||||
|
||||
fn current_pts(&self) -> Option<gst::ClockTime> {
|
||||
// get the last seen pts and the amount of bytes
|
||||
// since then
|
||||
let (prev_pts, distance) = self.adapter.prev_pts();
|
||||
|
||||
// Use the distance to get the amount of samples
|
||||
// and with it calculate the time-offset which
|
||||
// can be added to the prev_pts to get the
|
||||
// pts at the beginning of the adapter.
|
||||
let samples = distance / self.in_info.bpf() as u64;
|
||||
prev_pts
|
||||
.opt_checked_add(self.samples_to_time(samples))
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn needs_more_data(&self) -> bool {
|
||||
self.adapter.available() < (FRAME_SIZE * self.in_info.bpf() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioRNNoise {
|
||||
fn drain(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let mut state_lock = self.state.borrow_mut();
|
||||
let state = state_lock.as_mut().unwrap();
|
||||
|
||||
let available = state.adapter.available();
|
||||
if available == 0 {
|
||||
return Ok(gst::FlowSuccess::Ok);
|
||||
}
|
||||
|
||||
let settings = *self.settings.lock().unwrap();
|
||||
let mut buffer = gst::Buffer::with_size(available).map_err(|e| {
|
||||
gst::error!(CAT, imp: self, "Failed to allocate buffer at EOS {:?}", e);
|
||||
gst::FlowError::Flushing
|
||||
})?;
|
||||
|
||||
let duration = state.buffer_duration(available as _);
|
||||
let pts = state.current_pts();
|
||||
|
||||
{
|
||||
let ibuffer = state.adapter.take_buffer(available).unwrap();
|
||||
let in_map = ibuffer.map_readable().map_err(|_| gst::FlowError::Error)?;
|
||||
let in_data = in_map.as_slice_of::<f32>().unwrap();
|
||||
|
||||
let buffer = buffer.get_mut().unwrap();
|
||||
buffer.set_duration(duration);
|
||||
buffer.set_pts(pts);
|
||||
|
||||
let (level, has_voice) = {
|
||||
let mut out_map = buffer.map_writable().map_err(|_| gst::FlowError::Error)?;
|
||||
let out_data = out_map.as_mut_slice_of::<f32>().unwrap();
|
||||
self.process(state, &settings, in_data, out_data)
|
||||
};
|
||||
|
||||
gst_audio::AudioLevelMeta::add(buffer, level, has_voice);
|
||||
}
|
||||
|
||||
self.obj().src_pad().push(buffer)
|
||||
}
|
||||
|
||||
fn generate_output(&self, state: &mut State) -> Result<GenerateOutputSuccess, gst::FlowError> {
|
||||
let available = state.adapter.available();
|
||||
let bpf = state.in_info.bpf() as usize;
|
||||
let output_size = available - (available % (FRAME_SIZE * bpf));
|
||||
let duration = state.buffer_duration(output_size as _);
|
||||
let pts = state.current_pts();
|
||||
|
||||
let settings = *self.settings.lock().unwrap();
|
||||
let mut buffer = gst::Buffer::with_size(output_size).map_err(|_| gst::FlowError::Error)?;
|
||||
|
||||
{
|
||||
let ibuffer = state
|
||||
.adapter
|
||||
.take_buffer(output_size)
|
||||
.map_err(|_| gst::FlowError::Error)?;
|
||||
let in_map = ibuffer.map_readable().map_err(|_| gst::FlowError::Error)?;
|
||||
let in_data = in_map.as_slice_of::<f32>().unwrap();
|
||||
|
||||
let buffer = buffer.get_mut().unwrap();
|
||||
buffer.set_duration(duration);
|
||||
buffer.set_pts(pts);
|
||||
|
||||
let (level, has_voice) = {
|
||||
let mut out_map = buffer.map_writable().map_err(|_| gst::FlowError::Error)?;
|
||||
let out_data = out_map.as_mut_slice_of::<f32>().unwrap();
|
||||
self.process(state, &settings, in_data, out_data)
|
||||
};
|
||||
|
||||
gst_audio::AudioLevelMeta::add(buffer, level, has_voice);
|
||||
}
|
||||
|
||||
Ok(GenerateOutputSuccess::Buffer(buffer))
|
||||
}
|
||||
|
||||
fn process(
|
||||
&self,
|
||||
state: &mut State,
|
||||
settings: &Settings,
|
||||
input_plane: &[f32],
|
||||
output_plane: &mut [f32],
|
||||
) -> (u8, bool) {
|
||||
let channels = state.in_info.channels() as usize;
|
||||
let size = FRAME_SIZE * channels;
|
||||
let mut has_voice = false;
|
||||
|
||||
for (out_frame, in_frame) in output_plane.chunks_mut(size).zip(input_plane.chunks(size)) {
|
||||
for (index, item) in in_frame.iter().enumerate() {
|
||||
let channel_index = index % channels;
|
||||
let channel_denoiser = &mut state.denoisers[channel_index];
|
||||
let pos = index / channels;
|
||||
channel_denoiser.frame_chunk[pos] = *item * 32767.0;
|
||||
}
|
||||
|
||||
for i in (in_frame.len() / channels)..(size / channels) {
|
||||
for c in 0..channels {
|
||||
let channel_denoiser = &mut state.denoisers[c];
|
||||
channel_denoiser.frame_chunk[i] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: The first chunks coming out of the denoisers contains some
|
||||
// fade-in artifacts. We might want to discard those.
|
||||
let mut vad: f32 = 0.0;
|
||||
for channel_denoiser in &mut state.denoisers {
|
||||
vad = f32::max(
|
||||
vad,
|
||||
channel_denoiser.denoiser.process_frame(
|
||||
&mut channel_denoiser.out_chunk[..],
|
||||
&channel_denoiser.frame_chunk[..],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
gst::trace!(CAT, imp: self, "Voice activity: {}", vad);
|
||||
if vad < settings.vad_threshold {
|
||||
out_frame.fill(0.0);
|
||||
} else {
|
||||
// Upon voice activity nnoiseless never really reports a 1.0
|
||||
// VAD, so we use a hardcoded value close to 1.0 here.
|
||||
if vad >= 0.98 {
|
||||
has_voice = true;
|
||||
}
|
||||
for (index, item) in out_frame.iter_mut().enumerate() {
|
||||
let channel_index = index % channels;
|
||||
let channel_denoiser = &state.denoisers[channel_index];
|
||||
let pos = index / channels;
|
||||
*item = channel_denoiser.out_chunk[pos] / 32767.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rms = output_plane.iter().copied().map(|x| x * x).sum::<f32>();
|
||||
let level = (20.0 * f32::log10(rms + f32::EPSILON)) as u8;
|
||||
|
||||
gst::trace!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"rms: {}, level: {}, has_voice : {} ", rms,
|
||||
level,
|
||||
has_voice
|
||||
);
|
||||
|
||||
(level, has_voice)
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for AudioRNNoise {
|
||||
const NAME: &'static str = "GstAudioRNNoise";
|
||||
type Type = super::AudioRNNoise;
|
||||
type ParentType = gst_audio::AudioFilter;
|
||||
}
|
||||
|
||||
impl ObjectImpl for AudioRNNoise {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![glib::ParamSpecFloat::builder("voice-activity-threshold")
|
||||
.nick("Voice activity threshold")
|
||||
.blurb("Threshold of the voice activity detector below which to mute the output")
|
||||
.minimum(0.0)
|
||||
.maximum(1.0)
|
||||
.default_value(DEFAULT_VOICE_ACTIVITY_THRESHOLD)
|
||||
.mutable_playing()
|
||||
.build()]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"voice-activity-threshold" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.vad_threshold = value.get().expect("type checked upstream");
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"voice-activity-threshold" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.vad_threshold.to_value()
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for AudioRNNoise {}
|
||||
|
||||
impl ElementImpl for AudioRNNoise {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Audio denoise",
|
||||
"Filter/Effect/Audio",
|
||||
"Removes noise from an audio stream",
|
||||
"Philippe Normand <philn@igalia.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseTransformImpl for AudioRNNoise {
|
||||
const MODE: gst_base::subclass::BaseTransformMode =
|
||||
gst_base::subclass::BaseTransformMode::NeverInPlace;
|
||||
const PASSTHROUGH_ON_SAME_CAPS: bool = false;
|
||||
const TRANSFORM_IP_ON_PASSTHROUGH: bool = false;
|
||||
|
||||
fn generate_output(&self) -> Result<GenerateOutputSuccess, gst::FlowError> {
|
||||
// Check if there are enough data in the queued buffer and adapter,
|
||||
// if it is not the case, just notify the parent class to not generate
|
||||
// an output
|
||||
if let Some(buffer) = self.take_queued_buffer() {
|
||||
if buffer.flags().contains(gst::BufferFlags::DISCONT) {
|
||||
self.drain()?;
|
||||
}
|
||||
|
||||
let mut state_guard = self.state.borrow_mut();
|
||||
let state = state_guard.as_mut().ok_or_else(|| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::CoreError::Negotiation,
|
||||
["Can not generate an output without State"]
|
||||
);
|
||||
gst::FlowError::NotNegotiated
|
||||
})?;
|
||||
|
||||
state.adapter.push(buffer);
|
||||
if !state.needs_more_data() {
|
||||
return self.generate_output(state);
|
||||
}
|
||||
}
|
||||
Ok(GenerateOutputSuccess::NoOutput)
|
||||
}
|
||||
|
||||
fn sink_event(&self, event: gst::Event) -> bool {
|
||||
use gst::EventView;
|
||||
|
||||
if let EventView::Eos(_) = event.view() {
|
||||
gst::debug!(CAT, imp: self, "Handling EOS");
|
||||
if self.drain().is_err() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.parent_sink_event(event)
|
||||
}
|
||||
|
||||
fn query(&self, direction: gst::PadDirection, query: &mut gst::QueryRef) -> bool {
|
||||
if direction == gst::PadDirection::Src {
|
||||
if let gst::QueryViewMut::Latency(q) = query.view_mut() {
|
||||
let mut upstream_query = gst::query::Latency::new();
|
||||
if self.obj().sink_pad().peer_query(&mut upstream_query) {
|
||||
let (live, mut min, mut max) = upstream_query.result();
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Peer latency: live {} min {} max {}",
|
||||
live,
|
||||
min,
|
||||
max.display(),
|
||||
);
|
||||
|
||||
min += ((FRAME_SIZE / 48000) as u64).seconds();
|
||||
max = max.opt_add(((FRAME_SIZE / 48000) as u64).seconds());
|
||||
q.set(live, min, max);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
BaseTransformImplExt::parent_query(self, direction, query)
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
// Drop state
|
||||
let _ = self.state.borrow_mut().take();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFilterImpl for AudioRNNoise {
|
||||
fn allowed_caps() -> &'static gst::Caps {
|
||||
static CAPS: Lazy<gst::Caps> = Lazy::new(|| {
|
||||
gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(48000)
|
||||
.build()
|
||||
});
|
||||
|
||||
&CAPS
|
||||
}
|
||||
|
||||
fn setup(&self, info: &gst_audio::AudioInfo) -> Result<(), gst::LoggableError> {
|
||||
// Flush previous state
|
||||
if self.state.borrow_mut().is_some() {
|
||||
self.drain().map_err(|e| {
|
||||
gst::loggable_error!(CAT, "Error flushing previous state data {:?}", e)
|
||||
})?;
|
||||
}
|
||||
|
||||
gst::debug!(CAT, imp: self, "Set caps to {:?}", info);
|
||||
|
||||
let mut denoisers = vec![];
|
||||
for _i in 0..info.channels() {
|
||||
denoisers.push(ChannelDenoiser {
|
||||
denoiser: DenoiseState::new(),
|
||||
frame_chunk: Box::new([0.0; FRAME_SIZE]),
|
||||
out_chunk: Box::new([0.0; FRAME_SIZE]),
|
||||
})
|
||||
}
|
||||
|
||||
let mut state_lock = self.state.borrow_mut();
|
||||
*state_lock = Some(State {
|
||||
in_info: info.clone(),
|
||||
denoisers,
|
||||
adapter: gst_base::UniqueAdapter::new(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
26
audio/audiofx/src/audiornnoise/mod.rs
Normal file
26
audio/audiofx/src/audiornnoise/mod.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright (C) 2020 Philippe Normand <philn@igalia.com>
|
||||
// Copyright (C) 2020 Natanael Mojica <neithanmo@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct AudioRNNoise(ObjectSubclass<imp::AudioRNNoise>) @extends gst_base::BaseTransform, gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"audiornnoise",
|
||||
gst::Rank::NONE,
|
||||
AudioRNNoise::static_type(),
|
||||
)
|
||||
}
|
806
audio/audiofx/src/ebur128level/imp.rs
Normal file
806
audio/audiofx/src/ebur128level/imp.rs
Normal file
|
@ -0,0 +1,806 @@
|
|||
// Copyright (C) 2021 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_audio::subclass::prelude::*;
|
||||
use gst_base::prelude::*;
|
||||
|
||||
use std::sync::atomic;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use atomic_refcell::AtomicRefCell;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"ebur128level",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("EBU R128 Level"),
|
||||
)
|
||||
});
|
||||
|
||||
#[glib::flags(name = "GstEbuR128LevelMode")]
|
||||
pub(crate) enum Mode {
|
||||
#[flags_value(name = "Calculate momentary loudness (400ms)", nick = "momentary")]
|
||||
MOMENTARY = 0b00000001,
|
||||
#[flags_value(name = "Calculate short-term loudness (3s)", nick = "short-term")]
|
||||
SHORT_TERM = 0b00000010,
|
||||
#[flags_value(
|
||||
name = "Calculate relative threshold and global loudness",
|
||||
nick = "global"
|
||||
)]
|
||||
GLOBAL = 0b00000100,
|
||||
#[flags_value(name = "Calculate loudness range", nick = "loudness-range")]
|
||||
LOUDNESS_RANGE = 0b00001000,
|
||||
#[flags_value(name = "Calculate sample peak", nick = "sample-peak")]
|
||||
SAMPLE_PEAK = 0b00010000,
|
||||
#[flags_value(name = "Calculate true peak", nick = "true-peak")]
|
||||
TRUE_PEAK = 0b00100000,
|
||||
}
|
||||
|
||||
impl From<Mode> for ebur128::Mode {
|
||||
fn from(mode: Mode) -> Self {
|
||||
// Should use histogram mode as otherwise the history will grow forever
|
||||
let mut ebur128_mode = ebur128::Mode::HISTOGRAM;
|
||||
if mode.contains(Mode::MOMENTARY) {
|
||||
ebur128_mode.set(ebur128::Mode::M, true);
|
||||
}
|
||||
if mode.contains(Mode::SHORT_TERM) {
|
||||
ebur128_mode.set(ebur128::Mode::S, true);
|
||||
}
|
||||
if mode.contains(Mode::GLOBAL) {
|
||||
ebur128_mode.set(ebur128::Mode::I, true);
|
||||
}
|
||||
if mode.contains(Mode::LOUDNESS_RANGE) {
|
||||
ebur128_mode.set(ebur128::Mode::LRA, true);
|
||||
}
|
||||
if mode.contains(Mode::SAMPLE_PEAK) {
|
||||
ebur128_mode.set(ebur128::Mode::SAMPLE_PEAK, true);
|
||||
}
|
||||
if mode.contains(Mode::TRUE_PEAK) {
|
||||
ebur128_mode.set(ebur128::Mode::TRUE_PEAK, true);
|
||||
}
|
||||
|
||||
ebur128_mode
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_MODE: Mode = Mode::all();
|
||||
const DEFAULT_POST_MESSAGES: bool = true;
|
||||
const DEFAULT_INTERVAL: gst::ClockTime = gst::ClockTime::SECOND;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Settings {
|
||||
mode: Mode,
|
||||
post_messages: bool,
|
||||
interval: gst::ClockTime,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
mode: DEFAULT_MODE,
|
||||
post_messages: DEFAULT_POST_MESSAGES,
|
||||
interval: DEFAULT_INTERVAL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
info: gst_audio::AudioInfo,
|
||||
ebur128: ebur128::EbuR128,
|
||||
num_frames: u64,
|
||||
interval_frames: gst::ClockTime,
|
||||
interval_frames_remaining: gst::ClockTime,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EbuR128Level {
|
||||
settings: Mutex<Settings>,
|
||||
state: AtomicRefCell<Option<State>>,
|
||||
reset: atomic::AtomicBool,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for EbuR128Level {
|
||||
const NAME: &'static str = "GstEbuR128Level";
|
||||
type Type = super::EbuR128Level;
|
||||
type ParentType = gst_audio::AudioFilter;
|
||||
}
|
||||
|
||||
impl ObjectImpl for EbuR128Level {
|
||||
fn signals() -> &'static [glib::subclass::Signal] {
|
||||
static SIGNALS: Lazy<Vec<glib::subclass::Signal>> = Lazy::new(|| {
|
||||
vec![glib::subclass::Signal::builder("reset")
|
||||
.action()
|
||||
.class_handler(|_token, args| {
|
||||
let this = args[0].get::<super::EbuR128Level>().unwrap();
|
||||
let imp = this.imp();
|
||||
|
||||
gst::info!(CAT, obj: this, "Resetting measurements",);
|
||||
imp.reset.store(true, atomic::Ordering::SeqCst);
|
||||
|
||||
None
|
||||
})
|
||||
.build()]
|
||||
});
|
||||
|
||||
&SIGNALS
|
||||
}
|
||||
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecFlags::builder::<Mode>("mode")
|
||||
.nick("Mode")
|
||||
.blurb("Selection of metrics to calculate")
|
||||
.default_value(DEFAULT_MODE)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecBoolean::builder("post-messages")
|
||||
.nick("Post Messages")
|
||||
.blurb("Whether to post messages on the bus for each interval")
|
||||
.default_value(DEFAULT_POST_MESSAGES)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecUInt64::builder("interval")
|
||||
.nick("Interval")
|
||||
.blurb("Interval in nanoseconds for posting messages")
|
||||
.maximum(u64::MAX - 1)
|
||||
.default_value(DEFAULT_INTERVAL.nseconds())
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
match pspec.name() {
|
||||
"mode" => {
|
||||
let mode = value.get().expect("type checked upstream");
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Changing mode from {:?} to {:?}",
|
||||
settings.mode,
|
||||
mode
|
||||
);
|
||||
settings.mode = mode;
|
||||
}
|
||||
"post-messages" => {
|
||||
let post_messages = value.get().expect("type checked upstream");
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Changing post-messages from {} to {}",
|
||||
settings.post_messages,
|
||||
post_messages
|
||||
);
|
||||
settings.post_messages = post_messages;
|
||||
}
|
||||
"interval" => {
|
||||
let interval = value.get::<u64>().unwrap().nseconds();
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Changing interval from {} to {}",
|
||||
settings.interval,
|
||||
interval,
|
||||
);
|
||||
settings.interval = interval;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
match pspec.name() {
|
||||
"mode" => settings.mode.to_value(),
|
||||
"post-messages" => settings.post_messages.to_value(),
|
||||
"interval" => settings.interval.to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for EbuR128Level {}
|
||||
|
||||
impl ElementImpl for EbuR128Level {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"EBU R128 Loudness Level Measurement",
|
||||
"Filter/Analyzer/Audio",
|
||||
"Measures different loudness metrics according to EBU R128",
|
||||
"Sebastian Dröge <sebastian@centricular.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let caps = gst_audio::AudioCapsBuilder::new()
|
||||
.format_list([
|
||||
gst_audio::AUDIO_FORMAT_S16,
|
||||
gst_audio::AUDIO_FORMAT_S32,
|
||||
gst_audio::AUDIO_FORMAT_F32,
|
||||
gst_audio::AUDIO_FORMAT_F64,
|
||||
])
|
||||
// Limit from ebur128
|
||||
.rate_range(1..2_822_400)
|
||||
// Limit from ebur128
|
||||
.channels_range(1..64)
|
||||
.layout_list([
|
||||
gst_audio::AudioLayout::Interleaved,
|
||||
gst_audio::AudioLayout::NonInterleaved,
|
||||
])
|
||||
.build();
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template, sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseTransformImpl for EbuR128Level {
|
||||
const MODE: gst_base::subclass::BaseTransformMode =
|
||||
gst_base::subclass::BaseTransformMode::AlwaysInPlace;
|
||||
const PASSTHROUGH_ON_SAME_CAPS: bool = true;
|
||||
const TRANSFORM_IP_ON_PASSTHROUGH: bool = true;
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
// Drop state
|
||||
let _ = self.state.borrow_mut().take();
|
||||
|
||||
gst::info!(CAT, imp: self, "Stopped");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transform_ip_passthrough(
|
||||
&self,
|
||||
buf: &gst::Buffer,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let settings = *self.settings.lock().unwrap();
|
||||
|
||||
let mut state_guard = self.state.borrow_mut();
|
||||
let mut state = state_guard.as_mut().ok_or_else(|| {
|
||||
gst::element_imp_error!(self, gst::CoreError::Negotiation, ["Have no state yet"]);
|
||||
gst::FlowError::NotNegotiated
|
||||
})?;
|
||||
|
||||
let mut timestamp = buf.pts();
|
||||
let segment = self.obj().segment().downcast::<gst::ClockTime>().ok();
|
||||
|
||||
let buf = gst_audio::AudioBufferRef::from_buffer_ref_readable(buf, &state.info).map_err(
|
||||
|_| {
|
||||
gst::element_imp_error!(self, gst::ResourceError::Read, ["Failed to map buffer"]);
|
||||
gst::FlowError::Error
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut frames = Frames::from_audio_buffer(self, &buf)?;
|
||||
while frames.num_frames() > 0 {
|
||||
if self
|
||||
.reset
|
||||
.compare_exchange(
|
||||
true,
|
||||
false,
|
||||
atomic::Ordering::SeqCst,
|
||||
atomic::Ordering::SeqCst,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
state.ebur128.reset();
|
||||
state.interval_frames_remaining = state.interval_frames;
|
||||
state.num_frames = 0;
|
||||
}
|
||||
|
||||
let to_process = u64::min(
|
||||
state.interval_frames_remaining.nseconds(),
|
||||
frames.num_frames() as u64,
|
||||
);
|
||||
|
||||
frames
|
||||
.process(to_process, &mut state.ebur128)
|
||||
.map_err(|err| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::ResourceError::Read,
|
||||
["Failed to process buffer: {}", err]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
state.interval_frames_remaining -= to_process.nseconds();
|
||||
state.num_frames += to_process;
|
||||
|
||||
// The timestamp we report in messages is always the timestamp until which measurements
|
||||
// are included, not the starting timestamp.
|
||||
timestamp = timestamp.opt_add(
|
||||
to_process
|
||||
.mul_div_floor(*gst::ClockTime::SECOND, state.info.rate() as u64)
|
||||
.map(gst::ClockTime::from_nseconds)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// Post a message whenever an interval is full
|
||||
if state.interval_frames_remaining.is_zero() {
|
||||
state.interval_frames_remaining = state.interval_frames;
|
||||
|
||||
if settings.post_messages {
|
||||
let running_time = segment.as_ref().and_then(|s| s.to_running_time(timestamp));
|
||||
let stream_time = segment.as_ref().and_then(|s| s.to_stream_time(timestamp));
|
||||
|
||||
let mut s = gst::Structure::builder("ebur128-level")
|
||||
.field("timestamp", timestamp)
|
||||
.field("running-time", running_time)
|
||||
.field("stream-time", stream_time)
|
||||
.build();
|
||||
|
||||
if state.ebur128.mode().contains(ebur128::Mode::M) {
|
||||
match state.ebur128.loudness_momentary() {
|
||||
Ok(loudness) => s.set("momentary-loudness", loudness),
|
||||
Err(err) => gst::error!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Failed to get momentary loudness: {}",
|
||||
err
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if state.ebur128.mode().contains(ebur128::Mode::S) {
|
||||
match state.ebur128.loudness_shortterm() {
|
||||
Ok(loudness) => s.set("shortterm-loudness", loudness),
|
||||
Err(err) => gst::error!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Failed to get shortterm loudness: {}",
|
||||
err
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if state.ebur128.mode().contains(ebur128::Mode::I) {
|
||||
match state.ebur128.loudness_global() {
|
||||
Ok(loudness) => s.set("global-loudness", loudness),
|
||||
Err(err) => gst::error!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Failed to get global loudness: {}",
|
||||
err
|
||||
),
|
||||
}
|
||||
|
||||
match state.ebur128.relative_threshold() {
|
||||
Ok(threshold) => s.set("relative-threshold", threshold),
|
||||
Err(err) => gst::error!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Failed to get relative threshold: {}",
|
||||
err
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if state.ebur128.mode().contains(ebur128::Mode::LRA) {
|
||||
match state.ebur128.loudness_range() {
|
||||
Ok(range) => s.set("loudness-range", range),
|
||||
Err(err) => {
|
||||
gst::error!(CAT, imp: self, "Failed to get loudness range: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.ebur128.mode().contains(ebur128::Mode::SAMPLE_PEAK) {
|
||||
let peaks = (0..state.info.channels())
|
||||
.map(|c| state.ebur128.sample_peak(c).map(|p| p.to_send_value()))
|
||||
.collect::<Result<gst::Array, _>>();
|
||||
|
||||
match peaks {
|
||||
Ok(peaks) => s.set("sample-peak", peaks),
|
||||
Err(err) => {
|
||||
gst::error!(CAT, imp: self, "Failed to get sample peaks: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.ebur128.mode().contains(ebur128::Mode::TRUE_PEAK) {
|
||||
let peaks = (0..state.info.channels())
|
||||
.map(|c| state.ebur128.true_peak(c).map(|p| p.to_send_value()))
|
||||
.collect::<Result<gst::Array, _>>();
|
||||
|
||||
match peaks {
|
||||
Ok(peaks) => s.set("true-peak", peaks),
|
||||
Err(err) => {
|
||||
gst::error!(CAT, imp: self, "Failed to get true peaks: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gst::debug!(CAT, imp: self, "Posting message {}", s);
|
||||
|
||||
let msg = gst::message::Element::builder(s).src(&*self.obj()).build();
|
||||
|
||||
// Release lock while posting the message to avoid deadlocks
|
||||
drop(state_guard);
|
||||
|
||||
let _ = self.obj().post_message(msg);
|
||||
|
||||
state_guard = self.state.borrow_mut();
|
||||
state = state_guard.as_mut().ok_or_else(|| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::CoreError::Negotiation,
|
||||
["Have no state yet"]
|
||||
);
|
||||
gst::FlowError::NotNegotiated
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFilterImpl for EbuR128Level {
|
||||
fn allowed_caps() -> &'static gst::Caps {
|
||||
static CAPS: Lazy<gst::Caps> = Lazy::new(|| {
|
||||
gst_audio::AudioCapsBuilder::new()
|
||||
.format_list([
|
||||
gst_audio::AUDIO_FORMAT_S16,
|
||||
gst_audio::AUDIO_FORMAT_S32,
|
||||
gst_audio::AUDIO_FORMAT_F32,
|
||||
gst_audio::AUDIO_FORMAT_F64,
|
||||
])
|
||||
// Limit from ebur128
|
||||
.rate_range(1..2_822_400)
|
||||
// Limit from ebur128
|
||||
.channels_range(1..64)
|
||||
.layout_list([
|
||||
gst_audio::AudioLayout::Interleaved,
|
||||
gst_audio::AudioLayout::NonInterleaved,
|
||||
])
|
||||
.build()
|
||||
});
|
||||
|
||||
&CAPS
|
||||
}
|
||||
|
||||
fn setup(&self, info: &gst_audio::AudioInfo) -> Result<(), gst::LoggableError> {
|
||||
gst::debug!(CAT, imp: self, "Configured for caps {:?}", info);
|
||||
|
||||
let settings = *self.settings.lock().unwrap();
|
||||
|
||||
let mut ebur128 = ebur128::EbuR128::new(info.channels(), info.rate(), settings.mode.into())
|
||||
.map_err(|err| gst::loggable_error!(CAT, "Failed to create EBU R128: {}", err))?;
|
||||
|
||||
// Map channel positions if we can to give correct weighting
|
||||
if let Some(positions) = info.positions() {
|
||||
let channel_map = positions
|
||||
.iter()
|
||||
.map(|p| {
|
||||
match p {
|
||||
gst_audio::AudioChannelPosition::Mono => ebur128::Channel::DualMono,
|
||||
gst_audio::AudioChannelPosition::FrontLeft => ebur128::Channel::Left,
|
||||
gst_audio::AudioChannelPosition::FrontRight => ebur128::Channel::Right,
|
||||
gst_audio::AudioChannelPosition::FrontCenter => ebur128::Channel::Center,
|
||||
gst_audio::AudioChannelPosition::Lfe1
|
||||
| gst_audio::AudioChannelPosition::Lfe2 => ebur128::Channel::Unused,
|
||||
gst_audio::AudioChannelPosition::RearLeft => ebur128::Channel::Mp135,
|
||||
gst_audio::AudioChannelPosition::RearRight => ebur128::Channel::Mm135,
|
||||
gst_audio::AudioChannelPosition::FrontLeftOfCenter => {
|
||||
ebur128::Channel::MpSC
|
||||
}
|
||||
gst_audio::AudioChannelPosition::FrontRightOfCenter => {
|
||||
ebur128::Channel::MmSC
|
||||
}
|
||||
gst_audio::AudioChannelPosition::RearCenter => ebur128::Channel::Mp180,
|
||||
gst_audio::AudioChannelPosition::SideLeft => ebur128::Channel::Mp090,
|
||||
gst_audio::AudioChannelPosition::SideRight => ebur128::Channel::Mm090,
|
||||
gst_audio::AudioChannelPosition::TopFrontLeft => ebur128::Channel::Up030,
|
||||
gst_audio::AudioChannelPosition::TopFrontRight => ebur128::Channel::Um030,
|
||||
gst_audio::AudioChannelPosition::TopFrontCenter => ebur128::Channel::Up000,
|
||||
gst_audio::AudioChannelPosition::TopCenter => ebur128::Channel::Tp000,
|
||||
gst_audio::AudioChannelPosition::TopRearLeft => ebur128::Channel::Up135,
|
||||
gst_audio::AudioChannelPosition::TopRearRight => ebur128::Channel::Um135,
|
||||
gst_audio::AudioChannelPosition::TopSideLeft => ebur128::Channel::Up090,
|
||||
gst_audio::AudioChannelPosition::TopSideRight => ebur128::Channel::Um090,
|
||||
gst_audio::AudioChannelPosition::TopRearCenter => ebur128::Channel::Up180,
|
||||
gst_audio::AudioChannelPosition::BottomFrontCenter => {
|
||||
ebur128::Channel::Bp000
|
||||
}
|
||||
gst_audio::AudioChannelPosition::BottomFrontLeft => ebur128::Channel::Bp045,
|
||||
gst_audio::AudioChannelPosition::BottomFrontRight => {
|
||||
ebur128::Channel::Bm045
|
||||
}
|
||||
gst_audio::AudioChannelPosition::WideLeft => {
|
||||
ebur128::Channel::Mp135 // Mp110?
|
||||
}
|
||||
gst_audio::AudioChannelPosition::WideRight => {
|
||||
ebur128::Channel::Mm135 // Mm110?
|
||||
}
|
||||
gst_audio::AudioChannelPosition::SurroundLeft => {
|
||||
ebur128::Channel::Mp135 // Mp110?
|
||||
}
|
||||
gst_audio::AudioChannelPosition::SurroundRight => {
|
||||
ebur128::Channel::Mm135 // Mm110?
|
||||
}
|
||||
gst_audio::AudioChannelPosition::Invalid
|
||||
| gst_audio::AudioChannelPosition::None => ebur128::Channel::Unused,
|
||||
val => {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Unknown channel position {:?}, ignoring channel",
|
||||
val
|
||||
);
|
||||
ebur128::Channel::Unused
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
ebur128
|
||||
.set_channel_map(&channel_map)
|
||||
.map_err(|err| gst::loggable_error!(CAT, "Failed to set channel map: {}", err))?;
|
||||
} else {
|
||||
// Weight all channels equally if we have no channel map
|
||||
let channel_map = std::iter::repeat(ebur128::Channel::Center)
|
||||
.take(info.channels() as usize)
|
||||
.collect::<Vec<_>>();
|
||||
ebur128
|
||||
.set_channel_map(&channel_map)
|
||||
.map_err(|err| gst::loggable_error!(CAT, "Failed to set channel map: {}", err))?;
|
||||
}
|
||||
|
||||
let interval_frames = settings
|
||||
.interval
|
||||
.mul_div_floor(info.rate() as u64, *gst::ClockTime::SECOND)
|
||||
.unwrap();
|
||||
|
||||
*self.state.borrow_mut() = Some(State {
|
||||
info: info.clone(),
|
||||
ebur128,
|
||||
num_frames: 0,
|
||||
interval_frames,
|
||||
interval_frames_remaining: interval_frames,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper struct to handle the different sample formats and layouts generically.
|
||||
enum Frames<'a> {
|
||||
S16(&'a [i16], usize),
|
||||
S32(&'a [i32], usize),
|
||||
F32(&'a [f32], usize),
|
||||
F64(&'a [f64], usize),
|
||||
S16P(SmallVec<[&'a [i16]; 64]>),
|
||||
S32P(SmallVec<[&'a [i32]; 64]>),
|
||||
F32P(SmallVec<[&'a [f32]; 64]>),
|
||||
F64P(SmallVec<[&'a [f64]; 64]>),
|
||||
}
|
||||
|
||||
impl<'a> Frames<'a> {
|
||||
/// Create a new frames wrapper that allows chunked processing.
|
||||
fn from_audio_buffer(
|
||||
imp: &EbuR128Level,
|
||||
buf: &'a gst_audio::AudioBufferRef<&'a gst::BufferRef>,
|
||||
) -> Result<Self, gst::FlowError> {
|
||||
match (buf.format(), buf.layout()) {
|
||||
(gst_audio::AUDIO_FORMAT_S16, gst_audio::AudioLayout::Interleaved) => Ok(Frames::S16(
|
||||
interleaved_channel_data_into_slice(imp, buf)?,
|
||||
buf.channels() as usize,
|
||||
)),
|
||||
(gst_audio::AUDIO_FORMAT_S32, gst_audio::AudioLayout::Interleaved) => Ok(Frames::S32(
|
||||
interleaved_channel_data_into_slice(imp, buf)?,
|
||||
buf.channels() as usize,
|
||||
)),
|
||||
(gst_audio::AUDIO_FORMAT_F32, gst_audio::AudioLayout::Interleaved) => Ok(Frames::F32(
|
||||
interleaved_channel_data_into_slice(imp, buf)?,
|
||||
buf.channels() as usize,
|
||||
)),
|
||||
(gst_audio::AUDIO_FORMAT_F64, gst_audio::AudioLayout::Interleaved) => Ok(Frames::F64(
|
||||
interleaved_channel_data_into_slice(imp, buf)?,
|
||||
buf.channels() as usize,
|
||||
)),
|
||||
(gst_audio::AUDIO_FORMAT_S16, gst_audio::AudioLayout::NonInterleaved) => Ok(
|
||||
Frames::S16P(non_interleaved_channel_data_into_slices(imp, buf)?),
|
||||
),
|
||||
(gst_audio::AUDIO_FORMAT_S32, gst_audio::AudioLayout::NonInterleaved) => Ok(
|
||||
Frames::S32P(non_interleaved_channel_data_into_slices(imp, buf)?),
|
||||
),
|
||||
(gst_audio::AUDIO_FORMAT_F32, gst_audio::AudioLayout::NonInterleaved) => Ok(
|
||||
Frames::F32P(non_interleaved_channel_data_into_slices(imp, buf)?),
|
||||
),
|
||||
(gst_audio::AUDIO_FORMAT_F64, gst_audio::AudioLayout::NonInterleaved) => Ok(
|
||||
Frames::F64P(non_interleaved_channel_data_into_slices(imp, buf)?),
|
||||
),
|
||||
_ => Err(gst::FlowError::NotNegotiated),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of remaining frames.
|
||||
fn num_frames(&self) -> usize {
|
||||
match self {
|
||||
Frames::S16(frames, channels) => frames.len() / channels,
|
||||
Frames::S32(frames, channels) => frames.len() / channels,
|
||||
Frames::F32(frames, channels) => frames.len() / channels,
|
||||
Frames::F64(frames, channels) => frames.len() / channels,
|
||||
Frames::S16P(frames) => frames[0].len(),
|
||||
Frames::S32P(frames) => frames[0].len(),
|
||||
Frames::F32P(frames) => frames[0].len(),
|
||||
Frames::F64P(frames) => frames[0].len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process `num_frames` with `ebur128` and advance to the next frames.
|
||||
fn process(
|
||||
&mut self,
|
||||
num_frames: u64,
|
||||
ebur128: &mut ebur128::EbuR128,
|
||||
) -> Result<(), ebur128::Error> {
|
||||
match self {
|
||||
Frames::S16(frames, channels) => {
|
||||
let (first, second) = frames.split_at(num_frames as usize * *channels);
|
||||
ebur128.add_frames_i16(first)?;
|
||||
*frames = second;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Frames::S32(frames, channels) => {
|
||||
let (first, second) = frames.split_at(num_frames as usize * *channels);
|
||||
ebur128.add_frames_i32(first)?;
|
||||
*frames = second;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Frames::F32(frames, channels) => {
|
||||
let (first, second) = frames.split_at(num_frames as usize * *channels);
|
||||
ebur128.add_frames_f32(first)?;
|
||||
*frames = second;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Frames::F64(frames, channels) => {
|
||||
let (first, second) = frames.split_at(num_frames as usize * *channels);
|
||||
ebur128.add_frames_f64(first)?;
|
||||
*frames = second;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Frames::S16P(channels) => {
|
||||
let (first, second) = split_vec(channels, num_frames as usize);
|
||||
ebur128.add_frames_planar_i16(&first)?;
|
||||
*channels = second;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Frames::S32P(channels) => {
|
||||
let (first, second) = split_vec(channels, num_frames as usize);
|
||||
ebur128.add_frames_planar_i32(&first)?;
|
||||
*channels = second;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Frames::F32P(channels) => {
|
||||
let (first, second) = split_vec(channels, num_frames as usize);
|
||||
ebur128.add_frames_planar_f32(&first)?;
|
||||
*channels = second;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Frames::F64P(channels) => {
|
||||
let (first, second) = split_vec(channels, num_frames as usize);
|
||||
ebur128.add_frames_planar_f64(&first)?;
|
||||
*channels = second;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an interleaved audio buffer into a typed slice.
|
||||
fn interleaved_channel_data_into_slice<'a, T: FromByteSlice>(
|
||||
imp: &EbuR128Level,
|
||||
buf: &'a gst_audio::AudioBufferRef<&gst::BufferRef>,
|
||||
) -> Result<&'a [T], gst::FlowError> {
|
||||
buf.plane_data(0)
|
||||
.map_err(|err| {
|
||||
gst::error!(CAT, imp: imp, "Failed to get audio data: {}", err);
|
||||
gst::FlowError::Error
|
||||
})?
|
||||
.as_slice_of::<T>()
|
||||
.map_err(|err| {
|
||||
gst::error!(CAT, imp: imp, "Failed to handle audio data: {}", err);
|
||||
gst::FlowError::Error
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts a non-interleaved audio buffer into a vector of typed slices.
|
||||
fn non_interleaved_channel_data_into_slices<'a, T: FromByteSlice>(
|
||||
imp: &EbuR128Level,
|
||||
buf: &'a gst_audio::AudioBufferRef<&gst::BufferRef>,
|
||||
) -> Result<SmallVec<[&'a [T]; 64]>, gst::FlowError> {
|
||||
(0..buf.channels())
|
||||
.map(|c| {
|
||||
buf.plane_data(c)
|
||||
.map_err(|err| {
|
||||
gst::error!(CAT, imp: imp, "Failed to get audio data: {}", err);
|
||||
gst::FlowError::Error
|
||||
})?
|
||||
.as_slice_of::<T>()
|
||||
.map_err(|err| {
|
||||
gst::error!(CAT, imp: imp, "Failed to handle audio data: {}", err);
|
||||
gst::FlowError::Error
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, _>>()
|
||||
}
|
||||
|
||||
/// Split a vector of slices into a tuple of slices with each slice split at `split_at`.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn split_vec<'a, T: Copy>(
|
||||
vec: &SmallVec<[&'a [T]; 64]>,
|
||||
split_at: usize,
|
||||
) -> (SmallVec<[&'a [T]; 64]>, SmallVec<[&'a [T]; 64]>) {
|
||||
let VecPair(first, second) = vec
|
||||
.iter()
|
||||
.map(|vec| vec.split_at(split_at))
|
||||
.collect::<VecPair<_>>();
|
||||
(first, second)
|
||||
}
|
||||
|
||||
/// Helper struct to collect from an iterator on pairs into two vectors.
|
||||
struct VecPair<T>(SmallVec<[T; 64]>, SmallVec<[T; 64]>);
|
||||
|
||||
impl<T> std::iter::FromIterator<(T, T)> for VecPair<T> {
|
||||
fn from_iter<I: IntoIterator<Item = (T, T)>>(iter: I) -> Self {
|
||||
let mut first_vec = SmallVec::new();
|
||||
let mut second_vec = SmallVec::new();
|
||||
for (first, second) in iter {
|
||||
first_vec.push(first);
|
||||
second_vec.push(second);
|
||||
}
|
||||
|
||||
VecPair(first_vec, second_vec)
|
||||
}
|
||||
}
|
28
audio/audiofx/src/ebur128level/mod.rs
Normal file
28
audio/audiofx/src/ebur128level/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright (C) 2021 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct EbuR128Level(ObjectSubclass<imp::EbuR128Level>) @extends gst_base::BaseTransform, gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
#[cfg(feature = "doc")]
|
||||
imp::Mode::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"ebur128level",
|
||||
gst::Rank::NONE,
|
||||
EbuR128Level::static_type(),
|
||||
)
|
||||
}
|
812
audio/audiofx/src/hrtfrender/imp.rs
Normal file
812
audio/audiofx/src/hrtfrender/imp.rs
Normal file
|
@ -0,0 +1,812 @@
|
|||
// Copyright (C) 2021 Tomasz Andrzejak <andreiltd@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
|
||||
use gst_base::prelude::*;
|
||||
use gst_base::subclass::prelude::*;
|
||||
|
||||
use hrtf::{HrirSphere, HrtfContext, HrtfProcessor, Vec3};
|
||||
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
|
||||
use byte_slice_cast::*;
|
||||
use rayon::prelude::*;
|
||||
use rayon::{ThreadPool, ThreadPoolBuilder};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"hrtfrender",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("Head-Related Transfer Function Renderer"),
|
||||
)
|
||||
});
|
||||
|
||||
static THREAD_POOL: Lazy<Mutex<Weak<ThreadPool>>> = Lazy::new(|| Mutex::new(Weak::new()));
|
||||
|
||||
const DEFAULT_INTERPOLATION_STEPS: u64 = 8;
|
||||
const DEFAULT_BLOCK_LENGTH: u64 = 512;
|
||||
const DEFAULT_DISTANCE_GAIN: f32 = 1.0;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SpatialObject {
|
||||
/// Position values use a left-handed Cartesian coordinate system
|
||||
position: Vec3,
|
||||
/// Object attenuation by distance
|
||||
distance_gain: f32,
|
||||
}
|
||||
|
||||
impl From<gst::Structure> for SpatialObject {
|
||||
fn from(s: gst::Structure) -> Self {
|
||||
SpatialObject {
|
||||
position: Vec3 {
|
||||
x: s.get("x").expect("type checked upstream"),
|
||||
y: s.get("y").expect("type checked upstream"),
|
||||
z: s.get("z").expect("type checked upstream"),
|
||||
},
|
||||
distance_gain: s.get("distance-gain").expect("type checked upstream"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpatialObject> for gst::Structure {
|
||||
fn from(obj: SpatialObject) -> Self {
|
||||
gst::Structure::builder("application/spatial-object")
|
||||
.field("x", obj.position.x)
|
||||
.field("y", obj.position.y)
|
||||
.field("z", obj.position.z)
|
||||
.field("distance-gain", obj.distance_gain)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<gst_audio::AudioChannelPosition> for SpatialObject {
|
||||
type Error = gst::FlowError;
|
||||
|
||||
fn try_from(pos: gst_audio::AudioChannelPosition) -> Result<Self, gst::FlowError> {
|
||||
use gst_audio::AudioChannelPosition::*;
|
||||
|
||||
let position = match pos {
|
||||
FrontLeft => Vec3::new(-1.45, 0.0, 2.5),
|
||||
FrontRight => Vec3::new(1.45, 0.0, 2.5),
|
||||
FrontCenter | Mono => Vec3::new(0.0, 0.0, 2.5),
|
||||
Lfe1 | Lfe2 => Vec3::new(0.0, 0.0, 0.0),
|
||||
RearLeft => Vec3::new(-1.45, 0.0, -2.5),
|
||||
RearRight => Vec3::new(1.45, 0.0, -2.5),
|
||||
FrontLeftOfCenter => Vec3::new(-0.72, 0.0, 2.5),
|
||||
FrontRightOfCenter => Vec3::new(0.72, 0.0, 2.5),
|
||||
RearCenter => Vec3::new(0.0, 0.0, -2.5),
|
||||
SideLeft => Vec3::new(-2.5, 0.0, -0.44),
|
||||
SideRight => Vec3::new(2.5, 0.0, -0.44),
|
||||
TopFrontLeft => Vec3::new(-0.72, 2.5, 1.25),
|
||||
TopFrontRight => Vec3::new(0.72, 2.5, 1.25),
|
||||
TopFrontCenter => Vec3::new(0.0, 2.5, 1.25),
|
||||
TopCenter => Vec3::new(0.0, 2.5, 0.0),
|
||||
TopRearLeft => Vec3::new(-0.72, 2.5, -1.25),
|
||||
TopRearRight => Vec3::new(0.72, 2.5, -1.25),
|
||||
TopSideLeft => Vec3::new(-1.25, 2.5, -0.22),
|
||||
TopSideRight => Vec3::new(1.25, 2.5, -0.22),
|
||||
TopRearCenter => Vec3::new(0.0, 2.5, -1.25),
|
||||
BottomFrontCenter => Vec3::new(0.0, -2.5, 1.25),
|
||||
BottomFrontLeft => Vec3::new(-0.72, -2.5, 1.25),
|
||||
BottomFrontRight => Vec3::new(0.72, -2.5, 1.25),
|
||||
WideLeft => Vec3::new(-2.5, 0.0, 1.45),
|
||||
WideRight => Vec3::new(2.5, 0.0, 1.45),
|
||||
SurroundLeft => Vec3::new(-2.5, 0.0, -1.45),
|
||||
SurroundRight => Vec3::new(2.5, 0.0, -1.45),
|
||||
_ => return Err(gst::FlowError::NotSupported),
|
||||
};
|
||||
|
||||
Ok(SpatialObject {
|
||||
position,
|
||||
distance_gain: DEFAULT_DISTANCE_GAIN,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Settings {
|
||||
interpolation_steps: u64,
|
||||
block_length: u64,
|
||||
spatial_objects: Option<Vec<SpatialObject>>,
|
||||
hrir_raw_bytes: Option<glib::Bytes>,
|
||||
hrir_file_location: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
interpolation_steps: DEFAULT_INTERPOLATION_STEPS,
|
||||
block_length: DEFAULT_BLOCK_LENGTH,
|
||||
spatial_objects: None,
|
||||
hrir_raw_bytes: None,
|
||||
hrir_file_location: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
fn position(&self, channel: usize) -> Result<Vec3, gst::FlowError> {
|
||||
Ok(self
|
||||
.spatial_objects
|
||||
.as_ref()
|
||||
.ok_or(gst::FlowError::NotNegotiated)?[channel]
|
||||
.position)
|
||||
}
|
||||
|
||||
fn distance_gain(&self, channel: usize) -> Result<f32, gst::FlowError> {
|
||||
Ok(self
|
||||
.spatial_objects
|
||||
.as_ref()
|
||||
.ok_or(gst::FlowError::NotNegotiated)?[channel]
|
||||
.distance_gain)
|
||||
}
|
||||
|
||||
fn sphere(&self, rate: u32) -> Result<hrtf::HrirSphere, hrtf::HrtfError> {
|
||||
if let Some(bytes) = &self.hrir_raw_bytes {
|
||||
return HrirSphere::new(bytes.as_byte_slice(), rate);
|
||||
}
|
||||
|
||||
if let Some(path) = &self.hrir_file_location {
|
||||
return HrirSphere::from_file(PathBuf::from(path), rate);
|
||||
}
|
||||
|
||||
Err(Error::new(ErrorKind::Other, "Impulse response not set").into())
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelProcessor {
|
||||
prev_left_samples: Vec<f32>,
|
||||
prev_right_samples: Vec<f32>,
|
||||
prev_sample_vector: Option<Vec3>,
|
||||
prev_distance_gain: Option<f32>,
|
||||
indata_scratch: Box<[f32]>,
|
||||
outdata_scratch: Box<[(f32, f32)]>,
|
||||
processor: HrtfProcessor,
|
||||
}
|
||||
|
||||
struct State {
|
||||
ininfo: gst_audio::AudioInfo,
|
||||
outinfo: gst_audio::AudioInfo,
|
||||
adapter: gst_base::UniqueAdapter,
|
||||
block_samples: usize,
|
||||
channel_processors: Vec<ChannelProcessor>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn input_block_size(&self) -> usize {
|
||||
self.block_samples * self.ininfo.bpf() as usize
|
||||
}
|
||||
|
||||
fn output_block_size(&self) -> usize {
|
||||
self.block_samples * self.outinfo.bpf() as usize
|
||||
}
|
||||
|
||||
fn reset_processors(&mut self) {
|
||||
for cp in self.channel_processors.iter_mut() {
|
||||
cp.prev_left_samples.fill(0.0);
|
||||
cp.prev_right_samples.fill(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HrtfRender {
|
||||
settings: Mutex<Settings>,
|
||||
state: Mutex<Option<State>>,
|
||||
thread_pool: Mutex<Option<Arc<ThreadPool>>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for HrtfRender {
|
||||
const NAME: &'static str = "GstHrtfRender";
|
||||
type Type = super::HrtfRender;
|
||||
type ParentType = gst_base::BaseTransform;
|
||||
}
|
||||
|
||||
impl HrtfRender {
|
||||
fn process(
|
||||
&self,
|
||||
outbuf: &mut gst::BufferRef,
|
||||
state: &mut State,
|
||||
settings: &Settings,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let mut outbuf =
|
||||
gst_audio::AudioBufferRef::from_buffer_ref_writable(outbuf, &state.outinfo).map_err(
|
||||
|err| {
|
||||
gst::error!(CAT, imp: self, "Failed to map buffer : {}", err);
|
||||
gst::FlowError::Error
|
||||
},
|
||||
)?;
|
||||
|
||||
let outdata = outbuf
|
||||
.plane_data_mut(0)
|
||||
.unwrap()
|
||||
.as_mut_slice_of::<f32>()
|
||||
.unwrap();
|
||||
|
||||
// prefill output with zeros so we can mix processed samples into it
|
||||
outdata.fill(0.0);
|
||||
|
||||
let inblksz = state.input_block_size();
|
||||
let channels = state.ininfo.channels();
|
||||
|
||||
let mut written_samples = 0;
|
||||
|
||||
let thread_pool_guard = self.thread_pool.lock().unwrap();
|
||||
let thread_pool = thread_pool_guard.as_ref().ok_or(gst::FlowError::Error)?;
|
||||
|
||||
while state.adapter.available() >= inblksz {
|
||||
let inbuf = state.adapter.take_buffer(inblksz).map_err(|_| {
|
||||
gst::error!(CAT, imp: self, "Failed to map buffer");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let inbuf = gst_audio::AudioBuffer::from_buffer_readable(inbuf, &state.ininfo)
|
||||
.map_err(|_| {
|
||||
gst::error!(CAT, imp: self, "Failed to map buffer");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let indata = inbuf.plane_data(0).unwrap().as_slice_of::<f32>().unwrap();
|
||||
|
||||
thread_pool.install(|| -> Result<(), gst::FlowError> {
|
||||
state
|
||||
.channel_processors
|
||||
.par_iter_mut()
|
||||
.enumerate()
|
||||
.try_for_each(|(i, cp)| -> Result<(), gst::FlowError> {
|
||||
let new_distance_gain = settings.distance_gain(i)?;
|
||||
let new_sample_vector = settings.position(i)?;
|
||||
|
||||
// Convert to Right Handed, this is what HRTF crate expects
|
||||
let new_sample_vector = Vec3 {
|
||||
z: new_sample_vector.z * -1.0,
|
||||
..new_sample_vector
|
||||
};
|
||||
|
||||
// Deinterleave single channel to scratch buffer
|
||||
for (x, y) in Iterator::zip(
|
||||
indata.iter().skip(i).step_by(channels as usize),
|
||||
cp.indata_scratch.iter_mut(),
|
||||
) {
|
||||
*y = *x;
|
||||
}
|
||||
|
||||
cp.processor.process_samples(HrtfContext {
|
||||
source: &cp.indata_scratch,
|
||||
output: &mut cp.outdata_scratch,
|
||||
new_sample_vector,
|
||||
new_distance_gain,
|
||||
prev_sample_vector: cp.prev_sample_vector.unwrap_or(new_sample_vector),
|
||||
prev_distance_gain: cp.prev_distance_gain.unwrap_or(new_distance_gain),
|
||||
prev_left_samples: &mut cp.prev_left_samples,
|
||||
prev_right_samples: &mut cp.prev_right_samples,
|
||||
});
|
||||
|
||||
cp.prev_sample_vector = Some(new_sample_vector);
|
||||
cp.prev_distance_gain = Some(new_distance_gain);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})?;
|
||||
|
||||
// unpack output scratch to output buffer
|
||||
state.channel_processors.iter_mut().for_each(|cp| {
|
||||
for (x, y) in Iterator::zip(
|
||||
cp.outdata_scratch.iter(),
|
||||
outdata[2 * written_samples..].chunks_exact_mut(2),
|
||||
) {
|
||||
y[0] += x.0;
|
||||
y[1] += x.1;
|
||||
}
|
||||
|
||||
// HRTF is mixing processed samples with samples in output buffer, we need to
|
||||
// reset scratch so it is not mixed with the next frame
|
||||
cp.outdata_scratch.fill((0.0, 0.0));
|
||||
});
|
||||
|
||||
written_samples += state.block_samples;
|
||||
}
|
||||
|
||||
// we only support stereo output, we can assert that we filled the whole
|
||||
// output buffer with stereo frames
|
||||
assert_eq!(outdata.len(), written_samples * 2);
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
|
||||
fn drain(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let settings = &self.settings.lock().unwrap();
|
||||
|
||||
let mut state_guard = self.state.lock().unwrap();
|
||||
let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?;
|
||||
|
||||
let avail = state.adapter.available();
|
||||
|
||||
if avail == 0 {
|
||||
return Ok(gst::FlowSuccess::Ok);
|
||||
}
|
||||
|
||||
let inblksz = state.input_block_size();
|
||||
let outblksz = state.output_block_size();
|
||||
let inbpf = state.ininfo.bpf() as usize;
|
||||
let outbpf = state.outinfo.bpf() as usize;
|
||||
|
||||
let inputsz = inblksz - avail;
|
||||
let outputsz = avail / inbpf * outbpf;
|
||||
|
||||
let mut inbuf = gst::Buffer::with_size(inputsz).map_err(|_| gst::FlowError::Error)?;
|
||||
let inbuf_mut = inbuf.get_mut().ok_or(gst::FlowError::Error)?;
|
||||
|
||||
let mut map = inbuf_mut
|
||||
.map_writable()
|
||||
.map_err(|_| gst::FlowError::Error)?;
|
||||
let data = map
|
||||
.as_mut_slice_of::<f32>()
|
||||
.map_err(|_| gst::FlowError::Error)?;
|
||||
|
||||
data.fill(0.0);
|
||||
drop(map);
|
||||
|
||||
let (pts, offset, duration) = {
|
||||
let samples_to_time = |samples: u64| {
|
||||
samples
|
||||
.mul_div_round(*gst::ClockTime::SECOND, state.ininfo.rate() as u64)
|
||||
.map(gst::ClockTime::from_nseconds)
|
||||
};
|
||||
|
||||
let (prev_pts, distance) = state.adapter.prev_pts();
|
||||
let distance_samples = distance / inbpf as u64;
|
||||
let pts = prev_pts.opt_add(samples_to_time(distance_samples));
|
||||
|
||||
let (prev_offset, _) = state.adapter.prev_offset();
|
||||
let offset = prev_offset.checked_add(distance_samples).unwrap_or(0);
|
||||
|
||||
let duration_samples = outputsz / outbpf;
|
||||
let duration = samples_to_time(duration_samples as u64);
|
||||
|
||||
(pts, offset, duration)
|
||||
};
|
||||
|
||||
state.adapter.push(inbuf);
|
||||
|
||||
let mut outbuf = gst::Buffer::with_size(outblksz).map_err(|_| gst::FlowError::Error)?;
|
||||
let outbuf_mut = outbuf.get_mut().unwrap();
|
||||
|
||||
self.process(outbuf_mut, state, settings)?;
|
||||
|
||||
outbuf_mut.set_size(outputsz);
|
||||
outbuf_mut.set_pts(pts);
|
||||
outbuf_mut.set_offset(offset);
|
||||
outbuf_mut.set_duration(duration);
|
||||
|
||||
state.reset_processors();
|
||||
|
||||
drop(state_guard);
|
||||
self.obj().src_pad().push(outbuf)
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for HrtfRender {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecBoxed::builder::<glib::Bytes>("hrir-raw")
|
||||
.nick("Head Transform Impulse Response")
|
||||
.blurb("Head Transform Impulse Response raw bytes")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("hrir-file")
|
||||
.nick("Head Transform Impulse Response")
|
||||
.blurb("Head Transform Impulse Response file location to read from")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecUInt64::builder("interpolation-steps")
|
||||
.nick("Interpolation Steps")
|
||||
.blurb("Interpolation Steps is the amount of slices to cut source to")
|
||||
.maximum(u64::MAX - 1)
|
||||
.default_value(DEFAULT_INTERPOLATION_STEPS)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecUInt64::builder("block-length")
|
||||
.nick("Block Length")
|
||||
.blurb("Block Length is the length of each slice")
|
||||
.maximum(u64::MAX - 1)
|
||||
.default_value(DEFAULT_BLOCK_LENGTH)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
gst::ParamSpecArray::builder("spatial-objects")
|
||||
.element_spec(
|
||||
&glib::ParamSpecBoxed::builder::<gst::Structure>("spatial-object")
|
||||
.nick("Spatial Object")
|
||||
.blurb("Spatial Object Metadata")
|
||||
.build(),
|
||||
)
|
||||
.nick("Spatial Objects")
|
||||
.blurb("Spatial object Metadata to apply on input channels")
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"hrir-raw" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.hrir_raw_bytes = value.get().expect("type checked upstream");
|
||||
}
|
||||
"hrir-file" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.hrir_file_location = value.get().expect("type checked upstream");
|
||||
}
|
||||
"interpolation-steps" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.interpolation_steps = value.get().expect("type checked upstream");
|
||||
}
|
||||
"block-length" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.block_length = value.get().expect("type checked upstream");
|
||||
}
|
||||
"spatial-objects" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
|
||||
let objs = value
|
||||
.get::<gst::Array>()
|
||||
.expect("type checked upstream")
|
||||
.iter()
|
||||
.map(|v| {
|
||||
let s = v.get::<gst::Structure>().expect("type checked upstream");
|
||||
SpatialObject::from(s)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut state_guard = self.state.lock().unwrap();
|
||||
|
||||
if let Some(state) = state_guard.as_mut() {
|
||||
if objs.len() != state.ininfo.channels() as usize {
|
||||
gst::warning!(
|
||||
CAT,
|
||||
"Could not update spatial objects, expected {} channels, got {}",
|
||||
state.ininfo.channels(),
|
||||
objs.len()
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
settings.spatial_objects = if objs.is_empty() { None } else { Some(objs) };
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"hrir-raw" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.hrir_raw_bytes.to_value()
|
||||
}
|
||||
"hrir-file" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.hrir_file_location.to_value()
|
||||
}
|
||||
"interpolation-steps" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.interpolation_steps.to_value()
|
||||
}
|
||||
"block-length" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.block_length.to_value()
|
||||
}
|
||||
"spatial-objects" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
settings
|
||||
.spatial_objects
|
||||
.as_ref()
|
||||
.unwrap_or(&Vec::new())
|
||||
.iter()
|
||||
.map(|x| gst::Structure::from(*x).to_send_value())
|
||||
.collect::<gst::Array>()
|
||||
.to_value()
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for HrtfRender {}
|
||||
|
||||
impl ElementImpl for HrtfRender {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Head-Related Transfer Function (HRTF) renderer",
|
||||
"Filter/Effect/Audio",
|
||||
"Renders spatial sounds to a given position",
|
||||
"Tomasz Andrzejak <andreiltd@gmail.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.channels(2)
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.build();
|
||||
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&src_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sink_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.channels_range(1..=64)
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.build();
|
||||
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&sink_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template, sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseTransformImpl for HrtfRender {
|
||||
const MODE: gst_base::subclass::BaseTransformMode =
|
||||
gst_base::subclass::BaseTransformMode::NeverInPlace;
|
||||
const PASSTHROUGH_ON_SAME_CAPS: bool = false;
|
||||
const TRANSFORM_IP_ON_PASSTHROUGH: bool = false;
|
||||
|
||||
fn transform(
|
||||
&self,
|
||||
inbuf: &gst::Buffer,
|
||||
outbuf: &mut gst::BufferRef,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let settings = &self.settings.lock().unwrap();
|
||||
|
||||
let mut state_guard = self.state.lock().unwrap();
|
||||
let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?;
|
||||
|
||||
state.adapter.push(inbuf.clone());
|
||||
|
||||
if state.adapter.available() >= state.input_block_size() {
|
||||
return self.process(outbuf, state, settings);
|
||||
}
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
|
||||
fn transform_size(
|
||||
&self,
|
||||
_direction: gst::PadDirection,
|
||||
_caps: &gst::Caps,
|
||||
size: usize,
|
||||
_othercaps: &gst::Caps,
|
||||
) -> Option<usize> {
|
||||
assert_ne!(_direction, gst::PadDirection::Src);
|
||||
|
||||
let mut state_guard = self.state.lock().unwrap();
|
||||
let state = state_guard.as_mut()?;
|
||||
|
||||
let othersize = {
|
||||
let full_blocks = (size + state.adapter.available()) / (state.input_block_size());
|
||||
full_blocks * state.output_block_size()
|
||||
};
|
||||
|
||||
gst::log!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Adapter size: {}, input size {}, transformed size {}",
|
||||
state.adapter.available(),
|
||||
size,
|
||||
othersize,
|
||||
);
|
||||
|
||||
Some(othersize)
|
||||
}
|
||||
|
||||
fn transform_caps(
|
||||
&self,
|
||||
direction: gst::PadDirection,
|
||||
caps: &gst::Caps,
|
||||
filter: Option<&gst::Caps>,
|
||||
) -> Option<gst::Caps> {
|
||||
let mut other_caps = {
|
||||
let mut new_caps = caps.clone();
|
||||
|
||||
for s in new_caps.make_mut().iter_mut() {
|
||||
s.set("format", gst_audio::AUDIO_FORMAT_F32.to_str());
|
||||
s.set("layout", "interleaved");
|
||||
|
||||
if direction == gst::PadDirection::Sink {
|
||||
s.set("channels", 2);
|
||||
s.set("channel-mask", gst::Bitmask(0x3));
|
||||
} else {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
if let Some(objs) = &settings.spatial_objects {
|
||||
s.set("channels", objs.len() as i32);
|
||||
} else {
|
||||
s.set("channels", gst::IntRange::new(1, i32::MAX));
|
||||
}
|
||||
|
||||
s.remove_field("channel-mask");
|
||||
}
|
||||
}
|
||||
new_caps
|
||||
};
|
||||
|
||||
if let Some(filter) = filter {
|
||||
other_caps = filter.intersect_with_mode(&other_caps, gst::CapsIntersectMode::First);
|
||||
}
|
||||
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Transformed caps from {} to {} in direction {:?}",
|
||||
caps,
|
||||
other_caps,
|
||||
direction
|
||||
);
|
||||
|
||||
Some(other_caps)
|
||||
}
|
||||
|
||||
fn set_caps(&self, incaps: &gst::Caps, outcaps: &gst::Caps) -> Result<(), gst::LoggableError> {
|
||||
let ininfo = gst_audio::AudioInfo::from_caps(incaps)
|
||||
.map_err(|_| gst::loggable_error!(CAT, "Failed to parse input caps"))?;
|
||||
|
||||
let outinfo = gst_audio::AudioInfo::from_caps(outcaps)
|
||||
.map_err(|_| gst::loggable_error!(CAT, "Failed to parse output caps"))?;
|
||||
|
||||
let settings = &mut self.settings.lock().unwrap();
|
||||
|
||||
if settings.spatial_objects.is_none() {
|
||||
if let Some(positions) = ininfo.positions() {
|
||||
let objs: Result<Vec<_>, _> = positions
|
||||
.iter()
|
||||
.map(|p| SpatialObject::try_from(*p))
|
||||
.collect();
|
||||
|
||||
if objs.is_err() {
|
||||
return Err(gst::loggable_error!(CAT, "Unsupported channel position"));
|
||||
}
|
||||
|
||||
settings.spatial_objects = objs.ok();
|
||||
} else {
|
||||
return Err(gst::loggable_error!(CAT, "Cannot infer object positions"));
|
||||
}
|
||||
}
|
||||
|
||||
if settings.spatial_objects.as_ref().unwrap().len() != ininfo.channels() as usize {
|
||||
return Err(gst::loggable_error!(CAT, "Wrong number of spatial objects"));
|
||||
}
|
||||
|
||||
let sphere = settings
|
||||
.sphere(ininfo.rate())
|
||||
.map_err(|e| gst::loggable_error!(CAT, "Failed to load sphere {:?}", e))?;
|
||||
|
||||
let steps = settings.interpolation_steps as usize;
|
||||
let blklen = settings.block_length as usize;
|
||||
|
||||
let block_samples = blklen
|
||||
.checked_mul(steps)
|
||||
.ok_or_else(|| gst::loggable_error!(CAT, "Not enough memory for frame allocation"))?;
|
||||
|
||||
let channel_processors = (0..ininfo.channels())
|
||||
.map(|_| ChannelProcessor {
|
||||
prev_left_samples: vec![0.0; block_samples],
|
||||
prev_right_samples: vec![0.0; block_samples],
|
||||
prev_sample_vector: None,
|
||||
prev_distance_gain: None,
|
||||
indata_scratch: vec![0.0; block_samples].into_boxed_slice(),
|
||||
outdata_scratch: vec![(0.0, 0.0); block_samples].into_boxed_slice(),
|
||||
processor: HrtfProcessor::new(sphere.to_owned(), steps, blklen),
|
||||
})
|
||||
.collect();
|
||||
|
||||
*self.state.lock().unwrap() = Some(State {
|
||||
ininfo,
|
||||
outinfo,
|
||||
block_samples,
|
||||
channel_processors,
|
||||
adapter: gst_base::UniqueAdapter::new(),
|
||||
});
|
||||
|
||||
gst::debug!(CAT, imp: self, "Configured for caps {}", incaps);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sink_event(&self, event: gst::Event) -> bool {
|
||||
use gst::EventView;
|
||||
|
||||
gst::debug!(CAT, imp: self, "Handling event {:?}", event);
|
||||
|
||||
match event.view() {
|
||||
EventView::FlushStop(_) => {
|
||||
let mut state_guard = self.state.lock().unwrap();
|
||||
|
||||
if let Some(state) = state_guard.as_mut() {
|
||||
let avail = state.adapter.available();
|
||||
state.adapter.flush(avail);
|
||||
state.reset_processors();
|
||||
}
|
||||
}
|
||||
EventView::Eos(_) => {
|
||||
if self.drain().is_err() {
|
||||
gst::warning!(CAT, "Failed to drain internal buffer");
|
||||
gst::element_imp_warning!(
|
||||
self,
|
||||
gst::CoreError::Event,
|
||||
["Failed to drain internal buffer"]
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.parent_sink_event(event)
|
||||
}
|
||||
|
||||
fn start(&self) -> Result<(), gst::ErrorMessage> {
|
||||
// get global thread pool
|
||||
let mut thread_pool_g = THREAD_POOL.lock().unwrap();
|
||||
let mut thread_pool = self.thread_pool.lock().unwrap();
|
||||
|
||||
if let Some(tp) = thread_pool_g.upgrade() {
|
||||
*thread_pool = Some(tp);
|
||||
} else {
|
||||
let tp = ThreadPoolBuilder::new().build().map_err(|_| {
|
||||
gst::error_msg!(
|
||||
gst::CoreError::Failed,
|
||||
["Could not create rayon thread pool"]
|
||||
)
|
||||
})?;
|
||||
|
||||
let tp = Arc::new(tp);
|
||||
|
||||
*thread_pool = Some(tp);
|
||||
*thread_pool_g = Arc::downgrade(thread_pool.as_ref().unwrap());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
// Drop state
|
||||
let _ = self.state.lock().unwrap().take();
|
||||
// Drop thread pool
|
||||
let _ = self.thread_pool.lock().unwrap().take();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
25
audio/audiofx/src/hrtfrender/mod.rs
Normal file
25
audio/audiofx/src/hrtfrender/mod.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright (C) 2021 Tomasz Andrzejak <andreiltd@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct HrtfRender(ObjectSubclass<imp::HrtfRender>) @extends gst_base::BaseTransform, gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"hrtfrender",
|
||||
gst::Rank::NONE,
|
||||
HrtfRender::static_type(),
|
||||
)
|
||||
}
|
43
audio/audiofx/src/lib.rs
Normal file
43
audio/audiofx/src/lib.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright (C) 2017 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
|
||||
|
||||
/**
|
||||
* plugin-rsaudiofx:
|
||||
*
|
||||
* Since: plugins-rs-0.1
|
||||
*/
|
||||
use gst::glib;
|
||||
|
||||
mod audioecho;
|
||||
mod audioloudnorm;
|
||||
mod audiornnoise;
|
||||
mod ebur128level;
|
||||
mod hrtfrender;
|
||||
|
||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
audioecho::register(plugin)?;
|
||||
audioloudnorm::register(plugin)?;
|
||||
audiornnoise::register(plugin)?;
|
||||
ebur128level::register(plugin)?;
|
||||
hrtfrender::register(plugin)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
gst::plugin_define!(
|
||||
rsaudiofx,
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
plugin_init,
|
||||
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||
// FIXME: MPL-2.0 is only allowed since 1.18.3 (as unknown) and 1.20 (as known)
|
||||
"MPL",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
env!("BUILD_REL_DATE")
|
||||
);
|
257
audio/audiofx/tests/audioloudnorm.rs
Normal file
257
audio/audiofx/tests/audioloudnorm.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
// Copyright (C) 2020 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstrsaudiofx::plugin_register_static().expect("Failed to register rsaudiofx plugin");
|
||||
});
|
||||
}
|
||||
|
||||
fn run_test(
|
||||
first_input: &str,
|
||||
second_input: Option<&str>,
|
||||
num_buffers: u32,
|
||||
samples_per_buffer: u32,
|
||||
channels: u32,
|
||||
expected_loudness: f64,
|
||||
) {
|
||||
init();
|
||||
|
||||
let format = if cfg!(target_endian = "little") {
|
||||
format!("audio/x-raw,format=F64LE,rate=192000,channels={channels}")
|
||||
} else {
|
||||
format!("audio/x-raw,format=F64BE,rate=192000,channels={channels}")
|
||||
};
|
||||
|
||||
let pipeline = if let Some(second_input) = second_input {
|
||||
gst::parse::launch(&format!(
|
||||
"audiotestsrc {first_input} num-buffers={num_buffers} samplesperbuffer={samples_per_buffer} ! {format} ! audiomixer name=mixer output-buffer-duration={output_buffer_duration} ! {format} ! audioloudnorm ! appsink name=sink audiotestsrc {second_input} num-buffers={num_buffers} samplesperbuffer={samples_per_buffer} ! {format} ! mixer.",
|
||||
first_input = first_input,
|
||||
second_input = second_input,
|
||||
num_buffers = num_buffers,
|
||||
samples_per_buffer = samples_per_buffer,
|
||||
output_buffer_duration = samples_per_buffer as u64 * *gst::ClockTime::SECOND / 192_000,
|
||||
format = format,
|
||||
))
|
||||
} else {
|
||||
gst::parse::launch(&format!(
|
||||
"audiotestsrc {first_input} num-buffers={num_buffers} samplesperbuffer={samples_per_buffer} ! {format} ! audioloudnorm ! appsink name=sink",
|
||||
))
|
||||
}
|
||||
.unwrap()
|
||||
.downcast::<gst::Pipeline>()
|
||||
.unwrap();
|
||||
let sink = pipeline
|
||||
.by_name("sink")
|
||||
.unwrap()
|
||||
.downcast::<gst_app::AppSink>()
|
||||
.unwrap();
|
||||
|
||||
sink.set_sync(false);
|
||||
let caps = gst_audio::AudioInfo::builder(gst_audio::AUDIO_FORMAT_F64, 192_000, channels)
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_caps()
|
||||
.unwrap();
|
||||
sink.set_caps(Some(&caps));
|
||||
|
||||
let samples = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let samples_clone = samples.clone();
|
||||
sink.set_callbacks(
|
||||
gst_app::AppSinkCallbacks::builder()
|
||||
.new_sample(move |sink| {
|
||||
let sample = sink.pull_sample().unwrap();
|
||||
|
||||
let mut samples = samples_clone.lock().unwrap();
|
||||
samples.push(sample);
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
})
|
||||
.build(),
|
||||
);
|
||||
|
||||
pipeline.set_state(gst::State::Playing).unwrap();
|
||||
|
||||
let mut eos = false;
|
||||
let bus = pipeline.bus().unwrap();
|
||||
while let Some(msg) = bus.timed_pop(gst::ClockTime::NONE) {
|
||||
use gst::MessageView;
|
||||
match msg.view() {
|
||||
MessageView::Eos(..) => {
|
||||
eos = true;
|
||||
break;
|
||||
}
|
||||
MessageView::Error(..) => unreachable!(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pipeline.set_state(gst::State::Null).unwrap();
|
||||
|
||||
assert!(eos);
|
||||
let samples = samples.lock().unwrap();
|
||||
|
||||
let mut r128 = ebur128::EbuR128::new(
|
||||
channels,
|
||||
192_000,
|
||||
ebur128::Mode::I | ebur128::Mode::SAMPLE_PEAK,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut num_samples = 0;
|
||||
let mut expected_ts = gst::ClockTime::ZERO;
|
||||
for sample in samples.iter() {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
let buf = sample.buffer().unwrap();
|
||||
|
||||
let ts = buf.pts().expect("undefined pts");
|
||||
match ts.cmp(&expected_ts) {
|
||||
Ordering::Greater => {
|
||||
assert!(
|
||||
ts - expected_ts <= gst::ClockTime::NSECOND,
|
||||
"TS is {ts} instead of {expected_ts}"
|
||||
);
|
||||
}
|
||||
Ordering::Less => {
|
||||
assert!(
|
||||
expected_ts - ts <= gst::ClockTime::NSECOND,
|
||||
"TS is {ts} instead of {expected_ts}"
|
||||
);
|
||||
}
|
||||
Ordering::Equal => (),
|
||||
}
|
||||
|
||||
let map = buf.map_readable().unwrap();
|
||||
let data = map.as_slice_of::<f64>().unwrap();
|
||||
|
||||
num_samples += data.len() / channels as usize;
|
||||
r128.add_frames_f64(data).unwrap();
|
||||
|
||||
expected_ts += (data.len() as u64 / channels as u64).seconds() / 192_000;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
num_samples,
|
||||
num_buffers as usize * samples_per_buffer as usize
|
||||
);
|
||||
|
||||
let loudness = r128.loudness_global().unwrap();
|
||||
|
||||
if expected_loudness.classify() == std::num::FpCategory::Infinite && expected_loudness < 0.0 {
|
||||
assert!(
|
||||
loudness.classify() == std::num::FpCategory::Infinite && loudness < 0.0,
|
||||
"Loudness is {loudness} instead of {expected_loudness}",
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
f64::abs(loudness - expected_loudness) < 1.0,
|
||||
"Loudness is {loudness} instead of {expected_loudness}",
|
||||
);
|
||||
}
|
||||
|
||||
for c in 0..channels {
|
||||
let peak = 20.0 * f64::log10(r128.sample_peak(c).unwrap());
|
||||
assert!(peak <= -2.0, "Peak {c} for channel {peak} is above -2.0",);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
run_test("wave=sine", None, 1000, 1920, 1, -24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_white_noise() {
|
||||
run_test("wave=white-noise", None, 1000, 1920, 1, -24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaining_at_eos() {
|
||||
run_test("wave=sine", None, 1000, 1024, 1, -24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_input() {
|
||||
run_test("wave=sine", None, 100, 1024, 1, -24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_two_channels() {
|
||||
run_test("wave=sine", None, 1000, 1920, 2, -24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn silence() {
|
||||
run_test("wave=silence", None, 1000, 1024, 1, f64::NEG_INFINITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quiet() {
|
||||
// -6dB
|
||||
run_test("wave=sine volume=0.5", None, 1000, 1024, 1, -24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn very_quiet() {
|
||||
// -20dB
|
||||
run_test("wave=sine volume=0.1", None, 1000, 1024, 1, -24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn very_very_quiet() {
|
||||
// -40dB
|
||||
run_test("wave=sine volume=0.01", None, 1000, 1024, 1, -24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn below_threshold() {
|
||||
// -70dB
|
||||
run_test(
|
||||
"wave=sine volume=0.00045",
|
||||
None,
|
||||
1000,
|
||||
1024,
|
||||
1,
|
||||
f64::NEG_INFINITY,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limiter() {
|
||||
run_test(
|
||||
"wave=sine volume=0.05",
|
||||
Some("wave=ticks sine-periods-per-tick=1 tick-interval=4000000000"),
|
||||
1000,
|
||||
1024,
|
||||
1,
|
||||
-24.0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limiter_on_first_frame() {
|
||||
run_test(
|
||||
"wave=sine volume=0.05",
|
||||
Some("wave=ticks sine-periods-per-tick=10 tick-interval=4000000000"),
|
||||
1000,
|
||||
1024,
|
||||
1,
|
||||
-24.0,
|
||||
);
|
||||
}
|
80
audio/audiofx/tests/audiornnoise.rs
Normal file
80
audio/audiofx/tests/audiornnoise.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Copyright (C) 2020 Philippe Normand <philn@igalia.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstrsaudiofx::plugin_register_static().expect("Failed to register rsaudiofx plugin");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rnnoise_silence_big_buffers() {
|
||||
init();
|
||||
let audio_info = gst_audio::AudioInfo::builder(gst_audio::AUDIO_FORMAT_F32, 48000, 2)
|
||||
.build()
|
||||
.unwrap();
|
||||
test_rnnoise(&audio_info, 4096);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rnnoise_silence_small_buffers() {
|
||||
init();
|
||||
let audio_info = gst_audio::AudioInfo::builder(gst_audio::AUDIO_FORMAT_F32, 48000, 2)
|
||||
.build()
|
||||
.unwrap();
|
||||
test_rnnoise(&audio_info, 1024);
|
||||
}
|
||||
|
||||
fn test_rnnoise(audio_info: &gst_audio::AudioInfo, buffer_size: usize) {
|
||||
let filter = gst::ElementFactory::make("audiornnoise").build().unwrap();
|
||||
let mut h = gst_check::Harness::with_element(&filter, Some("sink"), Some("src"));
|
||||
let sink_caps = audio_info.to_caps().unwrap();
|
||||
let src_caps = sink_caps.clone();
|
||||
h.set_caps(src_caps, sink_caps);
|
||||
h.play();
|
||||
|
||||
let buffer = {
|
||||
let mut buffer = gst::Buffer::with_size(buffer_size * audio_info.bpf() as usize).unwrap();
|
||||
{
|
||||
let format_info = audio_info.format_info();
|
||||
let buffer_mut = buffer.get_mut().unwrap();
|
||||
let mut omap = buffer_mut.map_writable().unwrap();
|
||||
let odata = omap.as_mut_slice_of::<u8>().unwrap();
|
||||
format_info.fill_silence(odata);
|
||||
}
|
||||
|
||||
buffer
|
||||
};
|
||||
|
||||
let num_buffers = 5;
|
||||
let mut total_processed = 0;
|
||||
for _ in 0..num_buffers {
|
||||
let result = h.push_and_pull(buffer.clone()).unwrap();
|
||||
let map = result.into_mapped_buffer_readable().unwrap();
|
||||
let output = map.as_slice().as_slice_of::<f64>().unwrap();
|
||||
|
||||
// all samples in the output buffers must value 0
|
||||
assert!(output.iter().all(|sample| *sample as u16 == 0u16));
|
||||
total_processed += output.len();
|
||||
}
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
let last_buffer = h.pull().unwrap();
|
||||
let map = last_buffer.into_mapped_buffer_readable().unwrap();
|
||||
let output = map.as_slice().as_slice_of::<f64>().unwrap();
|
||||
total_processed += output.len();
|
||||
// The total amount of samples pushed into the element shall be equal to the
|
||||
// total amount of samples pulled from it.
|
||||
assert_eq!(total_processed, num_buffers * buffer_size);
|
||||
}
|
153
audio/audiofx/tests/ebur128level.rs
Normal file
153
audio/audiofx/tests/ebur128level.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
// Copyright (C) 2021 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
#![allow(clippy::single_match)]
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstrsaudiofx::plugin_register_static().expect("Failed to register rsaudiofx plugin");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ebur128level_s16_interleaved() {
|
||||
init();
|
||||
run_test(
|
||||
gst_audio::AudioLayout::Interleaved,
|
||||
gst_audio::AUDIO_FORMAT_S16,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ebur128level_s32_interleaved() {
|
||||
init();
|
||||
run_test(
|
||||
gst_audio::AudioLayout::Interleaved,
|
||||
gst_audio::AUDIO_FORMAT_S32,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ebur128level_f32_interleaved() {
|
||||
init();
|
||||
run_test(
|
||||
gst_audio::AudioLayout::Interleaved,
|
||||
gst_audio::AUDIO_FORMAT_F32,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ebur128level_f64_interleaved() {
|
||||
init();
|
||||
run_test(
|
||||
gst_audio::AudioLayout::Interleaved,
|
||||
gst_audio::AUDIO_FORMAT_F64,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ebur128level_s16_non_interleaved() {
|
||||
init();
|
||||
run_test(
|
||||
gst_audio::AudioLayout::NonInterleaved,
|
||||
gst_audio::AUDIO_FORMAT_S16,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ebur128level_s32_non_interleaved() {
|
||||
init();
|
||||
run_test(
|
||||
gst_audio::AudioLayout::NonInterleaved,
|
||||
gst_audio::AUDIO_FORMAT_S32,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ebur128level_f32_non_interleaved() {
|
||||
init();
|
||||
run_test(
|
||||
gst_audio::AudioLayout::NonInterleaved,
|
||||
gst_audio::AUDIO_FORMAT_F32,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ebur128level_f64_non_interleaved() {
|
||||
init();
|
||||
run_test(
|
||||
gst_audio::AudioLayout::NonInterleaved,
|
||||
gst_audio::AUDIO_FORMAT_F64,
|
||||
);
|
||||
}
|
||||
|
||||
fn run_test(layout: gst_audio::AudioLayout, format: gst_audio::AudioFormat) {
|
||||
let mut h = gst_check::Harness::new_parse(&format!(
|
||||
"audiotestsrc num-buffers=5 samplesperbuffer=48000 ! \
|
||||
audioconvert ! \
|
||||
audio/x-raw,layout={},format={},channels=2,rate=48000 ! \
|
||||
ebur128level interval=500000000",
|
||||
match layout {
|
||||
gst_audio::AudioLayout::Interleaved => "interleaved",
|
||||
gst_audio::AudioLayout::NonInterleaved => "non-interleaved",
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
format.to_str()
|
||||
));
|
||||
let bus = gst::Bus::new();
|
||||
h.element().unwrap().set_bus(Some(&bus));
|
||||
h.play();
|
||||
|
||||
// Pull all buffers until EOS
|
||||
let mut num_buffers = 0;
|
||||
while let Some(_buffer) = h.pull_until_eos().unwrap() {
|
||||
num_buffers += 1;
|
||||
}
|
||||
assert_eq!(num_buffers, 5);
|
||||
|
||||
let mut num_msgs = 0;
|
||||
while let Some(msg) = bus.pop() {
|
||||
match msg.view() {
|
||||
gst::MessageView::Element(msg) => {
|
||||
let s = msg.structure().unwrap();
|
||||
if s.name() == "ebur128-level" {
|
||||
num_msgs += 1;
|
||||
let timestamp = s.get::<u64>("timestamp").unwrap();
|
||||
let running_time = s.get::<u64>("running-time").unwrap();
|
||||
let stream_time = s.get::<u64>("stream-time").unwrap();
|
||||
assert_eq!(timestamp, num_msgs * 500 * *gst::ClockTime::MSECOND);
|
||||
assert_eq!(running_time, num_msgs * 500 * *gst::ClockTime::MSECOND);
|
||||
assert_eq!(stream_time, num_msgs * 500 * *gst::ClockTime::MSECOND);
|
||||
|
||||
// Check if all these exist
|
||||
let _momentary_loudness = s.get::<f64>("momentary-loudness").unwrap();
|
||||
let _shortterm_loudness = s.get::<f64>("shortterm-loudness").unwrap();
|
||||
let _global_loudness = s.get::<f64>("global-loudness").unwrap();
|
||||
let _relative_threshold = s.get::<f64>("relative-threshold").unwrap();
|
||||
let _loudness_range = s.get::<f64>("loudness-range").unwrap();
|
||||
let sample_peak = s.get::<gst::ArrayRef>("sample-peak").unwrap();
|
||||
assert_eq!(sample_peak.as_slice().len(), 2);
|
||||
assert_eq!(sample_peak.as_slice()[0].type_(), glib::Type::F64);
|
||||
let true_peak = s.get::<gst::ArrayRef>("true-peak").unwrap();
|
||||
assert_eq!(true_peak.as_slice().len(), 2);
|
||||
assert_eq!(true_peak.as_slice()[0].type_(), glib::Type::F64);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(num_msgs, 10);
|
||||
}
|
246
audio/audiofx/tests/hrtfrender.rs
Normal file
246
audio/audiofx/tests/hrtfrender.rs
Normal file
|
@ -0,0 +1,246 @@
|
|||
// Copyright (C) 2021 Tomasz Andrzejak <andreiltd@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CONFIG: Lazy<glib::Bytes> = Lazy::new(|| {
|
||||
let buff = include_bytes!("test.hrir");
|
||||
glib::Bytes::from_owned(buff)
|
||||
});
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstrsaudiofx::plugin_register_static().expect("Failed to register rsaudiofx plugin");
|
||||
});
|
||||
}
|
||||
|
||||
fn build_harness(src_caps: gst::Caps, sink_caps: gst::Caps) -> (gst_check::Harness, gst::Element) {
|
||||
let hrtf = gst::ElementFactory::make("hrtfrender")
|
||||
.property("hrir-raw", &*CONFIG)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut h = gst_check::Harness::with_element(&hrtf, Some("sink"), Some("src"));
|
||||
h.set_caps(src_caps, sink_caps);
|
||||
|
||||
(h, hrtf)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hrtfrender_samples_in_samples_out() {
|
||||
init();
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(1)
|
||||
.channel_mask(0x1)
|
||||
.build();
|
||||
|
||||
let sink_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(2)
|
||||
.build();
|
||||
|
||||
let (mut h, _) = build_harness(src_caps, sink_caps);
|
||||
h.play();
|
||||
|
||||
let inbpf = 4;
|
||||
let outbpf = 8;
|
||||
let full_block = 512 * 8;
|
||||
|
||||
let mut buffer = gst::Buffer::with_size(full_block * inbpf + 20 * inbpf).unwrap();
|
||||
let buffer_mut = buffer.get_mut().unwrap();
|
||||
|
||||
let full_block_time = (full_block as u64)
|
||||
.mul_div_round(*gst::ClockTime::SECOND, 44_100)
|
||||
.map(gst::ClockTime::from_nseconds);
|
||||
|
||||
buffer_mut.set_pts(gst::ClockTime::ZERO);
|
||||
buffer_mut.set_duration(full_block_time);
|
||||
buffer_mut.set_offset(0);
|
||||
|
||||
let ret = h.push(buffer);
|
||||
assert!(ret.is_ok());
|
||||
|
||||
let buffer = h.pull().unwrap();
|
||||
assert_eq!(buffer.size(), full_block * outbpf);
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
let buffer = h.pull().unwrap();
|
||||
|
||||
assert_eq!(buffer.size(), 20 * outbpf);
|
||||
assert_eq!(buffer.offset(), full_block as u64);
|
||||
assert_eq!(buffer.pts(), full_block_time);
|
||||
|
||||
let residue_time = 20
|
||||
.mul_div_round(*gst::ClockTime::SECOND, 44_100)
|
||||
.map(gst::ClockTime::from_nseconds);
|
||||
|
||||
assert_eq!(buffer.duration(), residue_time);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hrtfrender_implicit_spatial_objects() {
|
||||
init();
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(8)
|
||||
.channel_mask(0xc3f)
|
||||
.build();
|
||||
|
||||
let sink_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(2)
|
||||
.build();
|
||||
|
||||
let (mut h, hrtf) = build_harness(src_caps, sink_caps);
|
||||
let objs = hrtf.property::<gst::Array>("spatial-objects");
|
||||
|
||||
h.play();
|
||||
|
||||
assert_eq!(objs.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hrtfrender_explicit_spatial_objects() {
|
||||
init();
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(8)
|
||||
.build();
|
||||
|
||||
let sink_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(2)
|
||||
.build();
|
||||
|
||||
let (mut h, hrtf) = build_harness(src_caps, sink_caps);
|
||||
|
||||
let objs = (0..8)
|
||||
.map(|x| {
|
||||
gst::Structure::builder("application/spatial-object")
|
||||
.field("x", -1f32 + x as f32 / 8f32)
|
||||
.field("y", 0f32)
|
||||
.field("z", 1f32)
|
||||
.field("distance-gain", 0.1f32)
|
||||
.build()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
hrtf.set_property("spatial-objects", gst::Array::new(objs));
|
||||
|
||||
h.play();
|
||||
|
||||
let objs = hrtf.property::<gst::Array>("spatial-objects");
|
||||
assert_eq!(objs.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Caps negotiation should fail if we have mismatch between input channels and
|
||||
// of objects that we set via property. In this test case input has 6 channels
|
||||
// but the number of spatial objects set is 2.
|
||||
fn test_hrtfrender_caps_negotiation_fail() {
|
||||
init();
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(6)
|
||||
.build();
|
||||
|
||||
let sink_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(2)
|
||||
.build();
|
||||
|
||||
let (mut h, hrtf) = build_harness(src_caps, sink_caps);
|
||||
|
||||
let objs = (0..2)
|
||||
.map(|_| {
|
||||
gst::Structure::builder("application/spatial-object")
|
||||
.field("x", 0f32)
|
||||
.field("y", 0f32)
|
||||
.field("z", 1f32)
|
||||
.field("distance-gain", 0.1f32)
|
||||
.build()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
hrtf.set_property("spatial-objects", gst::Array::new(objs));
|
||||
|
||||
h.play();
|
||||
|
||||
let buffer = gst::Buffer::with_size(2048).unwrap();
|
||||
assert_eq!(h.push(buffer), Err(gst::FlowError::NotNegotiated));
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
// The harness sinkpad end up not having defined caps so, the current_caps
|
||||
// should be None
|
||||
let current_caps = h.sinkpad().expect("harness has no sinkpad").current_caps();
|
||||
|
||||
assert!(current_caps.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hrtfrender_multiple_instances_sharing_thread_pool() {
|
||||
init();
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(1)
|
||||
.build();
|
||||
|
||||
let sink_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(2)
|
||||
.build();
|
||||
|
||||
let (_, hrtf) = build_harness(src_caps.clone(), sink_caps.clone());
|
||||
|
||||
let block_length: u64 = hrtf.property::<u64>("block-length");
|
||||
let steps: u64 = hrtf.property::<u64>("interpolation-steps");
|
||||
let bps: u64 = 4;
|
||||
|
||||
drop(hrtf);
|
||||
let blksz = (block_length * steps * bps) as usize;
|
||||
|
||||
let mut harnesses = (0..4)
|
||||
.map(|_| build_harness(src_caps.clone(), sink_caps.clone()).0)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for h in harnesses.iter_mut() {
|
||||
h.play();
|
||||
|
||||
let buffer = gst::Buffer::with_size(blksz).unwrap();
|
||||
assert!(h.push(buffer).is_ok());
|
||||
|
||||
let buffer = h.pull().unwrap();
|
||||
assert_eq!(buffer.size(), 2 * blksz);
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
}
|
||||
}
|
BIN
audio/audiofx/tests/test.hrir
Normal file
BIN
audio/audiofx/tests/test.hrir
Normal file
Binary file not shown.
47
audio/claxon/Cargo.toml
Normal file
47
audio/claxon/Cargo.toml
Normal file
|
@ -0,0 +1,47 @@
|
|||
[package]
|
||||
name = "gst-plugin-claxon"
|
||||
version.workspace = true
|
||||
authors = ["Ruben Gonzalez <rgonzalez@fluendo.com>"]
|
||||
repository.workspace = true
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "GStreamer Claxon FLAC Decoder Plugin"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gst.workspace = true
|
||||
gst-audio.workspace = true
|
||||
claxon = { version = "0.4" }
|
||||
byte-slice-cast = "1.0"
|
||||
atomic_refcell = "0.1"
|
||||
once_cell.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gst-check.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "gstclaxon"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper.workspace = true
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
doc = ["gst/v1_18"]
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.9.21"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
import_library = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0"
|
201
audio/claxon/LICENSE-APACHE
Normal file
201
audio/claxon/LICENSE-APACHE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
23
audio/claxon/LICENSE-MIT
Normal file
23
audio/claxon/LICENSE-MIT
Normal file
|
@ -0,0 +1,23 @@
|
|||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
3
audio/claxon/build.rs
Normal file
3
audio/claxon/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
484
audio/claxon/src/claxondec/imp.rs
Normal file
484
audio/claxon/src/claxondec/imp.rs
Normal file
|
@ -0,0 +1,484 @@
|
|||
// Copyright (C) 2019 Ruben Gonzalez <rgonzalez@fluendo.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_audio::prelude::*;
|
||||
use gst_audio::subclass::prelude::*;
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use atomic_refcell::AtomicRefCell;
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"claxondec",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("Claxon FLAC decoder"),
|
||||
)
|
||||
});
|
||||
|
||||
struct State {
|
||||
audio_info: Option<gst_audio::AudioInfo>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClaxonDec {
|
||||
state: AtomicRefCell<Option<State>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ClaxonDec {
|
||||
const NAME: &'static str = "GstClaxonDec";
|
||||
type Type = super::ClaxonDec;
|
||||
type ParentType = gst_audio::AudioDecoder;
|
||||
}
|
||||
|
||||
impl ObjectImpl for ClaxonDec {}
|
||||
|
||||
impl GstObjectImpl for ClaxonDec {}
|
||||
|
||||
impl ElementImpl for ClaxonDec {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Claxon FLAC decoder",
|
||||
"Decoder/Audio",
|
||||
"Claxon FLAC decoder",
|
||||
"Ruben Gonzalez <rgonzalez@fluendo.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let sink_caps = gst::Caps::builder("audio/x-flac")
|
||||
.field("framed", true)
|
||||
.build();
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&sink_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format_list([
|
||||
gst_audio::AudioFormat::S8,
|
||||
gst_audio::AUDIO_FORMAT_S16,
|
||||
gst_audio::AUDIO_FORMAT_S2432,
|
||||
gst_audio::AUDIO_FORMAT_S32,
|
||||
])
|
||||
.rate_range(1..655_350)
|
||||
.channels_range(1..8)
|
||||
.build();
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&src_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![sink_pad_template, src_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioDecoderImpl for ClaxonDec {
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
*self.state.borrow_mut() = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&self) -> Result<(), gst::ErrorMessage> {
|
||||
*self.state.borrow_mut() = Some(State { audio_info: None });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_format(&self, caps: &gst::Caps) -> Result<(), gst::LoggableError> {
|
||||
gst::debug!(CAT, imp: self, "Setting format {:?}", caps);
|
||||
|
||||
let mut audio_info: Option<gst_audio::AudioInfo> = None;
|
||||
|
||||
let s = caps.structure(0).unwrap();
|
||||
if let Ok(Some(streamheaders)) = s.get_optional::<gst::ArrayRef>("streamheader") {
|
||||
let streamheaders = streamheaders.as_slice();
|
||||
|
||||
if streamheaders.len() < 2 {
|
||||
gst::debug!(CAT, imp: self, "Not enough streamheaders, trying in-band");
|
||||
} else {
|
||||
let ident_buf = streamheaders[0].get::<Option<gst::Buffer>>();
|
||||
if let Ok(Some(ident_buf)) = ident_buf {
|
||||
gst::debug!(CAT, imp: self, "Got streamheader buffers");
|
||||
let inmap = ident_buf.map_readable().unwrap();
|
||||
|
||||
if inmap[0..7] != [0x7f, b'F', b'L', b'A', b'C', 0x01, 0x00] {
|
||||
gst::debug!(CAT, imp: self, "Unknown streamheader format");
|
||||
} else if let Ok(tstreaminfo) = claxon_streaminfo(&inmap[13..]) {
|
||||
if let Ok(taudio_info) = gstaudioinfo(&tstreaminfo) {
|
||||
// To speed up negotiation
|
||||
let element = self.obj();
|
||||
if element.set_output_format(&taudio_info).is_err()
|
||||
|| element.negotiate().is_err()
|
||||
{
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Error to negotiate output from based on in-caps streaminfo"
|
||||
);
|
||||
}
|
||||
|
||||
audio_info = Some(taudio_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut state_guard = self.state.borrow_mut();
|
||||
*state_guard = Some(State { audio_info });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::verbose_bit_mask)]
|
||||
fn handle_frame(
|
||||
&self,
|
||||
inbuf: Option<&gst::Buffer>,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
gst::debug!(CAT, imp: self, "Handling buffer {:?}", inbuf);
|
||||
|
||||
let inbuf = match inbuf {
|
||||
None => return Ok(gst::FlowSuccess::Ok),
|
||||
Some(inbuf) => inbuf,
|
||||
};
|
||||
|
||||
let inmap = inbuf.map_readable().map_err(|_| {
|
||||
gst::error!(CAT, imp: self, "Failed to buffer readable");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let mut state_guard = self.state.borrow_mut();
|
||||
let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?;
|
||||
|
||||
if inmap.as_slice() == b"fLaC" {
|
||||
gst::debug!(CAT, imp: self, "fLaC buffer received");
|
||||
} else if inmap[0] & 0x7F == 0x00 {
|
||||
gst::debug!(CAT, imp: self, "Streaminfo header buffer received");
|
||||
return self.handle_streaminfo_header(state, inmap.as_ref());
|
||||
} else if inmap[0] == 0b1111_1111 && inmap[1] & 0b1111_1100 == 0b1111_1000 {
|
||||
gst::debug!(CAT, imp: self, "Data buffer received");
|
||||
return self.handle_data(state, inmap.as_ref());
|
||||
} else {
|
||||
// info about other headers in flacparse and https://xiph.org/flac/format.html
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Other header buffer received {:?}",
|
||||
inmap[0] & 0x7F
|
||||
);
|
||||
}
|
||||
|
||||
self.obj().finish_frame(None, 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClaxonDec {
|
||||
fn handle_streaminfo_header(
|
||||
&self,
|
||||
state: &mut State,
|
||||
indata: &[u8],
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let streaminfo = claxon_streaminfo(indata).map_err(|e| {
|
||||
gst::element_imp_error!(self, gst::StreamError::Decode, ["{e}"]);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let audio_info = gstaudioinfo(&streaminfo).map_err(|e| {
|
||||
gst::element_imp_error!(self, gst::StreamError::Decode, ["{e}"]);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Successfully parsed headers: {:?}",
|
||||
audio_info
|
||||
);
|
||||
|
||||
let element = self.obj();
|
||||
element.set_output_format(&audio_info)?;
|
||||
element.negotiate()?;
|
||||
|
||||
state.audio_info = Some(audio_info);
|
||||
|
||||
element.finish_frame(None, 1)
|
||||
}
|
||||
|
||||
fn handle_data(
|
||||
&self,
|
||||
state: &mut State,
|
||||
indata: &[u8],
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
// TODO It's valid for FLAC to not have any streaminfo header at all, for a small subset
|
||||
// of possible FLAC configurations. (claxon does not actually support that)
|
||||
let audio_info = state
|
||||
.audio_info
|
||||
.as_ref()
|
||||
.ok_or(gst::FlowError::NotNegotiated)?;
|
||||
let depth = AudioDepth::validate(audio_info.depth())?;
|
||||
|
||||
let channels = audio_info.channels() as usize;
|
||||
if channels > 8 {
|
||||
unreachable!(
|
||||
"FLAC only supports from 1 to 8 channels (audio contains {} channels)",
|
||||
channels
|
||||
);
|
||||
}
|
||||
|
||||
let buffer = Vec::new();
|
||||
let mut cursor = Cursor::new(indata);
|
||||
let mut reader = claxon::frame::FrameReader::new(&mut cursor);
|
||||
let result = match reader.read_next_or_eof(buffer) {
|
||||
Ok(Some(result)) => result,
|
||||
Ok(None) => return self.obj().finish_frame(None, 1),
|
||||
Err(err) => {
|
||||
return gst_audio::audio_decoder_error!(
|
||||
self.obj(),
|
||||
1,
|
||||
gst::StreamError::Decode,
|
||||
["Failed to decode packet: {:?}", err]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(cursor.position(), indata.len() as u64);
|
||||
|
||||
let v = if channels != 1 {
|
||||
let mut v: Vec<i32> = vec![0; result.len() as usize];
|
||||
|
||||
for (o, i) in v.chunks_exact_mut(channels).enumerate() {
|
||||
for (c, s) in i.iter_mut().enumerate() {
|
||||
*s = result.sample(c as u32, o as u32);
|
||||
}
|
||||
}
|
||||
v
|
||||
} else {
|
||||
result.into_buffer()
|
||||
};
|
||||
|
||||
let depth_adjusted = depth.adjust_samples(v);
|
||||
let outbuf = gst::Buffer::from_mut_slice(depth_adjusted);
|
||||
self.obj().finish_frame(Some(outbuf), 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Depth of audio samples
|
||||
enum AudioDepth {
|
||||
/// 8bits.
|
||||
I8,
|
||||
/// 16bits.
|
||||
I16,
|
||||
/// 24bits.
|
||||
I24,
|
||||
/// 32bits.
|
||||
I32,
|
||||
}
|
||||
|
||||
enum ByteVec {
|
||||
I8(Vec<i8>),
|
||||
I16(Vec<i16>),
|
||||
I32(Vec<i32>),
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for ByteVec {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
ByteVec::I8(ref vec) => vec.as_byte_slice(),
|
||||
ByteVec::I16(ref vec) => vec.as_byte_slice(),
|
||||
ByteVec::I32(ref vec) => vec.as_byte_slice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<[u8]> for ByteVec {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
match self {
|
||||
ByteVec::I8(ref mut vec) => vec.as_mut_byte_slice(),
|
||||
ByteVec::I16(ref mut vec) => vec.as_mut_byte_slice(),
|
||||
ByteVec::I32(ref mut vec) => vec.as_mut_byte_slice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioDepth {
|
||||
/// Validate input audio depth.
|
||||
fn validate(input: u32) -> Result<Self, gst::FlowError> {
|
||||
let depth = match input {
|
||||
8 => AudioDepth::I8,
|
||||
16 => AudioDepth::I16,
|
||||
24 => AudioDepth::I24,
|
||||
32 => AudioDepth::I32,
|
||||
_ => return Err(gst::FlowError::NotSupported),
|
||||
};
|
||||
Ok(depth)
|
||||
}
|
||||
|
||||
/// Adjust samples depth.
|
||||
///
|
||||
/// This takes a vector of 32bits samples, adjusts the depth of each,
|
||||
/// and returns the adjusted bytes stream.
|
||||
fn adjust_samples(&self, input: Vec<i32>) -> ByteVec {
|
||||
match *self {
|
||||
AudioDepth::I8 => ByteVec::I8(input.into_iter().map(|x| x as i8).collect::<Vec<_>>()),
|
||||
AudioDepth::I16 => {
|
||||
ByteVec::I16(input.into_iter().map(|x| x as i16).collect::<Vec<_>>())
|
||||
}
|
||||
AudioDepth::I24 | AudioDepth::I32 => ByteVec::I32(input),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn claxon_streaminfo(indata: &[u8]) -> Result<claxon::metadata::StreamInfo, &'static str> {
|
||||
let mut cursor = Cursor::new(indata);
|
||||
let mut metadata_iter = claxon::metadata::MetadataBlockReader::new(&mut cursor);
|
||||
let streaminfo = match metadata_iter.next() {
|
||||
Some(Ok(claxon::metadata::MetadataBlock::StreamInfo(info))) => info,
|
||||
_ => return Err("Failed to decode STREAMINFO"),
|
||||
};
|
||||
|
||||
assert_eq!(cursor.position(), indata.len() as u64);
|
||||
|
||||
Ok(streaminfo)
|
||||
}
|
||||
|
||||
fn gstaudioinfo(streaminfo: &claxon::metadata::StreamInfo) -> Result<gst_audio::AudioInfo, String> {
|
||||
let format = match streaminfo.bits_per_sample {
|
||||
8 => gst_audio::AudioFormat::S8,
|
||||
16 => gst_audio::AUDIO_FORMAT_S16,
|
||||
24 => gst_audio::AUDIO_FORMAT_S2432,
|
||||
32 => gst_audio::AUDIO_FORMAT_S32,
|
||||
_ => return Err("format not supported".to_string()),
|
||||
};
|
||||
|
||||
let index = match streaminfo.channels as usize {
|
||||
0 => return Err("no channels".to_string()),
|
||||
n if n > 8 => return Err("more than 8 channels, not supported yet".to_string()),
|
||||
n => n,
|
||||
};
|
||||
let to = &FLAC_CHANNEL_POSITIONS[index - 1][..index];
|
||||
let info_builder =
|
||||
gst_audio::AudioInfo::builder(format, streaminfo.sample_rate, streaminfo.channels)
|
||||
.positions(to);
|
||||
|
||||
let audio_info = info_builder
|
||||
.build()
|
||||
.map_err(|e| format!("failed to build audio info: {e}"))?;
|
||||
|
||||
Ok(audio_info)
|
||||
}
|
||||
|
||||
// http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-800004.3.9
|
||||
// http://flac.sourceforge.net/format.html#frame_header
|
||||
const FLAC_CHANNEL_POSITIONS: [[gst_audio::AudioChannelPosition; 8]; 8] = [
|
||||
[
|
||||
gst_audio::AudioChannelPosition::Mono,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
// FIXME: 7/8 channel layouts are not defined in the FLAC specs
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::SideLeft,
|
||||
gst_audio::AudioChannelPosition::SideRight,
|
||||
gst_audio::AudioChannelPosition::RearCenter,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::SideLeft,
|
||||
gst_audio::AudioChannelPosition::SideRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
],
|
||||
];
|
27
audio/claxon/src/claxondec/mod.rs
Normal file
27
audio/claxon/src/claxondec/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright (C) 2019 Ruben Gonzalez <rgonzalez@fluendo.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClaxonDec(ObjectSubclass<imp::ClaxonDec>) @extends gst_audio::AudioDecoder, gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"claxondec",
|
||||
gst::Rank::MARGINAL,
|
||||
ClaxonDec::static_type(),
|
||||
)
|
||||
}
|
35
audio/claxon/src/lib.rs
Normal file
35
audio/claxon/src/lib.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Copyright (C) 2019 Ruben Gonzalez <rgonzalez@fluendo.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
|
||||
|
||||
/**
|
||||
* plugin-claxon:
|
||||
*
|
||||
* Since: plugins-rs-0.6.0
|
||||
*/
|
||||
use gst::glib;
|
||||
|
||||
mod claxondec;
|
||||
|
||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
claxondec::register(plugin)
|
||||
}
|
||||
|
||||
gst::plugin_define!(
|
||||
claxon,
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
plugin_init,
|
||||
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||
"MIT/X11",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
env!("BUILD_REL_DATE")
|
||||
);
|
107
audio/claxon/tests/claxondec.rs
Normal file
107
audio/claxon/tests/claxondec.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright (C) 2019 Ruben Gonzalez <rgonzalez@fluendo.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstclaxon::plugin_register_static().expect("claxon test");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mono_s16() {
|
||||
let data = include_bytes!("test_mono_s16.flac");
|
||||
// 4 fLaC header, 38 streaminfo_header, 66 other header, 18 data
|
||||
let packet_sizes = [4, 38, 66, 18];
|
||||
let decoded_samples = [0usize, 0usize, 0usize, 2];
|
||||
|
||||
let caps = do_test(data, &packet_sizes, &decoded_samples);
|
||||
|
||||
assert_eq!(
|
||||
caps,
|
||||
gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_S16)
|
||||
.rate(44_100)
|
||||
.channels(1)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stereo_s32() {
|
||||
let data = include_bytes!("test_stereo_s32.flac");
|
||||
// 4 fLaC header, 38 streaminfo_header, 17465 data
|
||||
let packet_sizes = [4, 38, 17465];
|
||||
let decoded_samples = [0usize, 0usize, 8192];
|
||||
|
||||
let caps = do_test(data, &packet_sizes, &decoded_samples);
|
||||
|
||||
assert_eq!(
|
||||
caps,
|
||||
gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_S2432)
|
||||
.rate(44100)
|
||||
.channels(2)
|
||||
.channel_mask(0x3)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
fn do_test(data: &'static [u8], packet_sizes: &[usize], decoded_samples: &[usize]) -> gst::Caps {
|
||||
let packet_offsets = packet_sizes
|
||||
.iter()
|
||||
.scan(0, |state, &size| {
|
||||
*state += size;
|
||||
Some(*state)
|
||||
})
|
||||
.collect::<Vec<usize>>();
|
||||
|
||||
init();
|
||||
|
||||
let mut h = gst_check::Harness::new("claxondec");
|
||||
h.play();
|
||||
|
||||
let caps = gst::Caps::builder("audio/x-flac")
|
||||
.field("framed", true)
|
||||
.build();
|
||||
h.set_src_caps(caps);
|
||||
|
||||
let packet_offsets_iter = std::iter::once(&0).chain(packet_offsets.iter());
|
||||
let skip = 0;
|
||||
|
||||
for (offset_start, offset_end) in packet_offsets_iter
|
||||
.clone()
|
||||
.skip(skip)
|
||||
.zip(packet_offsets_iter.clone().skip(skip + 1))
|
||||
{
|
||||
let buffer = gst::Buffer::from_slice(&data[*offset_start..*offset_end]);
|
||||
h.push(buffer).unwrap();
|
||||
}
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
for samples in decoded_samples {
|
||||
if *samples == 0 {
|
||||
continue;
|
||||
}
|
||||
let buffer = h.pull().unwrap();
|
||||
assert_eq!(buffer.size(), 4 * samples);
|
||||
}
|
||||
|
||||
h.sinkpad()
|
||||
.expect("harness has no sinkpad")
|
||||
.current_caps()
|
||||
.expect("pad has no caps")
|
||||
}
|
BIN
audio/claxon/tests/test_mono_s16.flac
Normal file
BIN
audio/claxon/tests/test_mono_s16.flac
Normal file
Binary file not shown.
BIN
audio/claxon/tests/test_stereo_s32.flac
Normal file
BIN
audio/claxon/tests/test_stereo_s32.flac
Normal file
Binary file not shown.
51
audio/csound/Cargo.toml
Normal file
51
audio/csound/Cargo.toml
Normal file
|
@ -0,0 +1,51 @@
|
|||
[package]
|
||||
name = "gst-plugin-csound"
|
||||
version.workspace = true
|
||||
authors = ["Natanael Mojica <neithanmo@gmail.com>"]
|
||||
repository.workspace = true
|
||||
license = "MPL-2.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "GStreamer Audio Filter plugin based on Csound"
|
||||
|
||||
[dependencies]
|
||||
gst.workspace = true
|
||||
gst-base.workspace = true
|
||||
gst-audio.workspace = true
|
||||
csound = "0.1.8"
|
||||
byte-slice-cast = "1.0"
|
||||
once_cell.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gst-check.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "gstcsound"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "csound-effect"
|
||||
path = "examples/effect_example.rs"
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper.workspace = true
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
doc = ["gst/v1_18"]
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.9.21"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
import_library = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0, csound"
|
373
audio/csound/LICENSE-MPL-2.0
Normal file
373
audio/csound/LICENSE-MPL-2.0
Normal file
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
21
audio/csound/README.md
Normal file
21
audio/csound/README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# gst-plugin-csound
|
||||
|
||||
This is a [GStreamer](https://gstreamer.freedesktop.org/) plugin to interact
|
||||
with the [Csound](https://csound.com/) sound computing system.
|
||||
|
||||
Currently, there is only a filter element, called, csoundfilter. Two more elements a source and sink would be implemented
|
||||
later on.
|
||||
|
||||
For more information about dependencies and installation process, please refer to the [csound-rs](https://crates.io/crates/csound)
|
||||
documentation
|
||||
|
||||
## simple example
|
||||
The included example constructs the follow pipeline
|
||||
```
|
||||
$ gst-launch-1.0 \
|
||||
audiotestsrc ! \
|
||||
audioconvert ! \
|
||||
csoundfilter location=effect.csd ! \
|
||||
audioconvert ! \
|
||||
autoaudiosink
|
||||
```
|
3
audio/csound/build.rs
Normal file
3
audio/csound/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
131
audio/csound/examples/effect_example.rs
Normal file
131
audio/csound/examples/effect_example.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
// Copyright (C) 2020 Natanael Mojica <neithanmo@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
const AUDIO_SRC: &str = "audiotestsrc";
|
||||
const AUDIO_SINK: &str = "audioconvert ! autoaudiosink";
|
||||
|
||||
// This example defines two instruments, the first instrument send to the output that is at its input and accumulates the received audio samples
|
||||
// into a global variable called gasig. The execution of this instrument last the first 2 seconds.
|
||||
// The second instrument starts it execution at 1.8 second, This instrument creates two audio buffers with samples that are read
|
||||
// from the global accumulator(gasig), then reads these buffers at a fixed delay time, creating the adelL, adelM and adelR buffers,
|
||||
// also, It multiplies the audio samples in the right channel by 0.5 * kdel, being kdel a line of values starting at 0.5 at increments of 0.001.
|
||||
// Finally, those buffers are mixed with the accumulator, and an audio envelop is applied(aseg) to them.
|
||||
// The result is similar to an audio echo in which the buffered samples are read at different delay times and also modified in frequency(right channel),
|
||||
// this creates an space effect using just one channel audio input.
|
||||
const CSD: &str = "
|
||||
<CsoundSynthesizer>
|
||||
<CsOptions>
|
||||
</CsOptions>
|
||||
<CsInstruments>
|
||||
|
||||
sr = 44100
|
||||
ksmps = 7
|
||||
|
||||
nchnls_i = 1
|
||||
nchnls = 2
|
||||
|
||||
gasig init 0
|
||||
gidel = 1
|
||||
|
||||
instr 1
|
||||
|
||||
ain in
|
||||
outs ain, ain
|
||||
|
||||
vincr gasig, ain
|
||||
endin
|
||||
|
||||
instr 2
|
||||
|
||||
ifeedback = p4
|
||||
|
||||
aseg linseg 1., p3, 0.0
|
||||
|
||||
abuf2 delayr gidel
|
||||
adelL deltap .4
|
||||
adelM deltap .5
|
||||
delayw gasig + (adelL * ifeedback)
|
||||
|
||||
abuf3 delayr gidel
|
||||
kdel line .5, p3, .001
|
||||
adelR deltap .5 * kdel
|
||||
delayw gasig + (adelR * ifeedback)
|
||||
outs (adelL + adelM) * aseg, (adelR + adelM) * aseg
|
||||
clear gasig
|
||||
endin
|
||||
|
||||
</CsInstruments>
|
||||
<CsScore>
|
||||
|
||||
i 1 0 2
|
||||
i 2 1.8 5 .8
|
||||
e
|
||||
</CsScore>
|
||||
</CsoundSynthesizer>";
|
||||
|
||||
fn create_pipeline() -> Result<gst::Pipeline, Box<dyn Error>> {
|
||||
let pipeline = gst::Pipeline::default();
|
||||
|
||||
let audio_src = gst::parse::bin_from_description(AUDIO_SRC, true)?.upcast();
|
||||
|
||||
let audio_sink = gst::parse::bin_from_description(AUDIO_SINK, true)?.upcast();
|
||||
|
||||
let csoundfilter = gst::ElementFactory::make("csoundfilter")
|
||||
.property("csd-text", CSD)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
pipeline.add_many([&audio_src, &csoundfilter, &audio_sink])?;
|
||||
|
||||
audio_src.link_pads(Some("src"), &csoundfilter, Some("sink"))?;
|
||||
csoundfilter.link_pads(Some("src"), &audio_sink, Some("sink"))?;
|
||||
|
||||
Ok(pipeline)
|
||||
}
|
||||
|
||||
fn main_loop(pipeline: gst::Pipeline) -> Result<(), Box<dyn Error>> {
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
let bus = pipeline
|
||||
.bus()
|
||||
.expect("Pipeline without bus. Shouldn't happen!");
|
||||
|
||||
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||
use gst::MessageView;
|
||||
|
||||
match msg.view() {
|
||||
MessageView::Eos(..) => break,
|
||||
MessageView::Error(err) => {
|
||||
println!(
|
||||
"Error from {:?}: {} ({:?})",
|
||||
msg.src().map(|s| s.path_string()),
|
||||
err.error(),
|
||||
err.debug()
|
||||
);
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pipeline.set_state(gst::State::Null)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
gst::init().unwrap();
|
||||
|
||||
gstcsound::plugin_register_static().expect("Failed to register csound plugin");
|
||||
|
||||
create_pipeline().and_then(main_loop)
|
||||
}
|
633
audio/csound/src/filter/imp.rs
Normal file
633
audio/csound/src/filter/imp.rs
Normal file
|
@ -0,0 +1,633 @@
|
|||
// Copyright (C) 2020 Natanael Mojica <neithanmo@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst::{element_imp_error, error_msg, loggable_error};
|
||||
use gst_base::prelude::*;
|
||||
use gst_base::subclass::base_transform::GenerateOutputSuccess;
|
||||
use gst_base::subclass::prelude::*;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
use csound::{Csound, MessageType};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"csoundfilter",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("Audio Filter based on Csound"),
|
||||
)
|
||||
});
|
||||
|
||||
const SCORE_OFFSET_DEFAULT: f64 = 0f64;
|
||||
const DEFAULT_LOOP: bool = false;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Settings {
|
||||
pub loop_: bool,
|
||||
pub location: Option<String>,
|
||||
pub csd_text: Option<String>,
|
||||
pub offset: f64,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
loop_: DEFAULT_LOOP,
|
||||
location: None,
|
||||
csd_text: None,
|
||||
offset: SCORE_OFFSET_DEFAULT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
in_info: gst_audio::AudioInfo,
|
||||
out_info: gst_audio::AudioInfo,
|
||||
adapter: gst_base::UniqueAdapter,
|
||||
ksmps: u32,
|
||||
}
|
||||
|
||||
pub struct CsoundFilter {
|
||||
settings: Mutex<Settings>,
|
||||
state: Mutex<Option<State>>,
|
||||
csound: Mutex<Csound>,
|
||||
compiled: AtomicBool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
// Considering an input of size: input_size and the user's ksmps,
|
||||
// calculates the equivalent output_size
|
||||
fn max_output_size(&self, input_size: usize) -> usize {
|
||||
let in_samples = input_size / self.in_info.bpf() as usize;
|
||||
let in_process_samples = in_samples - (in_samples % self.ksmps as usize);
|
||||
in_process_samples * self.out_info.bpf() as usize
|
||||
}
|
||||
|
||||
fn bytes_to_read(&mut self, output_size: usize) -> usize {
|
||||
// The max amount of bytes at the input that We would need
|
||||
// for filling an output buffer of size *output_size*
|
||||
(output_size / self.out_info.bpf() as usize) * self.in_info.bpf() as usize
|
||||
}
|
||||
|
||||
// returns the spin capacity in bytes
|
||||
fn spin_capacity(&self) -> usize {
|
||||
(self.ksmps * self.in_info.bpf()) as _
|
||||
}
|
||||
|
||||
fn needs_more_data(&self) -> bool {
|
||||
self.adapter.available() < self.spin_capacity()
|
||||
}
|
||||
|
||||
fn samples_to_time(&self, samples: u64) -> Option<gst::ClockTime> {
|
||||
samples
|
||||
.mul_div_round(*gst::ClockTime::SECOND, self.in_info.rate() as u64)
|
||||
.map(gst::ClockTime::from_nseconds)
|
||||
}
|
||||
|
||||
fn current_pts(&self) -> Option<gst::ClockTime> {
|
||||
// get the last seen pts and the amount of bytes
|
||||
// since then
|
||||
let (prev_pts, distance) = self.adapter.prev_pts();
|
||||
|
||||
// Use the distance to get the amount of samples
|
||||
// and with it calculate the time-offset which
|
||||
// can be added to the prev_pts to get the
|
||||
// pts at the beginning of the adapter.
|
||||
let samples = distance / self.in_info.bpf() as u64;
|
||||
prev_pts
|
||||
.opt_checked_add(self.samples_to_time(samples))
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn buffer_duration(&self, buffer_size: u64) -> Option<gst::ClockTime> {
|
||||
let samples = buffer_size / self.out_info.bpf() as u64;
|
||||
self.samples_to_time(samples)
|
||||
}
|
||||
}
|
||||
|
||||
impl CsoundFilter {
|
||||
fn process(&self, csound: &mut Csound, idata: &[f64], odata: &mut [f64]) -> bool {
|
||||
let spin = csound.get_spin().unwrap();
|
||||
let spout = csound.get_spout().unwrap();
|
||||
|
||||
let in_chunks = idata.chunks_exact(spin.len());
|
||||
let out_chunks = odata.chunks_exact_mut(spout.len());
|
||||
let mut end_score = false;
|
||||
for (ichunk, ochunk) in in_chunks.zip(out_chunks) {
|
||||
spin.copy_from_slice(ichunk);
|
||||
end_score = csound.perform_ksmps();
|
||||
spout.copy_to_slice(ochunk);
|
||||
}
|
||||
|
||||
end_score
|
||||
}
|
||||
|
||||
fn compile_score(&self) -> std::result::Result<(), gst::ErrorMessage> {
|
||||
let csound = self.csound.lock().unwrap();
|
||||
let settings = self.settings.lock().unwrap();
|
||||
if let Some(ref location) = settings.location {
|
||||
csound
|
||||
.compile_csd(location)
|
||||
.map_err(|e| error_msg!(gst::LibraryError::Failed, ["{e}"]))?;
|
||||
} else if let Some(ref text) = settings.csd_text {
|
||||
csound
|
||||
.compile_csd_text(text)
|
||||
.map_err(|e| error_msg!(gst::LibraryError::Failed, ["{e}"]))?;
|
||||
} else {
|
||||
return Err(error_msg!(
|
||||
gst::LibraryError::Failed,
|
||||
["No Csound score specified to compile. Use either location or csd-text but not both"]
|
||||
));
|
||||
}
|
||||
|
||||
self.compiled.store(true, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn message_callback(msg_type: MessageType, msg: &str) {
|
||||
match msg_type {
|
||||
MessageType::CSOUNDMSG_ERROR => gst::error!(CAT, "{}", msg),
|
||||
MessageType::CSOUNDMSG_WARNING => gst::warning!(CAT, "{}", msg),
|
||||
MessageType::CSOUNDMSG_ORCH => gst::info!(CAT, "{}", msg),
|
||||
MessageType::CSOUNDMSG_REALTIME => gst::log!(CAT, "{}", msg),
|
||||
MessageType::CSOUNDMSG_DEFAULT => gst::log!(CAT, "{}", msg),
|
||||
MessageType::CSOUNDMSG_STDOUT => gst::log!(CAT, "{}", msg),
|
||||
}
|
||||
}
|
||||
|
||||
fn drain(&self) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let csound = self.csound.lock().unwrap();
|
||||
let mut state_lock = self.state.lock().unwrap();
|
||||
let state = state_lock.as_mut().unwrap();
|
||||
|
||||
let avail = state.adapter.available();
|
||||
|
||||
// Complete processing blocks should have been processed in the transform call
|
||||
assert!(avail < state.spin_capacity());
|
||||
|
||||
if avail == 0 {
|
||||
return Ok(gst::FlowSuccess::Ok);
|
||||
}
|
||||
|
||||
let mut spin = csound.get_spin().unwrap();
|
||||
let spout = csound.get_spout().unwrap();
|
||||
|
||||
let out_bytes =
|
||||
(avail / state.in_info.channels() as usize) * state.out_info.channels() as usize;
|
||||
|
||||
let mut buffer = gst::Buffer::with_size(out_bytes).map_err(|e| {
|
||||
gst::error!(CAT, imp: self, "Failed to allocate buffer at EOS {:?}", e);
|
||||
gst::FlowError::Flushing
|
||||
})?;
|
||||
|
||||
let buffer_mut = buffer.get_mut().ok_or(gst::FlowError::NotSupported)?;
|
||||
|
||||
let pts = state.current_pts();
|
||||
let duration = state.buffer_duration(out_bytes as _);
|
||||
|
||||
buffer_mut.set_pts(pts);
|
||||
buffer_mut.set_duration(duration);
|
||||
|
||||
let adapter_map = state.adapter.map(avail).unwrap();
|
||||
let data = adapter_map
|
||||
.as_ref()
|
||||
.as_slice_of::<f64>()
|
||||
.map_err(|_| gst::FlowError::NotSupported)?;
|
||||
|
||||
let mut omap = buffer_mut
|
||||
.map_writable()
|
||||
.map_err(|_| gst::FlowError::NotSupported)?;
|
||||
let odata = omap
|
||||
.as_mut_slice_of::<f64>()
|
||||
.map_err(|_| gst::FlowError::NotSupported)?;
|
||||
|
||||
spin.clear();
|
||||
spin.copy_from_slice(data);
|
||||
csound.perform_ksmps();
|
||||
spout.copy_to_slice(odata);
|
||||
|
||||
drop(adapter_map);
|
||||
drop(omap);
|
||||
|
||||
state.adapter.flush(avail);
|
||||
// Drop the locks before pushing buffers into the srcpad
|
||||
drop(state_lock);
|
||||
drop(csound);
|
||||
|
||||
self.obj().src_pad().push(buffer)
|
||||
}
|
||||
|
||||
fn generate_output(&self, state: &mut State) -> Result<GenerateOutputSuccess, gst::FlowError> {
|
||||
let output_size = state.max_output_size(state.adapter.available());
|
||||
|
||||
let mut output = gst::Buffer::with_size(output_size).map_err(|_| gst::FlowError::Error)?;
|
||||
let outbuf = output.get_mut().ok_or(gst::FlowError::Error)?;
|
||||
|
||||
let pts = state.current_pts();
|
||||
let duration = state.buffer_duration(output_size as _);
|
||||
|
||||
outbuf.set_pts(pts);
|
||||
outbuf.set_duration(duration);
|
||||
|
||||
gst::log!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Generating output at: {} - duration: {}",
|
||||
pts.display(),
|
||||
duration.display(),
|
||||
);
|
||||
|
||||
// Get the required amount of bytes to be read from
|
||||
// the adapter to fill an output buffer of size output_size
|
||||
let bytes_to_read = state.bytes_to_read(output_size);
|
||||
|
||||
let indata = state
|
||||
.adapter
|
||||
.map(bytes_to_read)
|
||||
.map_err(|_| gst::FlowError::Error)?;
|
||||
let idata = indata
|
||||
.as_ref()
|
||||
.as_slice_of::<f64>()
|
||||
.map_err(|_| gst::FlowError::Error)?;
|
||||
|
||||
let mut omap = outbuf.map_writable().map_err(|_| gst::FlowError::Error)?;
|
||||
let odata = omap
|
||||
.as_mut_slice_of::<f64>()
|
||||
.map_err(|_| gst::FlowError::Error)?;
|
||||
|
||||
let mut csound = self.csound.lock().unwrap();
|
||||
let end_score = self.process(&mut csound, idata, odata);
|
||||
|
||||
drop(indata);
|
||||
drop(omap);
|
||||
state.adapter.flush(bytes_to_read);
|
||||
|
||||
if end_score {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
if settings.loop_ {
|
||||
csound.set_score_offset_seconds(settings.offset);
|
||||
csound.rewind_score();
|
||||
} else {
|
||||
// clear the adapter here because our eos event handler
|
||||
// will try to flush it calling csound.perform()
|
||||
// which does not make sense since
|
||||
// the end of score has been reached.
|
||||
state.adapter.clear();
|
||||
return Err(gst::FlowError::Eos);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GenerateOutputSuccess::Buffer(output))
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for CsoundFilter {
|
||||
const NAME: &'static str = "GstCsoundFilter";
|
||||
type Type = super::CsoundFilter;
|
||||
type ParentType = gst_base::BaseTransform;
|
||||
|
||||
fn new() -> Self {
|
||||
let csound = Csound::new();
|
||||
// create the csound instance and configure
|
||||
csound.message_string_callback(Self::message_callback);
|
||||
// Disable all default handling of sound I/O by csound internal library
|
||||
// by giving to it a hardware buffer size of zero, and setting a state,
|
||||
// higher than zero.
|
||||
csound.set_host_implemented_audioIO(1, 0);
|
||||
// We don't want csound to write samples to our HW
|
||||
csound.set_option("--nosound").unwrap();
|
||||
Self {
|
||||
settings: Mutex::new(Default::default()),
|
||||
state: Mutex::new(None),
|
||||
csound: Mutex::new(csound),
|
||||
compiled: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for CsoundFilter {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecBoolean::builder("loop")
|
||||
.nick("Loop")
|
||||
.blurb("loop over the score (can be changed in PLAYING or PAUSED state)")
|
||||
.default_value(DEFAULT_LOOP)
|
||||
.mutable_playing()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("location")
|
||||
.nick("Location")
|
||||
.blurb("Location of the csd file to be used by csound.
|
||||
Use either location or CSD-text but not both at the same time, if so and error would be triggered")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("csd-text")
|
||||
.nick("CSD-text")
|
||||
.blurb("The content of a csd file passed as a String.
|
||||
Use either location or csd-text but not both at the same time, if so and error would be triggered")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecDouble::builder("score-offset")
|
||||
.nick("Score Offset")
|
||||
.blurb("Score offset in seconds to start the performance")
|
||||
.minimum(0.0)
|
||||
.default_value(SCORE_OFFSET_DEFAULT)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"loop" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.loop_ = value.get().expect("type checked upstream");
|
||||
}
|
||||
"location" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
if self.state.lock().unwrap().is_none() {
|
||||
settings.location = match value.get::<Option<String>>() {
|
||||
Ok(location) => location,
|
||||
_ => unreachable!("type checked upstream"),
|
||||
};
|
||||
}
|
||||
}
|
||||
"csd-text" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
if self.state.lock().unwrap().is_none() {
|
||||
settings.csd_text = match value.get::<Option<String>>() {
|
||||
Ok(text) => text,
|
||||
_ => unreachable!("type checked upstream"),
|
||||
};
|
||||
}
|
||||
}
|
||||
"score_offset" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.offset = value.get().expect("type checked upstream");
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"loop" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.loop_.to_value()
|
||||
}
|
||||
"location" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.location.to_value()
|
||||
}
|
||||
"csd-text" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.csd_text.to_value()
|
||||
}
|
||||
"score-offset" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.offset.to_value()
|
||||
}
|
||||
name => panic!("No getter for {name}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for CsoundFilter {}
|
||||
|
||||
impl ElementImpl for CsoundFilter {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Audio filter",
|
||||
"Filter/Effect/Audio",
|
||||
"Implement an audio filter/effects using Csound",
|
||||
"Natanael Mojica <neithanmo@gmail.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F64)
|
||||
.build();
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template, sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseTransformImpl for CsoundFilter {
|
||||
const MODE: gst_base::subclass::BaseTransformMode =
|
||||
gst_base::subclass::BaseTransformMode::NeverInPlace;
|
||||
const PASSTHROUGH_ON_SAME_CAPS: bool = false;
|
||||
const TRANSFORM_IP_ON_PASSTHROUGH: bool = false;
|
||||
|
||||
fn start(&self) -> std::result::Result<(), gst::ErrorMessage> {
|
||||
self.compile_score()?;
|
||||
|
||||
let csound = self.csound.lock().unwrap();
|
||||
let settings = self.settings.lock().unwrap();
|
||||
csound.set_score_offset_seconds(settings.offset);
|
||||
|
||||
if let Err(e) = csound.start() {
|
||||
return Err(error_msg!(gst::LibraryError::Failed, ["{e}"]));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
let csound = self.csound.lock().unwrap();
|
||||
csound.stop();
|
||||
csound.reset();
|
||||
let _ = self.state.lock().unwrap().take();
|
||||
|
||||
gst::info!(CAT, imp: self, "Stopped");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sink_event(&self, event: gst::Event) -> bool {
|
||||
use gst::EventView;
|
||||
|
||||
if let EventView::Eos(_) = event.view() {
|
||||
gst::log!(CAT, imp: self, "Handling Eos");
|
||||
if self.drain().is_err() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.parent_sink_event(event)
|
||||
}
|
||||
|
||||
fn transform_caps(
|
||||
&self,
|
||||
direction: gst::PadDirection,
|
||||
caps: &gst::Caps,
|
||||
filter: Option<&gst::Caps>,
|
||||
) -> Option<gst::Caps> {
|
||||
let compiled = self.compiled.load(Ordering::SeqCst);
|
||||
|
||||
let mut other_caps = {
|
||||
// Our caps proposal
|
||||
let mut new_caps = caps.clone();
|
||||
if compiled {
|
||||
let csound = self.csound.lock().unwrap();
|
||||
// Use the sample rate and channels configured in the csound score
|
||||
let sr = csound.get_sample_rate() as i32;
|
||||
let ichannels = csound.input_channels() as i32;
|
||||
let ochannels = csound.output_channels() as i32;
|
||||
for s in new_caps.make_mut().iter_mut() {
|
||||
s.set("format", gst_audio::AUDIO_FORMAT_F64.to_str());
|
||||
s.set("rate", sr);
|
||||
|
||||
// replace the channel property with our values,
|
||||
// if they are not supported, the negotiation will fail.
|
||||
if direction == gst::PadDirection::Src {
|
||||
s.set("channels", ichannels);
|
||||
} else {
|
||||
s.set("channels", ochannels);
|
||||
}
|
||||
// Csound does not have a concept of channel-mask
|
||||
s.remove_field("channel-mask");
|
||||
}
|
||||
}
|
||||
new_caps
|
||||
};
|
||||
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Transformed caps from {} to {} in direction {:?}",
|
||||
caps,
|
||||
other_caps,
|
||||
direction
|
||||
);
|
||||
|
||||
if let Some(filter) = filter {
|
||||
other_caps = filter.intersect_with_mode(&other_caps, gst::CapsIntersectMode::First);
|
||||
}
|
||||
|
||||
Some(other_caps)
|
||||
}
|
||||
|
||||
fn set_caps(&self, incaps: &gst::Caps, outcaps: &gst::Caps) -> Result<(), gst::LoggableError> {
|
||||
// Flush previous state
|
||||
if self.state.lock().unwrap().is_some() {
|
||||
self.drain()
|
||||
.map_err(|e| loggable_error!(CAT, "Error flushing previous state data {:?}", e))?;
|
||||
}
|
||||
|
||||
let in_info = gst_audio::AudioInfo::from_caps(incaps)
|
||||
.map_err(|_| loggable_error!(CAT, "Failed to parse input caps"))?;
|
||||
let out_info = gst_audio::AudioInfo::from_caps(outcaps)
|
||||
.map_err(|_| loggable_error!(CAT, "Failed to parse output caps"))?;
|
||||
|
||||
let csound = self.csound.lock().unwrap();
|
||||
|
||||
let ichannels = in_info.channels();
|
||||
let ochannels = out_info.channels();
|
||||
let rate = in_info.rate();
|
||||
|
||||
// Check if the negotiated caps are the right ones
|
||||
if rate != out_info.rate() || rate != csound.get_sample_rate() as u32 {
|
||||
return Err(loggable_error!(
|
||||
CAT,
|
||||
"Failed to negotiate caps: invalid sample rate {}",
|
||||
rate
|
||||
));
|
||||
} else if ichannels != csound.input_channels() {
|
||||
return Err(loggable_error!(
|
||||
CAT,
|
||||
"Failed to negotiate caps: input channels {} not supported",
|
||||
ichannels
|
||||
));
|
||||
} else if ochannels != csound.output_channels() {
|
||||
return Err(loggable_error!(
|
||||
CAT,
|
||||
"Failed to negotiate caps: output channels {} not supported",
|
||||
ochannels
|
||||
));
|
||||
}
|
||||
|
||||
let ksmps = csound.get_ksmps();
|
||||
|
||||
let adapter = gst_base::UniqueAdapter::new();
|
||||
|
||||
let mut state_lock = self.state.lock().unwrap();
|
||||
*state_lock = Some(State {
|
||||
in_info,
|
||||
out_info,
|
||||
adapter,
|
||||
ksmps,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_output(&self) -> Result<GenerateOutputSuccess, gst::FlowError> {
|
||||
// Check if there are enough data in the queued buffer and adapter,
|
||||
// if it is not the case, just notify the parent class to not generate
|
||||
// an output
|
||||
if let Some(buffer) = self.take_queued_buffer() {
|
||||
if buffer.flags().contains(gst::BufferFlags::DISCONT) {
|
||||
self.drain()?;
|
||||
}
|
||||
|
||||
let mut state_guard = self.state.lock().unwrap();
|
||||
let state = state_guard.as_mut().ok_or_else(|| {
|
||||
element_imp_error!(
|
||||
self,
|
||||
gst::CoreError::Negotiation,
|
||||
["Can not generate an output without State"]
|
||||
);
|
||||
gst::FlowError::NotNegotiated
|
||||
})?;
|
||||
|
||||
state.adapter.push(buffer);
|
||||
if !state.needs_more_data() {
|
||||
return self.generate_output(state);
|
||||
}
|
||||
}
|
||||
gst::log!(CAT, "No enough data to generate output");
|
||||
Ok(GenerateOutputSuccess::NoOutput)
|
||||
}
|
||||
}
|
25
audio/csound/src/filter/mod.rs
Normal file
25
audio/csound/src/filter/mod.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright (C) 2020 Natanael Mojica <neithanmo@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct CsoundFilter(ObjectSubclass<imp::CsoundFilter>) @extends gst_base::BaseTransform, gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"csoundfilter",
|
||||
gst::Rank::NONE,
|
||||
CsoundFilter::static_type(),
|
||||
)
|
||||
}
|
34
audio/csound/src/lib.rs
Normal file
34
audio/csound/src/lib.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2020 Natanael Mojica <neithanmo@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
|
||||
|
||||
/**
|
||||
* plugin-csound:
|
||||
*
|
||||
* Since: plugins-rs-0.6.0
|
||||
*/
|
||||
use gst::glib;
|
||||
|
||||
mod filter;
|
||||
|
||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
filter::register(plugin)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
gst::plugin_define!(
|
||||
csound,
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
plugin_init,
|
||||
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||
"MIT/X11",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
env!("BUILD_REL_DATE")
|
||||
);
|
381
audio/csound/tests/csound_filter.rs
Normal file
381
audio/csound/tests/csound_filter.rs
Normal file
|
@ -0,0 +1,381 @@
|
|||
// Copyright (C) 2020 Natanael Mojica <neithanmo@gmail.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
// This macro allows us to create a kind of dynamic CSD file,
|
||||
// we need to pass in the ksmps, channels and input/output
|
||||
// operations that are going to be done by Csound over input and output
|
||||
// audio samples
|
||||
macro_rules! CSD {
|
||||
($ksmps:expr, $ichannels:expr, $ochannels:expr, $ins:expr, $out:expr) => {
|
||||
format!(
|
||||
"
|
||||
<CsoundSynthesizer>
|
||||
<CsOptions>
|
||||
</CsOptions>
|
||||
<CsInstruments>
|
||||
sr = 44100 ; default sample rate
|
||||
ksmps = {}
|
||||
nchnls_i = {}
|
||||
nchnls = {}
|
||||
0dbfs = 1
|
||||
|
||||
instr 1
|
||||
|
||||
{} ;input
|
||||
{} ; csound output
|
||||
|
||||
endin
|
||||
</CsInstruments>
|
||||
<CsScore>
|
||||
i 1 0 2
|
||||
e
|
||||
</CsScore>
|
||||
</CsoundSynthesizer>",
|
||||
$ksmps, $ichannels, $ochannels, $ins, $out
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstcsound::plugin_register_static().expect("Failed to register csound plugin");
|
||||
});
|
||||
}
|
||||
|
||||
fn build_harness(src_caps: gst::Caps, sink_caps: gst::Caps, csd: &str) -> gst_check::Harness {
|
||||
let filter = gst::ElementFactory::make("csoundfilter")
|
||||
.property("csd-text", csd)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut h = gst_check::Harness::with_element(&filter, Some("sink"), Some("src"));
|
||||
|
||||
h.set_caps(src_caps, sink_caps);
|
||||
h
|
||||
}
|
||||
|
||||
fn duration_from_samples(num_samples: u64, rate: u64) -> Option<gst::ClockTime> {
|
||||
num_samples
|
||||
.mul_div_round(*gst::ClockTime::SECOND, rate)
|
||||
.map(gst::ClockTime::from_nseconds)
|
||||
}
|
||||
|
||||
// This test verifies the well functioning of the EOS logic,
|
||||
// we generate EOS_NUM_BUFFERS=10 buffers with EOS_NUM_SAMPLES=62 samples each one,
|
||||
// for a total of 10 * 62 = 620 samples, but 620%32(ksmps)= 12 will be leftover and should be processed when
|
||||
// the eos event is received, which generates another buffer, so that, the total amount of buffers that
|
||||
// the harness would have at its sinkpad should be EOS_NUM_BUFFERS + 1, being the total amount of processed samples
|
||||
// equals to EOS_NUM_BUFFERS * EOS_NUM_SAMPLES = 620 samples.It is important to mention that the created buffers have silenced samples(being 0),
|
||||
// but csoundfilter would add 1.0 to each incoming sample.
|
||||
// at the end, all of the output samples should have a value of 1.0.
|
||||
const EOS_NUM_BUFFERS: usize = 10;
|
||||
const EOS_NUM_SAMPLES: usize = 62;
|
||||
#[test]
|
||||
fn csound_filter_eos() {
|
||||
init();
|
||||
|
||||
// Sets the ksmps to 32,
|
||||
// input = output channels = 1
|
||||
let ksmps: usize = 32;
|
||||
let num_channels = 1;
|
||||
let sr: i32 = 44_100;
|
||||
|
||||
let caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F64)
|
||||
.rate(sr)
|
||||
.channels(num_channels)
|
||||
.build();
|
||||
|
||||
let mut h = build_harness(
|
||||
caps.clone(),
|
||||
caps,
|
||||
// this score instructs Csound to add 1.0 to each input sample
|
||||
&CSD!(ksmps, num_channels, num_channels, "ain in", "out ain + 1.0"),
|
||||
);
|
||||
h.play();
|
||||
|
||||
// The input buffer pts and duration
|
||||
let mut in_pts = gst::ClockTime::ZERO;
|
||||
let in_duration = duration_from_samples(EOS_NUM_SAMPLES as _, sr as _)
|
||||
.expect("duration defined because sr is > 0");
|
||||
// The number of samples that were leftover during the previous iteration
|
||||
let mut samples_offset = 0;
|
||||
// Output samples and buffers counters
|
||||
let mut num_samples: usize = 0;
|
||||
let mut num_buffers = 0;
|
||||
// The expected pts of output buffers
|
||||
let mut expected_pts = gst::ClockTime::ZERO;
|
||||
|
||||
for _ in 0..EOS_NUM_BUFFERS {
|
||||
let mut buffer =
|
||||
gst::Buffer::with_size(EOS_NUM_SAMPLES * std::mem::size_of::<f64>()).unwrap();
|
||||
|
||||
buffer.make_mut().set_pts(in_pts);
|
||||
buffer.make_mut().set_duration(in_duration);
|
||||
|
||||
let in_samples = samples_offset + EOS_NUM_SAMPLES as u64;
|
||||
// Gets amount of samples that are going to be processed,
|
||||
// the output buffer must be in_process_samples length
|
||||
let in_process_samples = in_samples - (in_samples % ksmps as u64);
|
||||
|
||||
// Push an input buffer and pull the result of processing it
|
||||
let buffer = h.push_and_pull(buffer);
|
||||
assert!(buffer.is_ok());
|
||||
|
||||
let buffer = buffer.unwrap();
|
||||
|
||||
// Checks output buffer timestamp and duration
|
||||
assert_eq!(
|
||||
buffer.as_ref().duration(),
|
||||
duration_from_samples(in_process_samples, sr as _)
|
||||
);
|
||||
assert_eq!(buffer.as_ref().pts(), Some(expected_pts));
|
||||
|
||||
// Get the number of samples that were not processed
|
||||
samples_offset = in_samples % ksmps as u64;
|
||||
// Calculates the next output buffer timestamp
|
||||
expected_pts = in_pts
|
||||
+ duration_from_samples(EOS_NUM_SAMPLES as u64 - samples_offset, sr as _)
|
||||
.expect("duration defined because sr is > 0");
|
||||
// Calculates the next input buffer timestamp
|
||||
in_pts += in_duration;
|
||||
|
||||
let map = buffer.into_mapped_buffer_readable().unwrap();
|
||||
let output = map.as_slice().as_slice_of::<f64>().unwrap();
|
||||
|
||||
// all samples in the output buffers must value 1
|
||||
assert!(output.iter().all(|sample| *sample as u16 == 1u16));
|
||||
|
||||
num_samples += output.len();
|
||||
num_buffers += 1;
|
||||
}
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
// pull the buffer produced after the EOS event
|
||||
let buffer = h.pull().unwrap();
|
||||
|
||||
let samples_at_eos = (EOS_NUM_BUFFERS * EOS_NUM_SAMPLES) % ksmps;
|
||||
assert_eq!(
|
||||
buffer.as_ref().pts(),
|
||||
in_pts.opt_sub(duration_from_samples(samples_at_eos as _, sr as _))
|
||||
);
|
||||
|
||||
let map = buffer.into_mapped_buffer_readable().unwrap();
|
||||
let output = map.as_slice().as_slice_of::<f64>().unwrap();
|
||||
num_samples += output.len();
|
||||
num_buffers += 1;
|
||||
|
||||
assert_eq!(output.len(), samples_at_eos);
|
||||
assert!(output.iter().all(|sample| *sample as u16 == 1u16));
|
||||
|
||||
// All the generated samples should have been processed at this point
|
||||
assert_eq!(num_samples, EOS_NUM_SAMPLES * EOS_NUM_BUFFERS);
|
||||
assert_eq!(num_buffers, EOS_NUM_BUFFERS + 1);
|
||||
}
|
||||
|
||||
// In this test, we generate UNDERFLOW_NUM_BUFFERS buffers with UNDERFLOW_NUM_SAMPLES samples each one, however,
|
||||
// Csound is waiting for UNDERFLOW_NUM_SAMPLES * 2 samples per buffer at its input, so that,
|
||||
// internally, the output will be only generated when enough data is available.
|
||||
// It happens, after every 2 * UNDERFLOW_NUM_BUFFERS input buffers, after processing, we should have UNDERFLOW_NUM_BUFFERS/2
|
||||
// output buffers containing UNDERFLOW_NUM_SAMPLES * 2 samples.
|
||||
const UNDERFLOW_NUM_BUFFERS: usize = 200;
|
||||
const UNDERFLOW_NUM_SAMPLES: usize = 2;
|
||||
#[test]
|
||||
fn csound_filter_underflow() {
|
||||
init();
|
||||
|
||||
let ksmps: usize = UNDERFLOW_NUM_SAMPLES * 2;
|
||||
let num_channels = 1;
|
||||
let sr: i32 = 44_100;
|
||||
|
||||
let caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F64)
|
||||
.rate(sr)
|
||||
.channels(num_channels)
|
||||
.build();
|
||||
|
||||
let mut h = build_harness(
|
||||
caps.clone(),
|
||||
caps,
|
||||
&CSD!(ksmps, num_channels, num_channels, "ain in", "out ain"),
|
||||
);
|
||||
h.play();
|
||||
|
||||
// Input buffers timestamp
|
||||
let mut in_pts = gst::ClockTime::ZERO;
|
||||
let in_samples_duration = duration_from_samples(UNDERFLOW_NUM_SAMPLES as _, sr as _)
|
||||
.expect("duration defined because sr is > 0");
|
||||
|
||||
for _ in 0..UNDERFLOW_NUM_BUFFERS {
|
||||
let mut buffer =
|
||||
gst::Buffer::with_size(UNDERFLOW_NUM_SAMPLES * std::mem::size_of::<f64>()).unwrap();
|
||||
|
||||
buffer.make_mut().set_pts(in_pts);
|
||||
buffer.make_mut().set_duration(in_samples_duration);
|
||||
|
||||
in_pts += in_samples_duration;
|
||||
|
||||
assert!(h.push(buffer).is_ok());
|
||||
}
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
// From here we check our output data
|
||||
let mut num_buffers = 0;
|
||||
let mut num_samples = 0;
|
||||
|
||||
let expected_duration = duration_from_samples(UNDERFLOW_NUM_SAMPLES as u64 * 2, sr as _)
|
||||
.expect("duration defined because sr is > 0");
|
||||
let expected_buffers = UNDERFLOW_NUM_BUFFERS / 2;
|
||||
let mut expected_pts = gst::ClockTime::ZERO;
|
||||
|
||||
for _ in 0..expected_buffers {
|
||||
let buffer = h.pull().unwrap();
|
||||
let samples = buffer.size() / std::mem::size_of::<f64>();
|
||||
|
||||
assert_eq!(buffer.as_ref().pts(), Some(expected_pts));
|
||||
assert_eq!(buffer.as_ref().duration(), Some(expected_duration));
|
||||
assert_eq!(samples, UNDERFLOW_NUM_SAMPLES * 2);
|
||||
// Output data is produced after 2 input buffers
|
||||
// so that, the next output buffer's PTS should be
|
||||
// equal to the last PTS plus the duration of 2 input buffers
|
||||
expected_pts += 2 * in_samples_duration;
|
||||
|
||||
num_buffers += 1;
|
||||
num_samples += samples;
|
||||
}
|
||||
|
||||
assert_eq!(num_buffers, UNDERFLOW_NUM_BUFFERS / 2);
|
||||
assert_eq!(num_samples, UNDERFLOW_NUM_SAMPLES * UNDERFLOW_NUM_BUFFERS);
|
||||
}
|
||||
|
||||
// Verifies that the caps negotiation is properly done, by pushing buffers whose caps
|
||||
// are the same as the one configured in csound, into the harness sink pad. Csoundfilter is expecting 2 channels audio
|
||||
// at a sample rate of 44100.
|
||||
// the output caps configured in the harness are not fixated but when the caps negotiation ends,
|
||||
// those caps must be fixated according to the csound output format which is defined once the csd file is compiled
|
||||
#[test]
|
||||
fn csound_filter_caps_negotiation() {
|
||||
init();
|
||||
|
||||
let ksmps = 4;
|
||||
let ichannels = 2;
|
||||
let ochannels = 1;
|
||||
let sr: i32 = 44_100;
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F64)
|
||||
.rate(sr)
|
||||
.channels(ichannels)
|
||||
.build();
|
||||
|
||||
// Define the output caps which would be fixated
|
||||
// at the end of the caps negotiation
|
||||
let sink_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F64)
|
||||
.rate_range(1..=48000)
|
||||
.channels_range(1..=2)
|
||||
.build();
|
||||
|
||||
// build the harness setting its src and sink caps,
|
||||
// also passing the csd score to the filter element
|
||||
let mut h = build_harness(
|
||||
src_caps,
|
||||
sink_caps,
|
||||
// creates a csd score that defines the input and output formats on the csound side
|
||||
// the output fomart would be 1 channel audio samples at 44100
|
||||
&CSD!(ksmps, ichannels, ochannels, "ain, ain2 ins", "out ain"),
|
||||
);
|
||||
|
||||
h.play();
|
||||
assert!(h.push(gst::Buffer::with_size(2048).unwrap()).is_ok());
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
let buffer = h.pull().unwrap();
|
||||
|
||||
// Pushing a buffer without a timestamp should produce a no timestamp output
|
||||
assert!(buffer.as_ref().pts().is_none());
|
||||
// But It should have a duration
|
||||
assert_eq!(
|
||||
buffer.as_ref().duration(),
|
||||
duration_from_samples(1024 / std::mem::size_of::<f64>() as u64, sr as u64)
|
||||
);
|
||||
|
||||
// get the negotiated harness sink caps
|
||||
let harness_sink_caps = h
|
||||
.sinkpad()
|
||||
.expect("harness has no sinkpad")
|
||||
.current_caps()
|
||||
.expect("pad has no caps");
|
||||
|
||||
// our expected caps at the harness sinkpad
|
||||
let expected_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F64)
|
||||
.rate(44_100)
|
||||
.channels(ochannels)
|
||||
.build();
|
||||
|
||||
assert_eq!(harness_sink_caps, expected_caps);
|
||||
}
|
||||
|
||||
// Similar to caps negotiation, but in this case, we configure a fixated caps in the harness sinkpad,
|
||||
// such caps are incompatible with the csoundfilter and it leads to an error during the caps negotiation,
|
||||
// because there is not a common intersection between both caps.
|
||||
#[test]
|
||||
fn csound_filter_caps_negotiation_fail() {
|
||||
init();
|
||||
|
||||
let ksmps = 4;
|
||||
let ichannels = 2;
|
||||
let ochannels = 1;
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F64)
|
||||
.rate(44_100)
|
||||
.channels(ichannels)
|
||||
.build();
|
||||
|
||||
// instead of having a range for channels/rate fields
|
||||
// we fixate them to 2 and 48_000 respectively, which would cause the negotiation error
|
||||
let sink_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F64)
|
||||
.rate(48_000)
|
||||
.channels(ichannels)
|
||||
.build();
|
||||
|
||||
let mut h = build_harness(
|
||||
src_caps,
|
||||
sink_caps,
|
||||
// creates a csd score that defines the input and output formats on the csound side
|
||||
// the output fomart would be 1 channel audio samples at 44100
|
||||
&CSD!(ksmps, ichannels, ochannels, "ain, ain2 ins", "out ain"),
|
||||
);
|
||||
|
||||
h.play();
|
||||
|
||||
let buffer = gst::Buffer::with_size(2048).unwrap();
|
||||
assert!(h.push(buffer).is_err());
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
// The harness sinkpad end up not having defined caps
|
||||
// so, the get_current_caps should be None
|
||||
let current_caps = h.sinkpad().expect("harness has no sinkpad").current_caps();
|
||||
|
||||
assert!(current_caps.is_none());
|
||||
}
|
47
audio/lewton/Cargo.toml
Normal file
47
audio/lewton/Cargo.toml
Normal file
|
@ -0,0 +1,47 @@
|
|||
[package]
|
||||
name = "gst-plugin-lewton"
|
||||
version.workspace = true
|
||||
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
|
||||
repository.workspace = true
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "GStreamer lewton Vorbis Decoder Plugin"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gst.workspace = true
|
||||
gst-audio.workspace = true
|
||||
lewton = { version = "0.10", default-features = false }
|
||||
byte-slice-cast = "1.0"
|
||||
atomic_refcell = "0.1"
|
||||
once_cell.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gst-check.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "gstlewton"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper.workspace = true
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
doc = ["gst/v1_18"]
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.9.21"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
import_library = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0"
|
201
audio/lewton/LICENSE-APACHE
Normal file
201
audio/lewton/LICENSE-APACHE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
23
audio/lewton/LICENSE-MIT
Normal file
23
audio/lewton/LICENSE-MIT
Normal file
|
@ -0,0 +1,23 @@
|
|||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
3
audio/lewton/build.rs
Normal file
3
audio/lewton/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
623
audio/lewton/src/lewtondec/imp.rs
Normal file
623
audio/lewton/src/lewtondec/imp.rs
Normal file
|
@ -0,0 +1,623 @@
|
|||
// Copyright (C) 2019 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_audio::audio_decoder_error;
|
||||
use gst_audio::prelude::*;
|
||||
use gst_audio::subclass::prelude::*;
|
||||
|
||||
use atomic_refcell::AtomicRefCell;
|
||||
|
||||
use byte_slice_cast::*;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
struct State {
|
||||
header_bufs: (
|
||||
Option<gst::Buffer>,
|
||||
Option<gst::Buffer>,
|
||||
Option<gst::Buffer>,
|
||||
),
|
||||
headerset: Option<lewton::header::HeaderSet>,
|
||||
pwr: lewton::audio::PreviousWindowRight,
|
||||
audio_info: Option<gst_audio::AudioInfo>,
|
||||
reorder_map: Option<[usize; 8]>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LewtonDec {
|
||||
state: AtomicRefCell<Option<State>>,
|
||||
}
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"lewtondec",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("lewton Vorbis decoder"),
|
||||
)
|
||||
});
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for LewtonDec {
|
||||
const NAME: &'static str = "GstLewtonDec";
|
||||
type Type = super::LewtonDec;
|
||||
type ParentType = gst_audio::AudioDecoder;
|
||||
}
|
||||
|
||||
impl ObjectImpl for LewtonDec {}
|
||||
|
||||
impl GstObjectImpl for LewtonDec {}
|
||||
|
||||
impl ElementImpl for LewtonDec {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"lewton Vorbis decoder",
|
||||
"Decoder/Audio",
|
||||
"lewton Vorbis decoder",
|
||||
"Sebastian Dröge <sebastian@centricular.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let sink_caps = gst::Caps::builder("audio/x-vorbis").build();
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&sink_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let src_caps = gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.channels_range(1..=255)
|
||||
.build();
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&src_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![sink_pad_template, src_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioDecoderImpl for LewtonDec {
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
*self.state.borrow_mut() = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&self) -> Result<(), gst::ErrorMessage> {
|
||||
*self.state.borrow_mut() = Some(State {
|
||||
header_bufs: (None, None, None),
|
||||
headerset: None,
|
||||
pwr: lewton::audio::PreviousWindowRight::new(),
|
||||
audio_info: None,
|
||||
reorder_map: None,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_format(&self, caps: &gst::Caps) -> Result<(), gst::LoggableError> {
|
||||
gst::debug!(CAT, imp: self, "Setting format {:?}", caps);
|
||||
|
||||
// When the caps are changing we require new headers
|
||||
let mut state_guard = self.state.borrow_mut();
|
||||
*state_guard = Some(State {
|
||||
header_bufs: (None, None, None),
|
||||
headerset: None,
|
||||
pwr: lewton::audio::PreviousWindowRight::new(),
|
||||
audio_info: None,
|
||||
reorder_map: None,
|
||||
});
|
||||
|
||||
let state = state_guard.as_mut().unwrap();
|
||||
|
||||
let s = caps.structure(0).unwrap();
|
||||
if let Ok(Some(streamheaders)) = s.get_optional::<gst::ArrayRef>("streamheader") {
|
||||
let streamheaders = streamheaders.as_slice();
|
||||
if streamheaders.len() < 3 {
|
||||
gst::debug!(CAT, imp: self, "Not enough streamheaders, trying in-band");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ident_buf = streamheaders[0].get::<Option<gst::Buffer>>();
|
||||
let comment_buf = streamheaders[1].get::<Option<gst::Buffer>>();
|
||||
let setup_buf = streamheaders[2].get::<Option<gst::Buffer>>();
|
||||
if let (Ok(Some(ident_buf)), Ok(Some(comment_buf)), Ok(Some(setup_buf))) =
|
||||
(ident_buf, comment_buf, setup_buf)
|
||||
{
|
||||
gst::debug!(CAT, imp: self, "Got streamheader buffers");
|
||||
state.header_bufs = (Some(ident_buf), Some(comment_buf), Some(setup_buf));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush(&self, _hard: bool) {
|
||||
gst::debug!(CAT, imp: self, "Flushing");
|
||||
|
||||
let mut state_guard = self.state.borrow_mut();
|
||||
if let Some(ref mut state) = *state_guard {
|
||||
state.pwr = lewton::audio::PreviousWindowRight::new();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_frame(
|
||||
&self,
|
||||
inbuf: Option<&gst::Buffer>,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
gst::debug!(CAT, imp: self, "Handling buffer {:?}", inbuf);
|
||||
|
||||
let inbuf = match inbuf {
|
||||
None => return Ok(gst::FlowSuccess::Ok),
|
||||
Some(inbuf) => inbuf,
|
||||
};
|
||||
|
||||
let inmap = inbuf.map_readable().map_err(|_| {
|
||||
gst::error!(CAT, imp: self, "Failed to buffer readable");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let mut state_guard = self.state.borrow_mut();
|
||||
let state = state_guard.as_mut().ok_or(gst::FlowError::NotNegotiated)?;
|
||||
|
||||
// Ignore empty packets unless we have no headers yet
|
||||
if inmap.len() == 0 {
|
||||
self.obj().finish_frame(None, 1)?;
|
||||
|
||||
if state.headerset.is_some() {
|
||||
return Ok(gst::FlowSuccess::Ok);
|
||||
} else {
|
||||
gst::error!(CAT, imp: self, "Got empty packet before all headers");
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a header packet then handle it
|
||||
if inmap[0] & 0x01 == 0x01 {
|
||||
return self.handle_header(state, inbuf, inmap.as_ref());
|
||||
}
|
||||
|
||||
// If it's a data packet then try to initialize the headerset now if we didn't yet
|
||||
if state.headerset.is_none() {
|
||||
self.initialize(state)?;
|
||||
}
|
||||
|
||||
self.handle_data(state, inmap.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl LewtonDec {
|
||||
fn handle_header(
|
||||
&self,
|
||||
state: &mut State,
|
||||
inbuf: &gst::Buffer,
|
||||
indata: &[u8],
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
// ident header
|
||||
if indata[0] == 0x01 {
|
||||
gst::debug!(CAT, imp: self, "Got ident header buffer");
|
||||
state.header_bufs = (Some(inbuf.clone()), None, None);
|
||||
} else if indata[0] == 0x03 {
|
||||
// comment header
|
||||
if state.header_bufs.0.is_none() {
|
||||
gst::warning!(CAT, imp: self, "Got comment header before ident header");
|
||||
} else {
|
||||
gst::debug!(CAT, imp: self, "Got comment header buffer");
|
||||
state.header_bufs.1 = Some(inbuf.clone());
|
||||
}
|
||||
} else if indata[0] == 0x05 {
|
||||
// setup header
|
||||
if state.header_bufs.0.is_none() || state.header_bufs.1.is_none() {
|
||||
gst::warning!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Got setup header before ident/comment header"
|
||||
);
|
||||
} else {
|
||||
gst::debug!(CAT, imp: self, "Got setup header buffer");
|
||||
state.header_bufs.2 = Some(inbuf.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.obj().finish_frame(None, 1)
|
||||
}
|
||||
|
||||
fn initialize(&self, state: &mut State) -> Result<(), gst::FlowError> {
|
||||
let (ident_buf, comment_buf, setup_buf) = match state.header_bufs {
|
||||
(Some(ref ident_buf), Some(ref comment_buf), Some(ref setup_buf)) => {
|
||||
(ident_buf, comment_buf, setup_buf)
|
||||
}
|
||||
_ => {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::StreamError::Decode,
|
||||
["Got no headers before data packets"]
|
||||
);
|
||||
return Err(gst::FlowError::NotNegotiated);
|
||||
}
|
||||
};
|
||||
|
||||
// First try to parse the headers
|
||||
let ident_map = ident_buf.map_readable().map_err(|_| {
|
||||
gst::error!(CAT, imp: self, "Failed to map ident buffer readable");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
let ident = lewton::header::read_header_ident(ident_map.as_ref()).map_err(|err| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::StreamError::Decode,
|
||||
["Failed to parse ident header: {:?}", err]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let comment_map = comment_buf.map_readable().map_err(|_| {
|
||||
gst::error!(CAT, imp: self, "Failed to map comment buffer readable");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
let comment = lewton::header::read_header_comment(comment_map.as_ref()).map_err(|err| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::StreamError::Decode,
|
||||
["Failed to parse comment header: {:?}", err]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let setup_map = setup_buf.map_readable().map_err(|_| {
|
||||
gst::error!(CAT, imp: self, "Failed to map setup buffer readable");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
let setup = lewton::header::read_header_setup(
|
||||
setup_map.as_ref(),
|
||||
ident.audio_channels,
|
||||
(ident.blocksize_0, ident.blocksize_1),
|
||||
)
|
||||
.map_err(|err| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::StreamError::Decode,
|
||||
["Failed to parse setup header: {:?}", err]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let mut audio_info = gst_audio::AudioInfo::builder(
|
||||
gst_audio::AUDIO_FORMAT_F32,
|
||||
ident.audio_sample_rate,
|
||||
ident.audio_channels as u32,
|
||||
);
|
||||
|
||||
// For 1-8 channels there are defined channel positions, so initialize
|
||||
// everything accordingly and set the channel positions in the output
|
||||
// format
|
||||
let mut reorder_map = None;
|
||||
if ident.audio_channels > 1 && ident.audio_channels < 9 {
|
||||
let channels = ident.audio_channels as usize;
|
||||
let from = &VORBIS_CHANNEL_POSITIONS[channels - 1][..channels];
|
||||
let to = &GST_VORBIS_CHANNEL_POSITIONS[channels - 1][..channels];
|
||||
|
||||
audio_info = audio_info.positions(to);
|
||||
|
||||
let mut map = [0; 8];
|
||||
if gst_audio::channel_reorder_map(from, to, &mut map[..channels]).is_err() {
|
||||
gst::error!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Failed to generate channel reorder map from {:?} to {:?}",
|
||||
from,
|
||||
to,
|
||||
);
|
||||
} else {
|
||||
// If the reorder map is not the identity matrix
|
||||
if !map[..channels].iter().enumerate().all(|(c1, c2)| c1 == *c2) {
|
||||
reorder_map = Some(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
let audio_info = audio_info.build().unwrap();
|
||||
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Successfully parsed headers: {:?}",
|
||||
audio_info
|
||||
);
|
||||
state.headerset = Some((ident, comment, setup));
|
||||
state.audio_info = Some(audio_info.clone());
|
||||
state.reorder_map = reorder_map;
|
||||
|
||||
self.obj().set_output_format(&audio_info)?;
|
||||
self.obj().negotiate()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_data(
|
||||
&self,
|
||||
state: &mut State,
|
||||
indata: &[u8],
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
// We ensured above that we have headers here now
|
||||
let headerset = state.headerset.as_ref().unwrap();
|
||||
let audio_info = state.audio_info.as_ref().unwrap();
|
||||
|
||||
// Decode the input packet
|
||||
let decoded = match lewton::audio::read_audio_packet_generic::<
|
||||
lewton::samples::InterleavedSamples<f32>,
|
||||
>(&headerset.0, &headerset.2, indata, &mut state.pwr)
|
||||
{
|
||||
Ok(decoded) => decoded,
|
||||
Err(err) => {
|
||||
return audio_decoder_error!(
|
||||
self.obj(),
|
||||
1,
|
||||
gst::StreamError::Decode,
|
||||
["Failed to decode packet: {:?}", err]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if decoded.channel_count != audio_info.channels() as usize {
|
||||
return audio_decoder_error!(
|
||||
self.obj(),
|
||||
1,
|
||||
gst::StreamError::Decode,
|
||||
[
|
||||
"Channel count mismatch (got {}, expected {})",
|
||||
decoded.channel_count,
|
||||
audio_info.channels()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
let sample_count = decoded.samples.len() / audio_info.channels() as usize;
|
||||
gst::debug!(CAT, imp: self, "Got {} decoded samples", sample_count);
|
||||
|
||||
if sample_count == 0 {
|
||||
return self.obj().finish_frame(None, 1);
|
||||
}
|
||||
|
||||
let outbuf = if let Some(ref reorder_map) = state.reorder_map {
|
||||
let mut outbuf = self
|
||||
.obj()
|
||||
.allocate_output_buffer(sample_count * audio_info.bpf() as usize);
|
||||
{
|
||||
// And copy the decoded data into our output buffer while reordering the channels to the
|
||||
// GStreamer channel order
|
||||
let outbuf = outbuf.get_mut().unwrap();
|
||||
let mut outmap = outbuf.map_writable().map_err(|_| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::StreamError::Decode,
|
||||
["Failed to map output buffer writable"]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let outdata = outmap.as_mut_slice_of::<f32>().unwrap();
|
||||
let channels = audio_info.channels() as usize;
|
||||
assert!(reorder_map.len() >= channels);
|
||||
assert!(reorder_map[..channels].iter().all(|c| *c < channels));
|
||||
|
||||
for (output, input) in outdata
|
||||
.chunks_exact_mut(channels)
|
||||
.zip(decoded.samples.chunks_exact(channels))
|
||||
{
|
||||
for (c, s) in input.iter().enumerate() {
|
||||
output[reorder_map[c]] = *s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outbuf
|
||||
} else {
|
||||
struct CastVec(Vec<f32>);
|
||||
impl AsRef<[u8]> for CastVec {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_byte_slice()
|
||||
}
|
||||
}
|
||||
impl AsMut<[u8]> for CastVec {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
self.0.as_mut_byte_slice()
|
||||
}
|
||||
}
|
||||
|
||||
gst::Buffer::from_mut_slice(CastVec(decoded.samples))
|
||||
};
|
||||
|
||||
self.obj().finish_frame(Some(outbuf), 1)
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-800004.3.9
|
||||
const VORBIS_CHANNEL_POSITIONS: [[gst_audio::AudioChannelPosition; 8]; 8] = [
|
||||
[
|
||||
gst_audio::AudioChannelPosition::Mono,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::SideLeft,
|
||||
gst_audio::AudioChannelPosition::SideRight,
|
||||
gst_audio::AudioChannelPosition::RearCenter,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::SideLeft,
|
||||
gst_audio::AudioChannelPosition::SideRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
],
|
||||
];
|
||||
|
||||
const GST_VORBIS_CHANNEL_POSITIONS: [[gst_audio::AudioChannelPosition; 8]; 8] = [
|
||||
[
|
||||
gst_audio::AudioChannelPosition::Mono,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
gst_audio::AudioChannelPosition::RearCenter,
|
||||
gst_audio::AudioChannelPosition::SideLeft,
|
||||
gst_audio::AudioChannelPosition::SideRight,
|
||||
gst_audio::AudioChannelPosition::Invalid,
|
||||
],
|
||||
[
|
||||
gst_audio::AudioChannelPosition::FrontLeft,
|
||||
gst_audio::AudioChannelPosition::FrontRight,
|
||||
gst_audio::AudioChannelPosition::FrontCenter,
|
||||
gst_audio::AudioChannelPosition::Lfe1,
|
||||
gst_audio::AudioChannelPosition::RearLeft,
|
||||
gst_audio::AudioChannelPosition::RearRight,
|
||||
gst_audio::AudioChannelPosition::SideLeft,
|
||||
gst_audio::AudioChannelPosition::SideRight,
|
||||
],
|
||||
];
|
27
audio/lewton/src/lewtondec/mod.rs
Normal file
27
audio/lewton/src/lewtondec/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright (C) 2019 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct LewtonDec(ObjectSubclass<imp::LewtonDec>) @extends gst_audio::AudioDecoder, gst::Element, gst::Object;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"lewtondec",
|
||||
gst::Rank::MARGINAL,
|
||||
LewtonDec::static_type(),
|
||||
)
|
||||
}
|
35
audio/lewton/src/lib.rs
Normal file
35
audio/lewton/src/lib.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Copyright (C) 2019 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
|
||||
|
||||
use gst::glib;
|
||||
|
||||
/**
|
||||
* plugin-lewton:
|
||||
*
|
||||
* Since: plugins-rs-0.6.0
|
||||
*/
|
||||
mod lewtondec;
|
||||
|
||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
lewtondec::register(plugin)
|
||||
}
|
||||
|
||||
gst::plugin_define!(
|
||||
lewton,
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
plugin_init,
|
||||
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||
"MIT/X11",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
env!("BUILD_REL_DATE")
|
||||
);
|
102
audio/lewton/tests/lewtondec.rs
Normal file
102
audio/lewton/tests/lewtondec.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Copyright (C) 2019 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::prelude::*;
|
||||
|
||||
fn init() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
INIT.call_once(|| {
|
||||
gst::init().unwrap();
|
||||
gstlewton::plugin_register_static().expect("lewton test");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_streamheader() {
|
||||
run_test(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_inline_headers() {
|
||||
run_test(true);
|
||||
}
|
||||
|
||||
fn run_test(inline_headers: bool) {
|
||||
let data = include_bytes!("test.vorbis");
|
||||
let packet_sizes = [30, 99, 3189, 43, 20, 56, 56, 21, 20, 22, 21, 22, 22, 43];
|
||||
let packet_offsets = packet_sizes
|
||||
.iter()
|
||||
.scan(0, |state, &size| {
|
||||
*state += size;
|
||||
Some(*state)
|
||||
})
|
||||
.collect::<Vec<usize>>();
|
||||
let decoded_samples = [0usize, 128, 576, 1472, 128, 128, 128, 128, 128, 128, 128];
|
||||
|
||||
init();
|
||||
|
||||
let mut h = gst_check::Harness::new("lewtondec");
|
||||
h.play();
|
||||
|
||||
if inline_headers {
|
||||
let caps = gst::Caps::builder("audio/x-vorbis").build();
|
||||
h.set_src_caps(caps);
|
||||
} else {
|
||||
let caps = gst::Caps::builder("audio/x-vorbis")
|
||||
.field(
|
||||
"streamheader",
|
||||
gst::Array::new([
|
||||
gst::Buffer::from_slice(&data[0..packet_offsets[0]]),
|
||||
gst::Buffer::from_slice(&data[packet_offsets[0]..packet_offsets[1]]),
|
||||
gst::Buffer::from_slice(&data[packet_offsets[1]..packet_offsets[2]]),
|
||||
]),
|
||||
)
|
||||
.build();
|
||||
h.set_src_caps(caps);
|
||||
}
|
||||
|
||||
let packet_offsets_iter = std::iter::once(&0).chain(packet_offsets.iter());
|
||||
let skip = if inline_headers { 0 } else { 3 };
|
||||
|
||||
for (offset_start, offset_end) in packet_offsets_iter
|
||||
.clone()
|
||||
.skip(skip)
|
||||
.zip(packet_offsets_iter.clone().skip(skip + 1))
|
||||
{
|
||||
let buffer = gst::Buffer::from_slice(&data[*offset_start..*offset_end]);
|
||||
h.push(buffer).unwrap();
|
||||
}
|
||||
|
||||
h.push_event(gst::event::Eos::new());
|
||||
|
||||
for samples in &decoded_samples {
|
||||
if *samples == 0 {
|
||||
continue;
|
||||
}
|
||||
let buffer = h.pull().unwrap();
|
||||
assert_eq!(buffer.size(), 4 * samples);
|
||||
}
|
||||
|
||||
let caps = h
|
||||
.sinkpad()
|
||||
.expect("harness has no sinkpad")
|
||||
.current_caps()
|
||||
.expect("pad has no caps");
|
||||
assert_eq!(
|
||||
caps,
|
||||
gst_audio::AudioCapsBuilder::new_interleaved()
|
||||
.format(gst_audio::AUDIO_FORMAT_F32)
|
||||
.rate(44_100)
|
||||
.channels(1)
|
||||
.build()
|
||||
);
|
||||
}
|
BIN
audio/lewton/tests/test.vorbis
Normal file
BIN
audio/lewton/tests/test.vorbis
Normal file
Binary file not shown.
46
audio/spotify/Cargo.toml
Normal file
46
audio/spotify/Cargo.toml
Normal file
|
@ -0,0 +1,46 @@
|
|||
[package]
|
||||
name = "gst-plugin-spotify"
|
||||
version.workspace = true
|
||||
authors = ["Guillaume Desmottes <guillaume@desmottes.be>"]
|
||||
repository.workspace = true
|
||||
license = "MPL-2.0"
|
||||
description = "GStreamer Spotify Plugin"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
gst.workspace = true
|
||||
gst-base.workspace = true
|
||||
librespot = { version = "0.4", default-features = false }
|
||||
tokio = "1.0"
|
||||
futures = "0.3"
|
||||
anyhow = "1.0"
|
||||
url = "2.3"
|
||||
once_cell.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "gstspotify"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper.workspace = true
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
doc = ["gst/v1_18"]
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.9.21"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
import_library = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0"
|
373
audio/spotify/LICENSE-MPL-2.0
Normal file
373
audio/spotify/LICENSE-MPL-2.0
Normal file
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
31
audio/spotify/README.md
Normal file
31
audio/spotify/README.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# gst-plugins-spotify
|
||||
|
||||
This is a [GStreamer](https://gstreamer.freedesktop.org/) plugin to read content from
|
||||
[Spotify](https://www.spotify.com/).
|
||||
|
||||
Make sure that your application follows [Spotify's design guidelines](https://developer.spotify.com/documentation/general/design-and-branding/)
|
||||
to respect their legal/licensing restrictions.
|
||||
|
||||
## Spotify Credentials
|
||||
|
||||
This plugin requires a [Spotify Premium](https://www.spotify.com/premium/) account.
|
||||
If your account is linked with Facebook, you'll need to setup
|
||||
a [device username and password](https://www.spotify.com/us/account/set-device-password/).
|
||||
|
||||
Those username and password are then set using the `username` and `password` properties.
|
||||
|
||||
You may also want to cache credentials and downloaded files, see the `cache-` properties on the element.
|
||||
|
||||
## spotifyaudiosrc
|
||||
|
||||
The `spotifyaudiosrc` element can be used to play a song from Spotify using its [Spotify URI](https://community.spotify.com/t5/FAQs/What-s-a-Spotify-URI/ta-p/919201).
|
||||
|
||||
```
|
||||
gst-launch-1.0 spotifyaudiosrc username=$USERNAME password=$PASSWORD track=spotify:track:3i3P1mGpV9eRlfKccjDjwi ! oggdemux ! vorbisdec ! audioconvert ! autoaudiosink
|
||||
```
|
||||
|
||||
The element also implements an URI handler which accepts credentials and cache settings as URI parameters:
|
||||
|
||||
```console
|
||||
gst-launch-1.0 playbin3 uri=spotify:track:3i3P1mGpV9eRlfKccjDjwi?username=$USERNAME\&password=$PASSWORD\&cache-credentials=cache\&cache-files=cache
|
||||
```
|
3
audio/spotify/build.rs
Normal file
3
audio/spotify/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
194
audio/spotify/src/common.rs
Normal file
194
audio/spotify/src/common.rs
Normal file
|
@ -0,0 +1,194 @@
|
|||
// Copyright (C) 2021 Guillaume Desmottes <guillaume@desmottes.be>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use anyhow::bail;
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
use librespot::core::{
|
||||
cache::Cache, config::SessionConfig, session::Session, spotify_id::SpotifyId,
|
||||
};
|
||||
use librespot::discovery::Credentials;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Settings {
|
||||
username: String,
|
||||
password: String,
|
||||
cache_credentials: String,
|
||||
cache_files: String,
|
||||
cache_max_size: u64,
|
||||
pub track: String,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn properties() -> Vec<glib::ParamSpec> {
|
||||
vec![glib::ParamSpecString::builder("username")
|
||||
.nick("Username")
|
||||
.blurb("Spotify username, Facebook accounts need a device username from https://www.spotify.com/us/account/set-device-password/")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("password")
|
||||
.nick("Password")
|
||||
.blurb("Spotify password, Facebook accounts need a device password from https://www.spotify.com/us/account/set-device-password/")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("cache-credentials")
|
||||
.nick("Credentials cache")
|
||||
.blurb("Directory where to cache Spotify credentials")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("cache-files")
|
||||
.nick("Files cache")
|
||||
.blurb("Directory where to cache downloaded files from Spotify")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecUInt64::builder("cache-max-size")
|
||||
.nick("Cache max size")
|
||||
.blurb("The max allowed size of the cache, in bytes, or 0 to disable the cache limit")
|
||||
.default_value(0)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecString::builder("track")
|
||||
.nick("Spotify URI")
|
||||
.blurb("Spotify track URI, in the form 'spotify:track:$SPOTIFY_ID'")
|
||||
.default_value(Some(""))
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn set_property(&mut self, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"username" => {
|
||||
self.username = value.get().expect("type checked upstream");
|
||||
}
|
||||
"password" => {
|
||||
self.password = value.get().expect("type checked upstream");
|
||||
}
|
||||
"cache-credentials" => {
|
||||
self.cache_credentials = value.get().expect("type checked upstream");
|
||||
}
|
||||
"cache-files" => {
|
||||
self.cache_files = value.get().expect("type checked upstream");
|
||||
}
|
||||
"cache-max-size" => {
|
||||
self.cache_max_size = value.get().expect("type checked upstream");
|
||||
}
|
||||
"track" => {
|
||||
self.track = value.get().expect("type checked upstream");
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn property(&self, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"username" => self.username.to_value(),
|
||||
"password" => self.password.to_value(),
|
||||
"cache-credentials" => self.cache_credentials.to_value(),
|
||||
"cache-files" => self.cache_files.to_value(),
|
||||
"cache-max-size" => self.cache_max_size.to_value(),
|
||||
"track" => self.track.to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_session<T>(
|
||||
&self,
|
||||
src: T,
|
||||
cat: &gst::DebugCategory,
|
||||
) -> anyhow::Result<Session>
|
||||
where
|
||||
T: IsA<glib::Object>,
|
||||
{
|
||||
let credentials_cache = if self.cache_credentials.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&self.cache_credentials)
|
||||
};
|
||||
|
||||
let files_cache = if self.cache_files.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&self.cache_files)
|
||||
};
|
||||
|
||||
let max_size = if self.cache_max_size != 0 {
|
||||
Some(self.cache_max_size)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let cache = Cache::new(credentials_cache, None, files_cache, max_size)?;
|
||||
|
||||
if let Some(cached_cred) = cache.credentials() {
|
||||
if !self.username.is_empty() && self.username != cached_cred.username {
|
||||
gst::debug!(
|
||||
cat,
|
||||
obj: &src,
|
||||
"ignore cached credentials for user {} which mismatch user {}",
|
||||
cached_cred.username,
|
||||
self.username
|
||||
);
|
||||
} else {
|
||||
gst::debug!(
|
||||
cat,
|
||||
obj: &src,
|
||||
"reuse cached credentials for user {}",
|
||||
cached_cred.username
|
||||
);
|
||||
if let Ok((session, _credentials)) = Session::connect(
|
||||
SessionConfig::default(),
|
||||
cached_cred,
|
||||
Some(cache.clone()),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gst::debug!(
|
||||
cat,
|
||||
obj: &src,
|
||||
"credentials not in cache or cached credentials invalid",
|
||||
);
|
||||
|
||||
if self.username.is_empty() {
|
||||
bail!("username is not set and credentials are not in cache");
|
||||
}
|
||||
if self.password.is_empty() {
|
||||
bail!("password is not set and credentials are not in cache");
|
||||
}
|
||||
|
||||
let cred = Credentials::with_password(&self.username, &self.password);
|
||||
|
||||
let (session, _credentials) =
|
||||
Session::connect(SessionConfig::default(), cred, Some(cache), true).await?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
pub fn track_id(&self) -> anyhow::Result<SpotifyId> {
|
||||
if self.track.is_empty() {
|
||||
bail!("track is not set");
|
||||
}
|
||||
let track = SpotifyId::from_uri(&self.track).map_err(|_| {
|
||||
anyhow::anyhow!("failed to create Spotify URI from track {}", self.track)
|
||||
})?;
|
||||
|
||||
Ok(track)
|
||||
}
|
||||
}
|
36
audio/spotify/src/lib.rs
Normal file
36
audio/spotify/src/lib.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright (C) 2021 Guillaume Desmottes <guillaume@desmottes.be>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
|
||||
|
||||
/**
|
||||
* plugin-spotify:
|
||||
*
|
||||
* Since: plugins-rs-0.8.0
|
||||
*/
|
||||
use gst::glib;
|
||||
|
||||
mod common;
|
||||
mod spotifyaudiosrc;
|
||||
|
||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
spotifyaudiosrc::register(plugin)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
gst::plugin_define!(
|
||||
spotify,
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
plugin_init,
|
||||
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||
// FIXME: MPL-2.0 is only allowed since 1.18.3 (as unknown) and 1.20 (as known)
|
||||
"MPL",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
env!("BUILD_REL_DATE")
|
||||
);
|
471
audio/spotify/src/spotifyaudiosrc/imp.rs
Normal file
471
audio/spotify/src/spotifyaudiosrc/imp.rs
Normal file
|
@ -0,0 +1,471 @@
|
|||
// Copyright (C) 2021 Guillaume Desmottes <guillaume@desmottes.be>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
|
||||
use futures::future::{AbortHandle, Abortable, Aborted};
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::{runtime, task::JoinHandle};
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_base::subclass::{base_src::CreateSuccess, prelude::*};
|
||||
|
||||
use librespot::playback::{
|
||||
audio_backend::{Sink, SinkResult},
|
||||
config::PlayerConfig,
|
||||
convert::Converter,
|
||||
decoder::AudioPacket,
|
||||
mixer::NoOpVolume,
|
||||
player::{Player, PlayerEvent},
|
||||
};
|
||||
|
||||
use super::Bitrate;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"spotifyaudiosrc",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("Spotify audio source"),
|
||||
)
|
||||
});
|
||||
|
||||
static RUNTIME: Lazy<runtime::Runtime> = Lazy::new(|| {
|
||||
runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.worker_threads(1)
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// Messages from the librespot thread
|
||||
enum Message {
|
||||
Buffer(gst::Buffer),
|
||||
Eos,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
struct State {
|
||||
player: Player,
|
||||
|
||||
/// receiver sending buffer to streaming thread
|
||||
receiver: mpsc::Receiver<Message>,
|
||||
/// thread receiving player events from librespot
|
||||
player_channel_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Settings {
|
||||
common: crate::common::Settings,
|
||||
bitrate: Bitrate,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum SetupThread {
|
||||
#[default]
|
||||
None,
|
||||
Pending {
|
||||
thread_handle: Option<std::thread::JoinHandle<Result<anyhow::Result<()>, Aborted>>>,
|
||||
abort_handle: AbortHandle,
|
||||
},
|
||||
Cancelled,
|
||||
Done,
|
||||
}
|
||||
|
||||
impl SetupThread {
|
||||
fn abort(&mut self) {
|
||||
// Cancel setup thread if it is pending and not done yet
|
||||
if matches!(self, SetupThread::None | SetupThread::Done) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let SetupThread::Pending {
|
||||
ref abort_handle, ..
|
||||
} = *self
|
||||
{
|
||||
abort_handle.abort();
|
||||
}
|
||||
*self = SetupThread::Cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SpotifyAudioSrc {
|
||||
setup_thread: Mutex<SetupThread>,
|
||||
state: Arc<Mutex<Option<State>>>,
|
||||
settings: Mutex<Settings>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for SpotifyAudioSrc {
|
||||
const NAME: &'static str = "GstSpotifyAudioSrc";
|
||||
type Type = super::SpotifyAudioSrc;
|
||||
type ParentType = gst_base::PushSrc;
|
||||
type Interfaces = (gst::URIHandler,);
|
||||
}
|
||||
|
||||
impl ObjectImpl for SpotifyAudioSrc {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
let mut props = crate::common::Settings::properties();
|
||||
let default = Settings::default();
|
||||
|
||||
props.push(
|
||||
glib::ParamSpecEnum::builder_with_default::<Bitrate>("bitrate", default.bitrate)
|
||||
.nick("Spotify bitrate")
|
||||
.blurb("Spotify audio bitrate in kbit/s")
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
);
|
||||
props
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
|
||||
match pspec.name() {
|
||||
"bitrate" => {
|
||||
settings.bitrate = value.get().expect("type checked upstream");
|
||||
}
|
||||
_ => settings.common.set_property(value, pspec),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
match pspec.name() {
|
||||
"bitrate" => settings.bitrate.to_value(),
|
||||
_ => settings.common.property(pspec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for SpotifyAudioSrc {}
|
||||
|
||||
impl ElementImpl for SpotifyAudioSrc {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"Spotify source",
|
||||
"Source/Audio",
|
||||
"Spotify source",
|
||||
"Guillaume Desmottes <guillaume@desmottes.be>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let caps = gst::Caps::builder("application/ogg").build();
|
||||
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseSrcImpl for SpotifyAudioSrc {
|
||||
fn start(&self) -> Result<(), gst::ErrorMessage> {
|
||||
{
|
||||
let state = self.state.lock().unwrap();
|
||||
if state.is_some() {
|
||||
// already started
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// If not started yet and not cancelled, start the setup
|
||||
let mut setup_thread = self.setup_thread.lock().unwrap();
|
||||
assert!(!matches!(&*setup_thread, SetupThread::Cancelled));
|
||||
if matches!(&*setup_thread, SetupThread::None) {
|
||||
self.start_setup(&mut setup_thread);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
if let Some(state) = self.state.lock().unwrap().take() {
|
||||
gst::debug!(CAT, imp: self, "stopping");
|
||||
state.player.stop();
|
||||
state.player_channel_handle.abort();
|
||||
// FIXME: not sure why this is needed to unblock BufferSink::write(), dropping State should drop the receiver
|
||||
drop(state.receiver);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unlock(&self) -> Result<(), gst::ErrorMessage> {
|
||||
let mut setup_thread = self.setup_thread.lock().unwrap();
|
||||
setup_thread.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unlock_stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
let mut setup_thread = self.setup_thread.lock().unwrap();
|
||||
if matches!(&*setup_thread, SetupThread::Cancelled) {
|
||||
*setup_thread = SetupThread::None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PushSrcImpl for SpotifyAudioSrc {
|
||||
fn create(
|
||||
&self,
|
||||
_buffer: Option<&mut gst::BufferRef>,
|
||||
) -> Result<CreateSuccess, gst::FlowError> {
|
||||
let state_set = {
|
||||
let state = self.state.lock().unwrap();
|
||||
state.is_some()
|
||||
};
|
||||
|
||||
if !state_set {
|
||||
// If not started yet and not cancelled, start the setup
|
||||
let mut setup_thread = self.setup_thread.lock().unwrap();
|
||||
if matches!(&*setup_thread, SetupThread::Cancelled) {
|
||||
return Err(gst::FlowError::Flushing);
|
||||
}
|
||||
|
||||
if matches!(&*setup_thread, SetupThread::None) {
|
||||
self.start_setup(&mut setup_thread);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// wait for the setup to be completed
|
||||
let mut setup_thread = self.setup_thread.lock().unwrap();
|
||||
if let SetupThread::Pending {
|
||||
ref mut thread_handle,
|
||||
..
|
||||
} = *setup_thread
|
||||
{
|
||||
let thread_handle = thread_handle.take().expect("Waiting multiple times");
|
||||
drop(setup_thread);
|
||||
let res = thread_handle.join().unwrap();
|
||||
|
||||
match res {
|
||||
Err(_aborted) => {
|
||||
gst::debug!(CAT, imp: self, "setup has been cancelled");
|
||||
setup_thread = self.setup_thread.lock().unwrap();
|
||||
*setup_thread = SetupThread::Cancelled;
|
||||
return Err(gst::FlowError::Flushing);
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
gst::error!(CAT, imp: self, "failed to start: {err:?}");
|
||||
gst::element_imp_error!(self, gst::ResourceError::Settings, ["{err:?}"]);
|
||||
setup_thread = self.setup_thread.lock().unwrap();
|
||||
*setup_thread = SetupThread::None;
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
Ok(Ok(_)) => {
|
||||
setup_thread = self.setup_thread.lock().unwrap();
|
||||
*setup_thread = SetupThread::Done;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let state = self.state.lock().unwrap();
|
||||
let state = state.as_ref().unwrap();
|
||||
|
||||
match state.receiver.recv().unwrap() {
|
||||
Message::Buffer(buffer) => {
|
||||
gst::log!(CAT, imp: self, "got buffer of size {}", buffer.size());
|
||||
Ok(CreateSuccess::NewBuffer(buffer))
|
||||
}
|
||||
Message::Eos => {
|
||||
gst::debug!(CAT, imp: self, "eos");
|
||||
Err(gst::FlowError::Eos)
|
||||
}
|
||||
Message::Unavailable => {
|
||||
gst::error!(CAT, imp: self, "track is not available");
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::ResourceError::NotFound,
|
||||
["track is not available"]
|
||||
);
|
||||
Err(gst::FlowError::Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BufferSink {
|
||||
sender: mpsc::SyncSender<Message>,
|
||||
}
|
||||
|
||||
impl Sink for BufferSink {
|
||||
fn write(&mut self, packet: AudioPacket, _converter: &mut Converter) -> SinkResult<()> {
|
||||
let oggdata = match packet {
|
||||
AudioPacket::OggData(data) => data,
|
||||
AudioPacket::Samples(_) => unimplemented!(),
|
||||
};
|
||||
let buffer = gst::Buffer::from_slice(oggdata);
|
||||
|
||||
// ignore if sending fails as that means the source element is being shutdown
|
||||
let _ = self.sender.send(Message::Buffer(buffer));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl URIHandlerImpl for SpotifyAudioSrc {
|
||||
const URI_TYPE: gst::URIType = gst::URIType::Src;
|
||||
|
||||
fn protocols() -> &'static [&'static str] {
|
||||
&["spotify"]
|
||||
}
|
||||
|
||||
fn uri(&self) -> Option<String> {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
if settings.common.track.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(settings.common.track.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn set_uri(&self, uri: &str) -> Result<(), glib::Error> {
|
||||
gst::debug!(CAT, imp: self, "set URI: {}", uri);
|
||||
|
||||
let url = url::Url::parse(uri)
|
||||
.map_err(|e| glib::Error::new(gst::URIError::BadUri, &format!("{e:?}")))?;
|
||||
|
||||
// allow to configure auth and cache settings from the URI
|
||||
for (key, value) in url.query_pairs() {
|
||||
match key.as_ref() {
|
||||
"username" | "password" | "cache-credentials" | "cache-files" => {
|
||||
self.obj().set_property(&key, value.as_ref());
|
||||
}
|
||||
_ => {
|
||||
gst::warning!(CAT, imp: self, "unsupported query: {}={}", key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.obj()
|
||||
.set_property("track", format!("{}:{}", url.scheme(), url.path()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SpotifyAudioSrc {
|
||||
fn start_setup(&self, setup_thread: &mut SetupThread) {
|
||||
assert!(matches!(setup_thread, SetupThread::None));
|
||||
|
||||
let self_ = self.to_owned();
|
||||
|
||||
// run the runtime from another thread to prevent the "start a runtime from within a runtime" panic
|
||||
// when the plugin is statically linked.
|
||||
let (abort_handle, abort_registration) = AbortHandle::new_pair();
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
RUNTIME.block_on(async move {
|
||||
let future = Abortable::new(self_.setup(), abort_registration);
|
||||
future.await
|
||||
})
|
||||
});
|
||||
|
||||
*setup_thread = SetupThread::Pending {
|
||||
thread_handle: Some(thread_handle),
|
||||
abort_handle,
|
||||
};
|
||||
}
|
||||
|
||||
async fn setup(&self) -> anyhow::Result<()> {
|
||||
{
|
||||
let state = self.state.lock().unwrap();
|
||||
|
||||
if state.is_some() {
|
||||
// already setup
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let src = self.obj();
|
||||
|
||||
let (session, track, bitrate) = {
|
||||
let (common, bitrate) = {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
let bitrate = settings.bitrate.into();
|
||||
|
||||
(settings.common.clone(), bitrate)
|
||||
};
|
||||
|
||||
let session = common.connect_session(src.clone(), &CAT).await?;
|
||||
let track = common.track_id()?;
|
||||
gst::debug!(CAT, imp: self, "Requesting bitrate {:?}", bitrate);
|
||||
|
||||
(session, track, bitrate)
|
||||
};
|
||||
|
||||
let player_config = PlayerConfig {
|
||||
passthrough: true,
|
||||
bitrate,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// use a sync channel to prevent buffering the whole track inside the channel
|
||||
let (sender, receiver) = mpsc::sync_channel(2);
|
||||
let sender_clone = sender.clone();
|
||||
|
||||
let (mut player, mut player_event_channel) =
|
||||
Player::new(player_config, session, Box::new(NoOpVolume), || {
|
||||
Box::new(BufferSink { sender })
|
||||
});
|
||||
|
||||
player.load(track, true, 0);
|
||||
|
||||
let player_channel_handle = RUNTIME.spawn(async move {
|
||||
let sender = sender_clone;
|
||||
|
||||
while let Some(event) = player_event_channel.recv().await {
|
||||
match event {
|
||||
PlayerEvent::EndOfTrack { .. } => {
|
||||
let _ = sender.send(Message::Eos);
|
||||
}
|
||||
PlayerEvent::Unavailable { .. } => {
|
||||
let _ = sender.send(Message::Unavailable);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
state.replace(State {
|
||||
player,
|
||||
receiver,
|
||||
player_channel_handle,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
56
audio/spotify/src/spotifyaudiosrc/mod.rs
Normal file
56
audio/spotify/src/spotifyaudiosrc/mod.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Copyright (C) 2021 Guillaume Desmottes <guillaume@desmottes.be>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "GstRsSpotifyBitrate")]
|
||||
enum Bitrate {
|
||||
#[enum_value(name = "96 kbit/s", nick = "96")]
|
||||
B96,
|
||||
#[enum_value(name = "160 kbit/s", nick = "160")]
|
||||
B160,
|
||||
#[enum_value(name = "320 kbit/s", nick = "320")]
|
||||
B320,
|
||||
}
|
||||
|
||||
impl Default for Bitrate {
|
||||
fn default() -> Self {
|
||||
Self::B160
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bitrate> for librespot::playback::config::Bitrate {
|
||||
fn from(value: Bitrate) -> Self {
|
||||
match value {
|
||||
Bitrate::B96 => Self::Bitrate96,
|
||||
Bitrate::B160 => Self::Bitrate160,
|
||||
Bitrate::B320 => Self::Bitrate320,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct SpotifyAudioSrc(ObjectSubclass<imp::SpotifyAudioSrc>) @extends gst_base::PushSrc, gst_base::BaseSrc, gst::Element, gst::Object, @implements gst::URIHandler;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
#[cfg(feature = "doc")]
|
||||
Bitrate::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
|
||||
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"spotifyaudiosrc",
|
||||
gst::Rank::PRIMARY,
|
||||
SpotifyAudioSrc::static_type(),
|
||||
)
|
||||
}
|
204
cargo_wrapper.py
Normal file
204
cargo_wrapper.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import shlex
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path as P
|
||||
|
||||
PARSER = ArgumentParser()
|
||||
PARSER.add_argument('command', choices=['build', 'test'])
|
||||
PARSER.add_argument('build_dir', type=P)
|
||||
PARSER.add_argument('src_dir', type=P)
|
||||
PARSER.add_argument('root_dir', type=P)
|
||||
PARSER.add_argument('target', choices=['release', 'debug'])
|
||||
PARSER.add_argument('prefix', type=P)
|
||||
PARSER.add_argument('libdir', type=P)
|
||||
PARSER.add_argument('--version', default=None)
|
||||
PARSER.add_argument('--bin', default=None, type=P)
|
||||
PARSER.add_argument('--features', nargs="+", default=[])
|
||||
PARSER.add_argument('--packages', nargs="+", default=[])
|
||||
PARSER.add_argument('--examples', nargs="+", default=[])
|
||||
PARSER.add_argument('--lib-suffixes', nargs="+", default=[])
|
||||
PARSER.add_argument('--exe-suffix')
|
||||
PARSER.add_argument('--depfile')
|
||||
PARSER.add_argument('--disable-doc', action="store_true", default=False)
|
||||
|
||||
|
||||
def shlex_join(args):
|
||||
if hasattr(shlex, 'join'):
|
||||
return shlex.join(args)
|
||||
return ' '.join([shlex.quote(arg) for arg in args])
|
||||
|
||||
|
||||
def generate_depfile_for(fpath):
|
||||
file_stem = fpath.parent / fpath.stem
|
||||
depfile_content = ""
|
||||
with open(f"{file_stem}.d", 'r') as depfile:
|
||||
for l in depfile.readlines():
|
||||
if l.startswith(str(file_stem)):
|
||||
# We can't blindly split on `:` because on Windows that's part
|
||||
# of the drive letter. Lucky for us, the format of the dep file
|
||||
# is one of:
|
||||
#
|
||||
# /path/to/output: /path/to/src1 /path/to/src2
|
||||
# /path/to/output:
|
||||
#
|
||||
# So we parse these two cases specifically
|
||||
if l.endswith(':'):
|
||||
output = l[:-1]
|
||||
srcs = ''
|
||||
else:
|
||||
output, srcs = l.split(": ", maxsplit=2)
|
||||
|
||||
all_deps = []
|
||||
for src in srcs.split(" "):
|
||||
all_deps.append(src)
|
||||
src = P(src)
|
||||
if src.name == 'lib.rs':
|
||||
# `rustc` doesn't take `Cargo.toml` into account
|
||||
# but we need to
|
||||
cargo_toml = src.parent.parent / 'Cargo.toml'
|
||||
if cargo_toml.exists():
|
||||
all_deps.append(str(cargo_toml))
|
||||
|
||||
depfile_content += f"{output}: {' '.join(all_deps)}\n"
|
||||
|
||||
return depfile_content
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
opts = PARSER.parse_args()
|
||||
logdir = opts.root_dir / 'meson-logs'
|
||||
logfile_path = logdir / f'{opts.src_dir.name}-cargo-wrapper.log'
|
||||
logfile = open(logfile_path, mode='w', buffering=1)
|
||||
|
||||
print(opts, file=logfile)
|
||||
cargo_target_dir = opts.build_dir / 'target'
|
||||
|
||||
env = os.environ.copy()
|
||||
if 'PKG_CONFIG_PATH' in env:
|
||||
pkg_config_path = env['PKG_CONFIG_PATH'].split(os.pathsep)
|
||||
else:
|
||||
pkg_config_path = []
|
||||
pkg_config_path.append(str(opts.root_dir / 'meson-uninstalled'))
|
||||
env['PKG_CONFIG_PATH'] = os.pathsep.join(pkg_config_path)
|
||||
|
||||
if 'NASM' in env:
|
||||
env['PATH'] = os.pathsep.join([os.path.dirname(env['NASM']), env['PATH']])
|
||||
|
||||
rustc_target = None
|
||||
if 'RUSTC' in env:
|
||||
rustc_cmdline = shlex.split(env['RUSTC'])
|
||||
# grab target from RUSTFLAGS
|
||||
rust_flags = rustc_cmdline[1:] + shlex.split(env.get('RUSTFLAGS', ''))
|
||||
if '--target' in rust_flags:
|
||||
rustc_target_idx = rust_flags.index('--target')
|
||||
_ = rust_flags.pop(rustc_target_idx) # drop '--target'
|
||||
rustc_target = rust_flags.pop(rustc_target_idx)
|
||||
env['RUSTFLAGS'] = shlex_join(rust_flags)
|
||||
env['RUSTC'] = rustc_cmdline[0]
|
||||
|
||||
features = opts.features
|
||||
if opts.command == 'build':
|
||||
cargo_cmd = ['cargo']
|
||||
if opts.bin or opts.examples:
|
||||
cargo_cmd += ['build']
|
||||
else:
|
||||
cargo_cmd += ['cbuild']
|
||||
if not opts.disable_doc:
|
||||
features += ['doc']
|
||||
if opts.target == 'release':
|
||||
cargo_cmd.append('--release')
|
||||
elif opts.command == 'test':
|
||||
# cargo test
|
||||
cargo_cmd = ['cargo', 'ctest', '--no-fail-fast', '--color=always']
|
||||
else:
|
||||
print("Unknown command:", opts.command, file=logfile)
|
||||
sys.exit(1)
|
||||
|
||||
if rustc_target:
|
||||
cargo_cmd += ['--target', rustc_target]
|
||||
if features:
|
||||
cargo_cmd += ['--features', ','.join(features)]
|
||||
cargo_cmd += ['--target-dir', cargo_target_dir]
|
||||
cargo_cmd += ['--manifest-path', opts.src_dir / 'Cargo.toml']
|
||||
if opts.bin:
|
||||
cargo_cmd.extend(['--bin', opts.bin.name])
|
||||
else:
|
||||
if not opts.examples:
|
||||
cargo_cmd.extend(['--prefix', opts.prefix, '--libdir',
|
||||
opts.prefix / opts.libdir])
|
||||
for p in opts.packages:
|
||||
cargo_cmd.extend(['-p', p])
|
||||
for e in opts.examples:
|
||||
cargo_cmd.extend(['--example', e])
|
||||
|
||||
def run(cargo_cmd, env):
|
||||
print(cargo_cmd, env, file=logfile)
|
||||
try:
|
||||
subprocess.run(cargo_cmd, env=env, cwd=opts.src_dir, check=True)
|
||||
except subprocess.SubprocessError:
|
||||
sys.exit(1)
|
||||
|
||||
run(cargo_cmd, env)
|
||||
|
||||
if opts.command == 'build':
|
||||
target_dir = cargo_target_dir / '**' / opts.target
|
||||
if opts.bin:
|
||||
exe = glob.glob(str(target_dir / opts.bin) + opts.exe_suffix, recursive=True)[0]
|
||||
shutil.copy2(exe, opts.build_dir)
|
||||
depfile_content = generate_depfile_for(P(exe))
|
||||
else:
|
||||
# Copy so files to build dir
|
||||
depfile_content = ""
|
||||
for suffix in opts.lib_suffixes:
|
||||
for f in glob.glob(str(target_dir / f'*.{suffix}'), recursive=True):
|
||||
libfile = P(f)
|
||||
|
||||
depfile_content += generate_depfile_for(libfile)
|
||||
|
||||
copied_file = (opts.build_dir / libfile.name)
|
||||
try:
|
||||
if copied_file.stat().st_mtime == libfile.stat().st_mtime:
|
||||
print(f"{copied_file} has not changed.", file=logfile)
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
print(f"Copying {copied_file}", file=logfile)
|
||||
shutil.copy2(f, opts.build_dir)
|
||||
# Copy examples to builddir
|
||||
for example in opts.examples:
|
||||
example_glob = str(target_dir / 'examples' / example) + opts.exe_suffix
|
||||
exe = glob.glob(example_glob, recursive=True)[0]
|
||||
shutil.copy2(exe, opts.build_dir)
|
||||
depfile_content += generate_depfile_for(P(exe))
|
||||
|
||||
with open(opts.depfile, 'w') as depfile:
|
||||
depfile.write(depfile_content)
|
||||
|
||||
# Copy generated pkg-config files
|
||||
for f in glob.glob(str(target_dir / '*.pc'), recursive=True):
|
||||
shutil.copy(f, opts.build_dir)
|
||||
|
||||
# Move -uninstalled.pc to meson-uninstalled
|
||||
uninstalled = opts.build_dir / 'meson-uninstalled'
|
||||
os.makedirs(uninstalled, exist_ok=True)
|
||||
|
||||
for f in opts.build_dir.glob('*-uninstalled.pc'):
|
||||
# move() does not allow us to update the file so remove it if it already exists
|
||||
dest = uninstalled / P(f).name
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
# move() takes paths from Python3.9 on
|
||||
if ((sys.version_info.major >= 3) and (sys.version_info.minor >= 9)):
|
||||
shutil.move(f, uninstalled)
|
||||
else:
|
||||
shutil.move(str(f), str(uninstalled))
|
||||
|
103
ci/cerbero/trigger_cerbero_pipeline.py
Executable file
103
ci/cerbero/trigger_cerbero_pipeline.py
Executable file
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/python3
|
||||
#
|
||||
# Copied from gstreamer.git/ci/gitlab/trigger_cerbero_pipeline.py
|
||||
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import gitlab
|
||||
|
||||
CERBERO_PROJECT = 'gstreamer/cerbero'
|
||||
|
||||
|
||||
class Status:
|
||||
FAILED = 'failed'
|
||||
MANUAL = 'manual'
|
||||
CANCELED = 'canceled'
|
||||
SUCCESS = 'success'
|
||||
SKIPPED = 'skipped'
|
||||
CREATED = 'created'
|
||||
|
||||
@classmethod
|
||||
def is_finished(cls, state):
|
||||
return state in [
|
||||
cls.FAILED,
|
||||
cls.MANUAL,
|
||||
cls.CANCELED,
|
||||
cls.SUCCESS,
|
||||
cls.SKIPPED,
|
||||
]
|
||||
|
||||
|
||||
def fprint(msg):
|
||||
print(msg, end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = os.environ['CI_SERVER_URL']
|
||||
gl = gitlab.Gitlab(server,
|
||||
private_token=os.environ.get('GITLAB_API_TOKEN'),
|
||||
job_token=os.environ.get('CI_JOB_TOKEN'))
|
||||
|
||||
def get_matching_user_project(project, branch):
|
||||
cerbero = gl.projects.get(project)
|
||||
# Search for matching branches, return only if the branch name matches
|
||||
# exactly
|
||||
for b in cerbero.branches.list(search=cerbero_branch, iterator=True):
|
||||
if branch == b.name:
|
||||
return cerbero
|
||||
return None
|
||||
|
||||
cerbero = None
|
||||
# We do not want to run on (often out of date) user upstream branch
|
||||
if os.environ["CI_COMMIT_REF_NAME"] != os.environ['CERBERO_UPSTREAM_BRANCH']:
|
||||
try:
|
||||
cerbero_name = f'{os.environ["CI_PROJECT_NAMESPACE"]}/cerbero'
|
||||
cerbero_branch = os.environ["CI_COMMIT_REF_NAME"]
|
||||
cerbero = get_matching_user_project(cerbero_name, cerbero_branch)
|
||||
except gitlab.exceptions.GitlabGetError:
|
||||
pass
|
||||
|
||||
if cerbero is None:
|
||||
cerbero_name = CERBERO_PROJECT
|
||||
cerbero_branch = os.environ["CERBERO_UPSTREAM_BRANCH"]
|
||||
cerbero = gl.projects.get(cerbero_name)
|
||||
|
||||
fprint(f"-> Triggering on branch {cerbero_branch} in {cerbero_name}\n")
|
||||
|
||||
# CI_PROJECT_URL is not necessarily the project where the branch we need to
|
||||
# build resides, for instance merge request pipelines can be run on
|
||||
# 'gstreamer' namespace. Fetch the branch name in the same way, just in
|
||||
# case it breaks in the future.
|
||||
if 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' in os.environ:
|
||||
project_url = os.environ['CI_MERGE_REQUEST_SOURCE_PROJECT_URL']
|
||||
project_branch = os.environ['CI_MERGE_REQUEST_SOURCE_BRANCH_NAME']
|
||||
else:
|
||||
project_url = os.environ['CI_PROJECT_URL']
|
||||
project_branch = os.environ['CI_COMMIT_REF_NAME']
|
||||
|
||||
variables = {
|
||||
"CI_GST_PLUGINS_RS_URL": project_url,
|
||||
"CI_GST_PLUGINS_RS_REF_NAME": project_branch,
|
||||
# This tells cerbero CI that this is a pipeline started via the
|
||||
# trigger API, which means it can use a deps cache instead of
|
||||
# building from scratch.
|
||||
"CI_GSTREAMER_TRIGGERED": "true",
|
||||
}
|
||||
|
||||
pipe = cerbero.trigger_pipeline(
|
||||
token=os.environ['CI_JOB_TOKEN'],
|
||||
ref=cerbero_branch,
|
||||
variables=variables,
|
||||
)
|
||||
|
||||
fprint(f'Cerbero pipeline running at {pipe.web_url} ')
|
||||
while True:
|
||||
time.sleep(15)
|
||||
pipe.refresh()
|
||||
if Status.is_finished(pipe.status):
|
||||
fprint(f": {pipe.status}\n")
|
||||
sys.exit(0 if pipe.status == Status.SUCCESS else 1)
|
||||
else:
|
||||
fprint(".")
|
20
ci/check-documentation-diff.py
Executable file
20
ci/check-documentation-diff.py
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/python3
|
||||
import os, subprocess, sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
diffsdir = 'plugins-cache-diffs'
|
||||
os.makedirs(diffsdir, exist_ok=True)
|
||||
res = 0
|
||||
try:
|
||||
subprocess.check_call(['git', 'diff', '--quiet'] )
|
||||
except subprocess.CalledProcessError:
|
||||
diffname = os.path.join(diffsdir, 'plugins_cache.diff')
|
||||
res += 1
|
||||
with open(diffname, 'w') as diff:
|
||||
subprocess.check_call(['git', 'diff'], stdout=diff)
|
||||
print('\033[91mYou have a diff in the documentation cache. Please update with:\033[0m')
|
||||
print(' $ curl %s/%s | git apply -' % (os.environ['CI_ARTIFACTS_URL'], diffname.replace('../', '')))
|
||||
|
||||
if res != 0:
|
||||
print('(note that it might take a few minutes for artefacts to be available on the server)\n')
|
||||
sys.exit(res)
|
12
ci/check-for-symlinks.sh
Executable file
12
ci/check-for-symlinks.sh
Executable file
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
LINKS=`find -type l`
|
||||
if test -z $LINKS; then
|
||||
echo "No symlinks found."
|
||||
else
|
||||
echo "===> FOUND SYMLINKS!"
|
||||
echo
|
||||
echo "$LINKS"
|
||||
echo
|
||||
echo "Please replace these with actual files."
|
||||
exit 1;
|
||||
fi
|
31
ci/check-installed.py
Executable file
31
ci/check-installed.py
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python3
|
||||
# Check that all available plugins have been build and installed in the prefix
|
||||
# directory passed in argument.
|
||||
import sys
|
||||
import os
|
||||
import glob
|
||||
|
||||
from utils import iterate_plugins
|
||||
|
||||
prefix = sys.argv[1]
|
||||
|
||||
plugins = glob.glob(os.path.join(
|
||||
prefix, '**', 'gstreamer-1.0', '*.so'), recursive=True)
|
||||
plugins = list(map(os.path.basename, plugins))
|
||||
print("Built plugins:", plugins)
|
||||
|
||||
success = True
|
||||
|
||||
for name in iterate_plugins():
|
||||
plugin = "libgst{}.so".format(name)
|
||||
|
||||
if plugin not in plugins:
|
||||
print(name, "missing in", prefix)
|
||||
success = False
|
||||
|
||||
if len(glob.glob(os.path.join(prefix, '**', 'bin', 'gst-webrtc-signalling-server'), recursive=True)) != 1:
|
||||
print("gst-webrtc-signalling-serverm is missing in", prefix)
|
||||
success = False
|
||||
|
||||
if not success:
|
||||
sys.exit(1)
|
14
ci/check-meson-version.sh
Executable file
14
ci/check-meson-version.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
MESON_VERSION=`head -n5 meson.build | grep ' version\s*:' | sed -e "s/.*version\s*:\s*'//" -e "s/',.*//"`
|
||||
CARGO_VERSION=`cat Cargo.toml | grep -A1 workspace.package | grep ^version | sed -e 's/^version = "\(.*\)"/\1/'`
|
||||
|
||||
echo "gst-plugins-rs version (meson.build) : $MESON_VERSION"
|
||||
echo "gst-plugins-rs version (Cargo.toml) : $CARGO_VERSION"
|
||||
|
||||
if test "x$MESON_VERSION" != "x$CARGO_VERSION"; then
|
||||
echo
|
||||
echo "===> Version mismatch between meson.build and Cargo.toml! <==="
|
||||
echo
|
||||
exit 1;
|
||||
fi
|
8
ci/env.sh
Normal file
8
ci/env.sh
Normal file
|
@ -0,0 +1,8 @@
|
|||
export RUSTUP_HOME='/usr/local/rustup'
|
||||
export PATH=$PATH:/usr/local/cargo/bin
|
||||
|
||||
export PKG_CONFIG_PATH=/usr/local/lib/x86_64-linux-gnu/pkgconfig
|
||||
export GST_PLUGIN_SYSTEM_PATH=/usr/local/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||
export GST_PLUGIN_SCANNER=/usr/local/libexec/gstreamer-1.0/gst-plugin-scanner
|
||||
export PATH=$PATH:/usr/local/bin
|
||||
export LD_LIBRARY_PATH=/usr/local/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
|
72
ci/generate-static-test.py
Executable file
72
ci/generate-static-test.py
Executable file
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env python3
|
||||
# Generate a meson project statically linking on all plugins
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from utils import iterate_plugins
|
||||
|
||||
# the csound version used on ci does not ship a .pc file
|
||||
# threadshare we skip in meson static build as well
|
||||
IGNORE = ['csound', 'threadshare', 'gtk4']
|
||||
|
||||
outdir = sys.argv[1]
|
||||
|
||||
plugins = list(filter(lambda p: p not in IGNORE, iterate_plugins()))
|
||||
deps = list(
|
||||
map(lambda p: " dependency('gst{}', static: true)".format(p), plugins))
|
||||
deps = ',\n'.join(deps)
|
||||
|
||||
meson = """
|
||||
project('test-gst-plugins-rs-static', 'c')
|
||||
|
||||
gst_deps = [
|
||||
dependency('gstreamer-1.0'),
|
||||
%s
|
||||
]
|
||||
|
||||
executable('test-gst-static', ['main.c'],
|
||||
dependencies: gst_deps,
|
||||
)
|
||||
""" % (deps)
|
||||
|
||||
declare = list(
|
||||
map(lambda p: "GST_PLUGIN_STATIC_DECLARE({});".format(p), plugins))
|
||||
declare = '\n'.join(declare)
|
||||
|
||||
register = list(
|
||||
map(lambda p: "\tGST_PLUGIN_STATIC_REGISTER({});".format(p), plugins))
|
||||
register = '\n'.join(register)
|
||||
|
||||
check = list(
|
||||
map(lambda p: "\tg_assert (gst_registry_find_plugin(registry, \"{}\"));".format(p), plugins))
|
||||
check = '\n'.join(check)
|
||||
|
||||
main = """
|
||||
#include <gst/gst.h>
|
||||
|
||||
%s
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_autoptr(GstRegistry) registry = NULL;
|
||||
|
||||
gst_init(&argc, &argv);
|
||||
|
||||
%s
|
||||
|
||||
registry = gst_registry_get();
|
||||
|
||||
%s
|
||||
|
||||
return 0;
|
||||
}
|
||||
""" % (declare, register, check)
|
||||
|
||||
os.makedirs(outdir)
|
||||
|
||||
meson_file = open(os.path.join(outdir, 'meson.build'), 'w')
|
||||
meson_file.write(meson)
|
||||
|
||||
main_file = open(os.path.join(outdir, 'main.c'), 'w')
|
||||
main_file.write(main)
|
11
ci/install-dav1d.sh
Normal file
11
ci/install-dav1d.sh
Normal file
|
@ -0,0 +1,11 @@
|
|||
set -e
|
||||
|
||||
RELEASE=1.1.0
|
||||
|
||||
git clone https://code.videolan.org/videolan/dav1d.git --branch $RELEASE
|
||||
cd dav1d
|
||||
meson build -D prefix=/usr/local
|
||||
ninja -C build
|
||||
ninja -C build install
|
||||
cd ..
|
||||
rm -rf dav1d
|
6
ci/install-rust-ext.sh
Executable file
6
ci/install-rust-ext.sh
Executable file
|
@ -0,0 +1,6 @@
|
|||
source ./ci/env.sh
|
||||
|
||||
set -e
|
||||
export CARGO_HOME='/usr/local/cargo'
|
||||
|
||||
cargo install cargo-c --version 0.9.15+cargo-0.67
|
50
ci/run_windows_tests.ps1
Normal file
50
ci/run_windows_tests.ps1
Normal file
|
@ -0,0 +1,50 @@
|
|||
$env:ErrorActionPreference='Stop'
|
||||
|
||||
[string[]] $exclude_crates = @(
|
||||
"--exclude",
|
||||
"gst-plugin-csound",
|
||||
"--exclude",
|
||||
"gst-plugin-webp"
|
||||
)
|
||||
|
||||
[string[]] $features_matrix = @(
|
||||
"--no-default-features",
|
||||
"",
|
||||
"--all-features"
|
||||
)
|
||||
|
||||
function Run-Tests {
|
||||
param (
|
||||
$Features
|
||||
)
|
||||
$local_exclude = $exclude_crates;
|
||||
|
||||
# In this case the plugin will pull x11/wayland features
|
||||
# which will fail to build on windows.
|
||||
if (($Features -eq '--all-features') -or ($Features -eq '')) {
|
||||
$local_exclude += @("--exclude", "gst-plugin-gtk4")
|
||||
}
|
||||
|
||||
Write-Host "Features: $Features"
|
||||
Write-Host "Exclude string: $local_exclude"
|
||||
|
||||
cargo build --color=always --workspace $local_exclude --all-targets $Features
|
||||
|
||||
if (!$?) {
|
||||
Write-Host "Build failed"
|
||||
Exit 1
|
||||
}
|
||||
|
||||
$env:G_DEBUG="fatal_warnings"
|
||||
$env:RUST_BACKTRACE="1"
|
||||
cargo test --no-fail-fast --color=always --workspace $local_exclude --all-targets $Features
|
||||
|
||||
if (!$?) {
|
||||
Write-Host "Tests failed"
|
||||
Exit 1
|
||||
}
|
||||
}
|
||||
|
||||
foreach($feature in $features_matrix) {
|
||||
Run-Tests -Features $feature
|
||||
}
|
43
ci/utils.py
Normal file
43
ci/utils.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import os
|
||||
|
||||
DIRS = [
|
||||
'audio',
|
||||
'generic',
|
||||
'mux',
|
||||
'net',
|
||||
'text',
|
||||
'utils',
|
||||
'video',
|
||||
]
|
||||
|
||||
# Plugins whose name is prefixed by 'rs'
|
||||
RS_PREFIXED = [
|
||||
'audiofx',
|
||||
'closedcaption',
|
||||
'file',
|
||||
'onvif',
|
||||
'webp',
|
||||
'videofx',
|
||||
'webrtc',
|
||||
'png',
|
||||
'tracers',
|
||||
'rtp',
|
||||
'rtsp',
|
||||
'inter',
|
||||
]
|
||||
|
||||
OVERRIDE = {
|
||||
'ahead': 'textahead',
|
||||
'flavors': 'rsflv',
|
||||
'wrap': 'textwrap',
|
||||
}
|
||||
|
||||
|
||||
def iterate_plugins():
|
||||
for d in DIRS:
|
||||
for name in os.listdir(d):
|
||||
if name in RS_PREFIXED:
|
||||
name = "rs{}".format(name)
|
||||
else:
|
||||
name = OVERRIDE.get(name, name)
|
||||
yield name
|
265
deny.toml
Normal file
265
deny.toml
Normal file
|
@ -0,0 +1,265 @@
|
|||
[advisories]
|
||||
db-path = "~/.cargo/advisory-db"
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
vulnerability = "deny"
|
||||
unmaintained = "warn"
|
||||
notice = "warn"
|
||||
ignore = [
|
||||
# Waiting for https://github.com/librespot-org/librespot/issues/937
|
||||
"RUSTSEC-2021-0059",
|
||||
"RUSTSEC-2021-0060",
|
||||
"RUSTSEC-2021-0061",
|
||||
"RUSTSEC-2021-0145",
|
||||
# sodiumoxide is deprecated
|
||||
"RUSTSEC-2021-0137",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
unlicensed = "deny"
|
||||
allow = [
|
||||
"MPL-2.0",
|
||||
]
|
||||
default = "deny"
|
||||
copyleft = "deny"
|
||||
allow-osi-fsf-free = "either"
|
||||
confidence-threshold = 0.8
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
version = "*"
|
||||
expression = "OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
]
|
||||
|
||||
# Allow AGPL3 from dssim-core, which is optionally used in gst-plugin-videofx
|
||||
[[licenses.exceptions]]
|
||||
allow = ["AGPL-3.0"]
|
||||
name = "dssim-core"
|
||||
version = "3.2"
|
||||
|
||||
# Allow LGPL 2.1 for the threadshare plugin as it includes some LGPL code
|
||||
[[licenses.exceptions]]
|
||||
allow = ["LGPL-2.1"]
|
||||
name = "gst-plugin-threadshare"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "deny"
|
||||
highlight = "all"
|
||||
wildcards = "allow"
|
||||
|
||||
# ignore duplicated crc dependency because ffv1 depends on an old version
|
||||
# https://github.com/rust-av/ffv1/issues/21
|
||||
[[bans.skip]]
|
||||
name = "crc"
|
||||
version = "1.8"
|
||||
|
||||
# Ignore various duplicated dependencies because librespot depends on an old versions
|
||||
[[bans.skip]]
|
||||
name = "block-buffer"
|
||||
version = "0.9"
|
||||
[[bans.skip]]
|
||||
name = "digest"
|
||||
version = "0.9"
|
||||
[[bans.skip]]
|
||||
name = "sha-1"
|
||||
version = "0.9"
|
||||
[[bans.skip]]
|
||||
name = "env_logger"
|
||||
version = "0.9"
|
||||
[[bans.skip]]
|
||||
name = "hmac"
|
||||
version = "0.11"
|
||||
[[bans.skip]]
|
||||
name = "zerocopy"
|
||||
version = "0.6"
|
||||
[[bans.skip]]
|
||||
name = "multimap"
|
||||
version = "0.8"
|
||||
[[bans.skip]]
|
||||
name = "nix"
|
||||
version = "0.23"
|
||||
|
||||
# field-offset and nix depend on an older memoffset
|
||||
# https://github.com/Diggsey/rust-field-offset/pull/23
|
||||
# https://github.com/nix-rust/nix/pull/1885
|
||||
[[bans.skip]]
|
||||
name = "memoffset"
|
||||
version = "0.6"
|
||||
|
||||
# Various crates depend on an older version of hermit-abi
|
||||
[[bans.skip]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1"
|
||||
|
||||
# Various crates depend on an older version of base64
|
||||
[[bans.skip]]
|
||||
name = "base64"
|
||||
version = "0.13"
|
||||
[[bans.skip]]
|
||||
name = "base64"
|
||||
version = "0.21"
|
||||
|
||||
# Various crates depend on an older version of socket2
|
||||
[[bans.skip]]
|
||||
name = "socket2"
|
||||
version = "0.4"
|
||||
|
||||
# Various crates depend on an older version of bitflags
|
||||
[[bans.skip]]
|
||||
name = "bitflags"
|
||||
version = "1.0"
|
||||
|
||||
# tracing-subscriber depends on an older version of regex-syntax
|
||||
[[bans.skip]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6"
|
||||
|
||||
# publicsuffix depends on an older version of idna
|
||||
# https://github.com/rushmorem/publicsuffix/pull/39
|
||||
[[bans.skip]]
|
||||
name = "idna"
|
||||
version = "0.3"
|
||||
|
||||
# Various crates depend on an older version of indexmap / hashbrown
|
||||
[[bans.skip]]
|
||||
name = "indexmap"
|
||||
version = "1.0"
|
||||
[[bans.skip]]
|
||||
name = "hashbrown"
|
||||
version = "0.12"
|
||||
|
||||
# various livekit dependencies depend on an old version of itertools
|
||||
[[bans.skip]]
|
||||
name = "itertools"
|
||||
version = "0.11"
|
||||
|
||||
# various rav1e / dssim-core depend on an old version of itertools
|
||||
[[bans.skip]]
|
||||
name = "itertools"
|
||||
version = "0.12"
|
||||
|
||||
# matchers depends on an old version of regex-automata
|
||||
[[bans.skip]]
|
||||
name = "regex-automata"
|
||||
version = "0.1"
|
||||
|
||||
# Various crates depend on old versions of the windows crates
|
||||
[[bans.skip]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48"
|
||||
[[bans.skip]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48"
|
||||
[[bans.skip]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48"
|
||||
[[bans.skip]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48"
|
||||
[[bans.skip]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48"
|
||||
[[bans.skip]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48"
|
||||
[[bans.skip]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48"
|
||||
[[bans.skip]]
|
||||
name = "windows-targets"
|
||||
version = "0.48"
|
||||
[[bans.skip]]
|
||||
name = "windows-sys"
|
||||
version = "0.48"
|
||||
|
||||
# Various crates depend on an older version of crypto-bigint
|
||||
[[bans.skip]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.4"
|
||||
|
||||
# livekit-api depends on an older version of tokio-tungstenite
|
||||
[[bans.skip]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.20"
|
||||
[[bans.skip]]
|
||||
name = "tungstenite"
|
||||
version = "0.20"
|
||||
|
||||
# Various crates depend on an older version of http
|
||||
[[bans.skip]]
|
||||
name = "http"
|
||||
version = "0.2"
|
||||
|
||||
# proc-macro-crate depends on an older version of toml_edit
|
||||
# https://github.com/bkchr/proc-macro-crate/pull/50
|
||||
[[bans.skip]]
|
||||
name = "toml_edit"
|
||||
version = "0.21"
|
||||
[[bans.skip]]
|
||||
name = "winnow"
|
||||
version = "0.5"
|
||||
|
||||
# Various crates depend on an older version of heck
|
||||
[[bans.skip]]
|
||||
name = "heck"
|
||||
version = "0.4"
|
||||
|
||||
# Various crates depend on an older version of hyper / reqwest / headers / etc
|
||||
[[bans.skip]]
|
||||
name = "hyper"
|
||||
version = "0.14"
|
||||
[[bans.skip]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5"
|
||||
[[bans.skip]]
|
||||
name = "http-body"
|
||||
version = "0.4"
|
||||
[[bans.skip]]
|
||||
name = "headers-core"
|
||||
version = "0.2"
|
||||
[[bans.skip]]
|
||||
name = "headers"
|
||||
version = "0.3"
|
||||
[[bans.skip]]
|
||||
name = "h2"
|
||||
version = "0.3"
|
||||
[[bans.skip]]
|
||||
name = "reqwest"
|
||||
version = "0.11"
|
||||
[[bans.skip]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0"
|
||||
[[bans.skip]]
|
||||
name = "winreg"
|
||||
version = "0.50"
|
||||
|
||||
# The AWS SDK uses old versions of rustls and related crates
|
||||
[[bans.skip]]
|
||||
name = "rustls"
|
||||
version = "0.21"
|
||||
[[bans.skip]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6"
|
||||
[[bans.skip]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101"
|
||||
|
||||
# warp depends on an older version of tokio-tungstenite
|
||||
[[bans.skip]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.21"
|
||||
[[bans.skip]]
|
||||
name = "tungstenite"
|
||||
version = "0.21"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://gitlab.freedesktop.org/gstreamer/gstreamer-rs",
|
||||
"https://github.com/gtk-rs/gtk-rs-core",
|
||||
"https://github.com/gtk-rs/gtk4-rs",
|
||||
"https://github.com/rust-av/ffv1",
|
||||
"https://github.com/rust-av/flavors",
|
||||
]
|
114
dependencies.py
Executable file
114
dependencies.py
Executable file
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Parse Cargo.toml files for each plugin to collect their external dependencies.
|
||||
# Meson will lookup those dependencies using pkg-config to be able to link
|
||||
# static Rust plugins into gst-full.
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
try:
|
||||
# Python11 stdlib
|
||||
import tomllib
|
||||
except ImportError:
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
PARSER = ArgumentParser()
|
||||
PARSER.add_argument('src_dir', type=Path)
|
||||
PARSER.add_argument('plugins', nargs='*')
|
||||
PARSER.add_argument('--features', action="store_true", help="Get list of features to activate")
|
||||
PARSER.add_argument('--gst-version', help="Get list of features to activate")
|
||||
|
||||
|
||||
# Map plugin name to directory name, for those that does not match.
|
||||
RENAMES = {
|
||||
'rsaudiofx': 'audiofx',
|
||||
'rsfile': 'file',
|
||||
'rsflv': 'flavors',
|
||||
'rsrtp': 'rtp',
|
||||
'rsrtsp': 'rtsp',
|
||||
'rswebp': 'webp',
|
||||
'rsonvif': 'onvif',
|
||||
'rstracers': 'tracers',
|
||||
'rsclosedcaption': 'closedcaption',
|
||||
'rswebrtc': 'webrtc',
|
||||
'rspng': 'png',
|
||||
'rsvideofx': 'videofx',
|
||||
'rsinter': 'inter',
|
||||
'textahead': 'ahead',
|
||||
'textwrap': 'wrap',
|
||||
}
|
||||
|
||||
class CargoAnalyzer:
|
||||
def __init__(self):
|
||||
self.src_dir = None
|
||||
self.plugins = None
|
||||
self.features = False
|
||||
self.gst_version = "1.18"
|
||||
|
||||
def extract_version(self, feature_name):
|
||||
if feature_name.startswith('v'):
|
||||
verindex = 1
|
||||
elif feature_name.startswith('gst'):
|
||||
verindex = 3
|
||||
else:
|
||||
return None
|
||||
|
||||
(majver, minver) = feature_name[verindex:].split("_")
|
||||
return (int(majver), int(minver))
|
||||
|
||||
def extract_features(self, cargo_data):
|
||||
features = cargo_data['features']
|
||||
wanted_features = set()
|
||||
gst_version_major = int(self.gst_version.split('.')[0])
|
||||
gst_version_minor = int(self.gst_version.split('.')[1])
|
||||
for (name, value) in features.items():
|
||||
version = self.extract_version(name)
|
||||
|
||||
if version is None:
|
||||
continue
|
||||
(majver, minver) = version
|
||||
|
||||
if gst_version_major < majver or gst_version_minor < minver:
|
||||
continue
|
||||
wanted_features |= set(value)
|
||||
if name.startswith("gst"):
|
||||
# Required for some reason for rswebrtc which has a specific feature
|
||||
wanted_features |= {f"{cargo_data['package']['name']}/{name}"}
|
||||
|
||||
return wanted_features
|
||||
|
||||
def run(self):
|
||||
with (opts.src_dir / 'Cargo.toml').open('rb') as f:
|
||||
crates = tomllib.load(f)['workspace']['members']
|
||||
res = set()
|
||||
for name in opts.plugins:
|
||||
if name.startswith('gst'):
|
||||
name = name[3:]
|
||||
|
||||
name = RENAMES.get(name, name)
|
||||
crate_path = None
|
||||
for crate in crates:
|
||||
if Path(crate).name == name:
|
||||
crate_path = opts.src_dir / crate / 'Cargo.toml'
|
||||
assert crate_path
|
||||
with crate_path.open('rb') as f:
|
||||
data = tomllib.load(f)
|
||||
if opts.features:
|
||||
res |= self.extract_features(data)
|
||||
else:
|
||||
try:
|
||||
requires = data['package']['metadata']['capi']['pkg_config']['requires_private']
|
||||
except KeyError:
|
||||
continue
|
||||
res.update([i.strip().replace('>', "|>").replace('<', "|<").replace("==", "|==") for i in requires.split(',')])
|
||||
return res
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyzer = CargoAnalyzer()
|
||||
opts = PARSER.parse_args(namespace=analyzer)
|
||||
|
||||
print(','.join(analyzer.run()))
|
123
docs/meson.build
Normal file
123
docs/meson.build
Normal file
|
@ -0,0 +1,123 @@
|
|||
build_hotdoc = false
|
||||
|
||||
if get_option('doc').disabled()
|
||||
subdir_done()
|
||||
endif
|
||||
|
||||
if meson.is_cross_build()
|
||||
if get_option('doc').enabled()
|
||||
error('Documentation enabled but building the doc while cross building is not supported yet.')
|
||||
endif
|
||||
|
||||
message('Documentation not built as building it while cross building is not supported yet.')
|
||||
subdir_done()
|
||||
endif
|
||||
|
||||
if default_library == 'static'
|
||||
if get_option('doc').enabled()
|
||||
error('Documentation enabled but not supported when building statically.')
|
||||
endif
|
||||
|
||||
message('Building statically, can\'t build the documentation')
|
||||
subdir_done()
|
||||
endif
|
||||
|
||||
required_hotdoc_extensions = ['gst-extension']
|
||||
if gst_dep.type_name() == 'internal'
|
||||
gst_proj = subproject('gstreamer')
|
||||
plugins_cache_generator = gst_proj.get_variable('plugins_cache_generator')
|
||||
else
|
||||
plugins_cache_generator = find_program(join_paths(gst_dep.get_variable('libexecdir'), 'gstreamer-1.0' , 'gst-plugins-doc-cache-generator'),
|
||||
required: false)
|
||||
if not plugins_cache_generator.found()
|
||||
plugins_cache_generator = find_program('gst-plugins-doc-cache-generator', required: false)
|
||||
endif
|
||||
endif
|
||||
libs_doc = []
|
||||
plugins_cache = join_paths(meson.current_source_dir(), 'plugins', 'gst_plugins_cache.json')
|
||||
if plugins.length() == 0
|
||||
message('All base plugins have been disabled')
|
||||
elif plugins_cache_generator.found()
|
||||
plugins_paths = []
|
||||
foreach plugin: plugins
|
||||
plugins_paths += [plugin.full_path()]
|
||||
endforeach
|
||||
# We do not let gstreamer update our cache
|
||||
_plugins_doc_dep = custom_target('rs-plugins-doc-cache',
|
||||
command: [plugins_cache_generator, plugins_cache, '@OUTPUT@', plugins_paths],
|
||||
input: plugins,
|
||||
output: 'gst_plugins_cache.json',
|
||||
build_always_stale: true,
|
||||
)
|
||||
else
|
||||
warning('GStreamer plugin inspector for documentation not found, can\'t update the cache')
|
||||
endif
|
||||
|
||||
hotdoc_p = find_program('hotdoc', required: get_option('doc'))
|
||||
if not hotdoc_p.found()
|
||||
message('Hotdoc not found, not building the documentation')
|
||||
subdir_done()
|
||||
endif
|
||||
|
||||
hotdoc_req = '>= 0.11.0'
|
||||
hotdoc_version = run_command(hotdoc_p, '--version', check: false).stdout()
|
||||
if not hotdoc_version.version_compare(hotdoc_req)
|
||||
if get_option('doc').enabled()
|
||||
error('Hotdoc version @0@ not found, got @1@'.format(hotdoc_req, hotdoc_version))
|
||||
else
|
||||
message('Hotdoc version @0@ not found, got @1@'.format(hotdoc_req, hotdoc_version))
|
||||
subdir_done()
|
||||
endif
|
||||
endif
|
||||
|
||||
hotdoc = import('hotdoc')
|
||||
foreach extension: required_hotdoc_extensions
|
||||
if not hotdoc.has_extensions(extension)
|
||||
if get_option('doc').enabled()
|
||||
error('Documentation enabled but @0@ missing'.format(extension))
|
||||
endif
|
||||
|
||||
message('@0@ extension not found, not building documentation'.format(extension))
|
||||
subdir_done()
|
||||
endif
|
||||
endforeach
|
||||
|
||||
build_hotdoc = true
|
||||
plugins_doc = []
|
||||
|
||||
|
||||
list_plugin_res = run_command(python, '-c',
|
||||
'''
|
||||
import sys
|
||||
import json
|
||||
|
||||
with open("@0@") as f:
|
||||
print(':'.join(json.load(f).keys()), end='')
|
||||
'''.format(plugins_cache),
|
||||
check: true)
|
||||
foreach plugin_name: list_plugin_res.stdout().split(':')
|
||||
plugins_doc += [hotdoc.generate_doc(plugin_name,
|
||||
project_version: '1.0',
|
||||
sitemap: 'plugins/sitemap.txt',
|
||||
index: 'plugins/index.md',
|
||||
gst_index: 'plugins/index.md',
|
||||
include_paths: join_paths(meson.current_source_dir(), '..'),
|
||||
gst_smart_index: true,
|
||||
gst_c_source_filters: [
|
||||
'../target/*/*.rs',
|
||||
'../target/*/*/*.rs',
|
||||
'../target/*/*/*/*.rs',
|
||||
'../target/*/*/*/*/*.rs',
|
||||
'../target/*/*/*/*/*/*.rs',
|
||||
],
|
||||
gst_c_sources: [
|
||||
'../*/*/*/*.rs',
|
||||
'../*/*/*/*/*.rs',
|
||||
'../*/*/*/*/*/*.rs',
|
||||
],
|
||||
dependencies: [gst_dep],
|
||||
gst_order_generated_subpages: true,
|
||||
gst_cache_file: plugins_cache,
|
||||
gst_plugin_name: plugin_name,
|
||||
)]
|
||||
endforeach
|
5
docs/plugins/all_index.md
Normal file
5
docs/plugins/all_index.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
short-description: Plugins from gst-plugins-rs
|
||||
...
|
||||
|
||||
# Plugins
|
12653
docs/plugins/gst_plugins_cache.json
Normal file
12653
docs/plugins/gst_plugins_cache.json
Normal file
File diff suppressed because it is too large
Load diff
0
docs/plugins/index.md
Normal file
0
docs/plugins/index.md
Normal file
1
docs/plugins/sitemap.txt
Normal file
1
docs/plugins/sitemap.txt
Normal file
|
@ -0,0 +1 @@
|
|||
gst-index
|
42
generic/file/Cargo.toml
Normal file
42
generic/file/Cargo.toml
Normal file
|
@ -0,0 +1,42 @@
|
|||
[package]
|
||||
name = "gst-plugin-file"
|
||||
version.workspace = true
|
||||
authors = ["Sebastian Dröge <sebastian@centricular.com>"]
|
||||
repository.workspace = true
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "GStreamer Rust File Source/Sink Plugin"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
url = "2"
|
||||
gst.workspace = true
|
||||
gst-base.workspace = true
|
||||
once_cell.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "gstrsfile"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper.workspace = true
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
doc = ["gst/v1_18"]
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.9.21"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
import_library = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0"
|
201
generic/file/LICENSE-APACHE
Normal file
201
generic/file/LICENSE-APACHE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
23
generic/file/LICENSE-MIT
Normal file
23
generic/file/LICENSE-MIT
Normal file
|
@ -0,0 +1,23 @@
|
|||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
3
generic/file/build.rs
Normal file
3
generic/file/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
143
generic/file/src/file_location.rs
Normal file
143
generic/file/src/file_location.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
use gst::glib;
|
||||
use url::Url;
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const WIN_EXT_PATH_PREFIX: &str = "\\\\?\\";
|
||||
#[cfg(target_os = "windows")]
|
||||
const WIN_EXT_PATH_PREFIX_LEN: usize = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct FileLocation(PathBuf);
|
||||
|
||||
impl FileLocation {
|
||||
pub(super) fn try_from_path_str(path_str: String) -> Result<Self, glib::Error> {
|
||||
FileLocation::try_from(PathBuf::from(path_str))
|
||||
}
|
||||
|
||||
pub(super) fn try_from_uri_str(uri_str: &str) -> Result<Self, glib::Error> {
|
||||
match Url::parse(uri_str) {
|
||||
Ok(url) => {
|
||||
if url.scheme() != "file" {
|
||||
return Err(glib::Error::new(
|
||||
gst::URIError::UnsupportedProtocol,
|
||||
format!("Unsupported URI {uri_str}").as_str(),
|
||||
));
|
||||
}
|
||||
|
||||
let path = url.to_file_path().map_err(|_| {
|
||||
glib::Error::new(
|
||||
gst::URIError::BadUri,
|
||||
format!("Unsupported URI {uri_str}").as_str(),
|
||||
)
|
||||
})?;
|
||||
|
||||
FileLocation::try_from(path)
|
||||
}
|
||||
Err(err) => Err(glib::Error::new(
|
||||
gst::URIError::BadUri,
|
||||
format!("Couldn't parse URI {uri_str}: {err}").as_str(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_from(location: PathBuf) -> Result<Self, glib::Error> {
|
||||
let location_str = location.to_str().ok_or_else(|| {
|
||||
glib::Error::new(
|
||||
gst::URIError::BadReference,
|
||||
format!("Invalid path {location:?}").as_str(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let file_name = location.file_name().ok_or_else(|| {
|
||||
glib::Error::new(
|
||||
gst::URIError::BadReference,
|
||||
format!("Expected a path with a filename, got {location_str}",).as_str(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// The filename might not exist yet, so check the parent only.
|
||||
// Note: `location` contains a filename, so its parent can't be `None`
|
||||
let mut parent_dir = location
|
||||
.parent()
|
||||
.expect("FileSink::set_location `location` with filename but without a parent")
|
||||
.to_owned();
|
||||
if parent_dir.is_relative() && parent_dir.components().next().is_none() {
|
||||
// `location` only contains the filename
|
||||
// need to specify "." for `canonicalize` to resolve the actual path
|
||||
parent_dir = PathBuf::from(".");
|
||||
}
|
||||
|
||||
let parent_canonical = parent_dir.canonicalize().map_err(|err| {
|
||||
glib::Error::new(
|
||||
gst::URIError::BadReference,
|
||||
format!("Could not resolve path {location_str}: {err}",).as_str(),
|
||||
)
|
||||
})?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let parent_canonical = {
|
||||
let has_prefix = parent_canonical
|
||||
.to_str()
|
||||
.unwrap() // already checked above
|
||||
.starts_with(WIN_EXT_PATH_PREFIX);
|
||||
if has_prefix {
|
||||
// Remove the "extended length path" prefix
|
||||
// for compatibility with applications which can't deal with it.
|
||||
// See https://doc.rust-lang.org/std/fs/fn.canonicalize.html
|
||||
let parent_canonical_str = parent_canonical.to_str().unwrap();
|
||||
PathBuf::from(&parent_canonical_str[WIN_EXT_PATH_PREFIX_LEN..])
|
||||
} else {
|
||||
parent_canonical
|
||||
}
|
||||
};
|
||||
|
||||
let location_canonical = parent_canonical.join(file_name);
|
||||
Url::from_file_path(&location_canonical)
|
||||
.map_err(|_| {
|
||||
glib::Error::new(
|
||||
gst::URIError::BadReference,
|
||||
format!("Could not resolve path to URL {location_str}").as_str(),
|
||||
)
|
||||
})
|
||||
.map(|_| FileLocation(location_canonical))
|
||||
}
|
||||
|
||||
fn to_str(&self) -> &str {
|
||||
self.0
|
||||
.to_str()
|
||||
.expect("FileLocation: couldn't get `&str` from internal `PathBuf`")
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for FileLocation {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FileLocation {
|
||||
type Target = Path;
|
||||
|
||||
fn deref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FileLocation {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.to_str())
|
||||
}
|
||||
}
|
313
generic/file/src/filesink/imp.rs
Normal file
313
generic/file/src/filesink/imp.rs
Normal file
|
@ -0,0 +1,313 @@
|
|||
// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com>
|
||||
// 2016 Luis de Bethencourt <luisbg@osg.samsung.com>
|
||||
// 2018 François Laignel <fengalin@free.fr>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_base::prelude::*;
|
||||
use gst_base::subclass::prelude::*;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::file_location::FileLocation;
|
||||
|
||||
const DEFAULT_LOCATION: Option<FileLocation> = None;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Settings {
|
||||
location: Option<FileLocation>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
location: DEFAULT_LOCATION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
Stopped,
|
||||
Started {
|
||||
file: File,
|
||||
position: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FileSink {
|
||||
settings: Mutex<Settings>,
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"rsfilesink",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("File Sink"),
|
||||
)
|
||||
});
|
||||
|
||||
impl FileSink {
|
||||
fn set_location(&self, location: Option<FileLocation>) -> Result<(), glib::Error> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if let State::Started { .. } = *state {
|
||||
return Err(glib::Error::new(
|
||||
gst::URIError::BadState,
|
||||
"Changing the `location` property on a started `filesink` is not supported",
|
||||
));
|
||||
}
|
||||
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.location = match location {
|
||||
Some(location) => {
|
||||
match settings.location {
|
||||
Some(ref location_cur) => {
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Changing `location` from {:?} to {}",
|
||||
location_cur,
|
||||
location,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
gst::info!(CAT, imp: self, "Setting `location` to {}", location,);
|
||||
}
|
||||
}
|
||||
Some(location)
|
||||
}
|
||||
None => {
|
||||
gst::info!(CAT, imp: self, "Resetting `location` to None",);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for FileSink {
|
||||
const NAME: &'static str = "GstRsFileSink";
|
||||
type Type = super::FileSink;
|
||||
type ParentType = gst_base::BaseSink;
|
||||
type Interfaces = (gst::URIHandler,);
|
||||
}
|
||||
|
||||
impl ObjectImpl for FileSink {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.obj().set_sync(false);
|
||||
}
|
||||
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![glib::ParamSpecString::builder("location")
|
||||
.nick("File Location")
|
||||
.blurb("Location of the file to write")
|
||||
.build()]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"location" => {
|
||||
let res = match value.get::<Option<String>>() {
|
||||
Ok(Some(location)) => FileLocation::try_from_path_str(location)
|
||||
.and_then(|file_location| self.set_location(Some(file_location))),
|
||||
Ok(None) => self.set_location(None),
|
||||
Err(_) => unreachable!("type checked upstream"),
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
gst::error!(CAT, imp: self, "Failed to set property `location`: {}", err);
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"location" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
let location = settings
|
||||
.location
|
||||
.as_ref()
|
||||
.map(|location| location.to_string());
|
||||
|
||||
location.to_value()
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for FileSink {}
|
||||
|
||||
impl ElementImpl for FileSink {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"File Sink",
|
||||
"Sink/File",
|
||||
"Write stream to a file",
|
||||
"François Laignel <fengalin@free.fr>, Luis de Bethencourt <luisbg@osg.samsung.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let caps = gst::Caps::new_any();
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseSinkImpl for FileSink {
|
||||
fn start(&self) -> Result<(), gst::ErrorMessage> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if let State::Started { .. } = *state {
|
||||
unreachable!("FileSink already started");
|
||||
}
|
||||
|
||||
let settings = self.settings.lock().unwrap();
|
||||
let location = settings.location.as_ref().ok_or_else(|| {
|
||||
gst::error_msg!(
|
||||
gst::ResourceError::Settings,
|
||||
["File location is not defined"]
|
||||
)
|
||||
})?;
|
||||
|
||||
let file = File::create(location).map_err(|err| {
|
||||
gst::error_msg!(
|
||||
gst::ResourceError::OpenWrite,
|
||||
[
|
||||
"Could not open file {} for writing: {}",
|
||||
location,
|
||||
err.to_string(),
|
||||
]
|
||||
)
|
||||
})?;
|
||||
gst::debug!(CAT, imp: self, "Opened file {:?}", file);
|
||||
|
||||
*state = State::Started { file, position: 0 };
|
||||
gst::info!(CAT, imp: self, "Started");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if let State::Stopped = *state {
|
||||
return Err(gst::error_msg!(
|
||||
gst::ResourceError::Settings,
|
||||
["FileSink not started"]
|
||||
));
|
||||
}
|
||||
|
||||
*state = State::Stopped;
|
||||
gst::info!(CAT, imp: self, "Stopped");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: implement seek in BYTES format
|
||||
|
||||
fn render(&self, buffer: &gst::Buffer) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let (file, position) = match *state {
|
||||
State::Started {
|
||||
ref mut file,
|
||||
ref mut position,
|
||||
} => (file, position),
|
||||
State::Stopped => {
|
||||
gst::element_imp_error!(self, gst::CoreError::Failed, ["Not started yet"]);
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
};
|
||||
|
||||
gst::trace!(CAT, imp: self, "Rendering {:?}", buffer);
|
||||
let map = buffer.map_readable().map_err(|_| {
|
||||
gst::element_imp_error!(self, gst::CoreError::Failed, ["Failed to map buffer"]);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
file.write_all(map.as_ref()).map_err(|err| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::ResourceError::Write,
|
||||
["Failed to write buffer: {}", err]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
*position += map.len() as u64;
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
impl URIHandlerImpl for FileSink {
|
||||
const URI_TYPE: gst::URIType = gst::URIType::Sink;
|
||||
|
||||
fn protocols() -> &'static [&'static str] {
|
||||
&["file"]
|
||||
}
|
||||
|
||||
fn uri(&self) -> Option<String> {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
// Conversion to Url already checked while building the `FileLocation`
|
||||
settings.location.as_ref().map(|location| {
|
||||
Url::from_file_path(location)
|
||||
.expect("FileSink::get_uri couldn't build `Url` from `location`")
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn set_uri(&self, uri: &str) -> Result<(), glib::Error> {
|
||||
// Special case for "file://" as this is used by some applications to test
|
||||
// with `gst_element_make_from_uri` if there's an element that supports the URI protocol
|
||||
|
||||
if uri != "file://" {
|
||||
let file_location = FileLocation::try_from_uri_str(uri)?;
|
||||
self.set_location(Some(file_location))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
29
generic/file/src/filesink/mod.rs
Normal file
29
generic/file/src/filesink/mod.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com>
|
||||
// 2016 Luis de Bethencourt <luisbg@osg.samsung.com>
|
||||
// 2018 François Laignel <fengalin@free.fr>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct FileSink(ObjectSubclass<imp::FileSink>) @extends gst_base::BaseSink, gst::Element, gst::Object, @implements gst::URIHandler;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"rsfilesink",
|
||||
gst::Rank::NONE,
|
||||
FileSink::static_type(),
|
||||
)
|
||||
}
|
363
generic/file/src/filesrc/imp.rs
Normal file
363
generic/file/src/filesrc/imp.rs
Normal file
|
@ -0,0 +1,363 @@
|
|||
// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com>
|
||||
// 2018 François Laignel <fengalin@free.fr>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
use gst_base::prelude::*;
|
||||
use gst_base::subclass::prelude::*;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::file_location::FileLocation;
|
||||
|
||||
const DEFAULT_LOCATION: Option<FileLocation> = None;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Settings {
|
||||
location: Option<FileLocation>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
location: DEFAULT_LOCATION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
Stopped,
|
||||
Started {
|
||||
file: File,
|
||||
position: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FileSrc {
|
||||
settings: Mutex<Settings>,
|
||||
state: Mutex<State>,
|
||||
}
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"rsfilesrc",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("File Source"),
|
||||
)
|
||||
});
|
||||
|
||||
impl FileSrc {
|
||||
fn set_location(&self, location: Option<FileLocation>) -> Result<(), glib::Error> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if let State::Started { .. } = *state {
|
||||
return Err(glib::Error::new(
|
||||
gst::URIError::BadState,
|
||||
"Changing the `location` property on a started `filesrc` is not supported",
|
||||
));
|
||||
}
|
||||
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.location = match location {
|
||||
Some(location) => {
|
||||
if !location.exists() {
|
||||
return Err(glib::Error::new(
|
||||
gst::URIError::BadReference,
|
||||
format!("{location} doesn't exist").as_str(),
|
||||
));
|
||||
}
|
||||
|
||||
if !location.is_file() {
|
||||
return Err(glib::Error::new(
|
||||
gst::URIError::BadReference,
|
||||
format!("{location} is not a file").as_str(),
|
||||
));
|
||||
}
|
||||
|
||||
match settings.location {
|
||||
Some(ref location_cur) => {
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Changing `location` from {:?} to {}",
|
||||
location_cur,
|
||||
location,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
gst::info!(CAT, imp: self, "Setting `location to {}", location,);
|
||||
}
|
||||
}
|
||||
Some(location)
|
||||
}
|
||||
None => {
|
||||
gst::info!(CAT, imp: self, "Resetting `location` to None",);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for FileSrc {
|
||||
const NAME: &'static str = "GstRsFileSrc";
|
||||
type Type = super::FileSrc;
|
||||
type ParentType = gst_base::BaseSrc;
|
||||
type Interfaces = (gst::URIHandler,);
|
||||
}
|
||||
|
||||
impl ObjectImpl for FileSrc {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![glib::ParamSpecString::builder("location")
|
||||
.nick("File Location")
|
||||
.blurb("Location of the file to read from")
|
||||
.mutable_ready()
|
||||
.build()]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"location" => {
|
||||
let res = match value.get::<Option<String>>() {
|
||||
Ok(Some(location)) => FileLocation::try_from_path_str(location)
|
||||
.and_then(|file_location| self.set_location(Some(file_location))),
|
||||
Ok(None) => self.set_location(None),
|
||||
Err(_) => unreachable!("type checked upstream"),
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
gst::error!(CAT, imp: self, "Failed to set property `location`: {}", err);
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"location" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
let location = settings
|
||||
.location
|
||||
.as_ref()
|
||||
.map(|location| location.to_string());
|
||||
|
||||
location.to_value()
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.obj().set_format(gst::Format::Bytes);
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for FileSrc {}
|
||||
|
||||
impl ElementImpl for FileSrc {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"File Source",
|
||||
"Source/File",
|
||||
"Read stream from a file",
|
||||
"François Laignel <fengalin@free.fr>, Sebastian Dröge <sebastian@centricular.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
let caps = gst::Caps::new_any();
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseSrcImpl for FileSrc {
|
||||
fn is_seekable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn size(&self) -> Option<u64> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if let State::Started { ref file, .. } = *state {
|
||||
file.metadata().ok().map(|m| m.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self) -> Result<(), gst::ErrorMessage> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if let State::Started { .. } = *state {
|
||||
unreachable!("FileSrc already started");
|
||||
}
|
||||
|
||||
let settings = self.settings.lock().unwrap();
|
||||
let location = settings.location.as_ref().ok_or_else(|| {
|
||||
gst::error_msg!(
|
||||
gst::ResourceError::Settings,
|
||||
["File location is not defined"]
|
||||
)
|
||||
})?;
|
||||
|
||||
let file = File::open(location).map_err(|err| {
|
||||
gst::error_msg!(
|
||||
gst::ResourceError::OpenRead,
|
||||
[
|
||||
"Could not open file {} for reading: {}",
|
||||
location,
|
||||
err.to_string(),
|
||||
]
|
||||
)
|
||||
})?;
|
||||
|
||||
gst::debug!(CAT, imp: self, "Opened file {:?}", file);
|
||||
|
||||
*state = State::Started { file, position: 0 };
|
||||
|
||||
gst::info!(CAT, imp: self, "Started");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<(), gst::ErrorMessage> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if let State::Stopped = *state {
|
||||
return Err(gst::error_msg!(
|
||||
gst::ResourceError::Settings,
|
||||
["FileSrc not started"]
|
||||
));
|
||||
}
|
||||
|
||||
*state = State::Stopped;
|
||||
|
||||
gst::info!(CAT, imp: self, "Stopped");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fill(
|
||||
&self,
|
||||
offset: u64,
|
||||
_length: u32,
|
||||
buffer: &mut gst::BufferRef,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
let (file, position) = match *state {
|
||||
State::Started {
|
||||
ref mut file,
|
||||
ref mut position,
|
||||
} => (file, position),
|
||||
State::Stopped => {
|
||||
gst::element_imp_error!(self, gst::CoreError::Failed, ["Not started yet"]);
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
};
|
||||
|
||||
if *position != offset {
|
||||
file.seek(SeekFrom::Start(offset)).map_err(|err| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::LibraryError::Failed,
|
||||
["Failed to seek to {}: {}", offset, err.to_string()]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
*position = offset;
|
||||
}
|
||||
|
||||
let size = {
|
||||
let mut map = buffer.map_writable().map_err(|_| {
|
||||
gst::element_imp_error!(self, gst::LibraryError::Failed, ["Failed to map buffer"]);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
file.read(map.as_mut()).map_err(|err| {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::LibraryError::Failed,
|
||||
["Failed to read at {}: {}", offset, err.to_string()]
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?
|
||||
};
|
||||
|
||||
*position += size as u64;
|
||||
|
||||
buffer.set_size(size);
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
impl URIHandlerImpl for FileSrc {
|
||||
const URI_TYPE: gst::URIType = gst::URIType::Src;
|
||||
|
||||
fn protocols() -> &'static [&'static str] {
|
||||
&["file"]
|
||||
}
|
||||
|
||||
fn uri(&self) -> Option<String> {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
|
||||
// Conversion to Url already checked while building the `FileLocation`
|
||||
settings.location.as_ref().map(|location| {
|
||||
Url::from_file_path(location)
|
||||
.expect("FileSrc::get_uri couldn't build `Url` from `location`")
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn set_uri(&self, uri: &str) -> Result<(), glib::Error> {
|
||||
// Special case for "file://" as this is used by some applications to test
|
||||
// with `gst_element_make_from_uri` if there's an element that supports the URI protocol
|
||||
|
||||
if uri != "file://" {
|
||||
let file_location = FileLocation::try_from_uri_str(uri)?;
|
||||
self.set_location(Some(file_location))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
28
generic/file/src/filesrc/mod.rs
Normal file
28
generic/file/src/filesrc/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com>
|
||||
// 2018 François Laignel <fengalin@free.fr>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
|
||||
mod imp;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct FileSrc(ObjectSubclass<imp::FileSrc>) @extends gst_base::BaseSrc, gst::Element, gst::Object, @implements gst::URIHandler;
|
||||
}
|
||||
|
||||
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
gst::Element::register(
|
||||
Some(plugin),
|
||||
"rsfilesrc",
|
||||
gst::Rank::NONE,
|
||||
FileSrc::static_type(),
|
||||
)
|
||||
}
|
39
generic/file/src/lib.rs
Normal file
39
generic/file/src/lib.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
|
||||
|
||||
/**
|
||||
* plugin-rsfile:
|
||||
*
|
||||
* Since: plugins-rs-0.1.0
|
||||
*/
|
||||
use gst::glib;
|
||||
|
||||
mod file_location;
|
||||
mod filesink;
|
||||
mod filesrc;
|
||||
|
||||
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
|
||||
filesink::register(plugin)?;
|
||||
filesrc::register(plugin)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
gst::plugin_define!(
|
||||
rsfile,
|
||||
env!("CARGO_PKG_DESCRIPTION"),
|
||||
plugin_init,
|
||||
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
|
||||
"MIT/X11",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
env!("BUILD_REL_DATE")
|
||||
);
|
44
generic/gopbuffer/Cargo.toml
Normal file
44
generic/gopbuffer/Cargo.toml
Normal file
|
@ -0,0 +1,44 @@
|
|||
[package]
|
||||
name = "gst-plugin-gopbuffer"
|
||||
version.workspace = true
|
||||
authors = ["Matthew Waters <matthew@centricular.com>"]
|
||||
license = "MPL-2.0"
|
||||
description = "Store complete groups of pictures at a time"
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
gst = { workspace = true, features = ["v1_18"] }
|
||||
gst-video = { workspace = true, features = ["v1_18"] }
|
||||
once_cell.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "gstgopbuffer"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
gst-app = { workspace = true, features = ["v1_18"] }
|
||||
gst-check = { workspace = true, features = ["v1_18"] }
|
||||
|
||||
[build-dependencies]
|
||||
gst-plugin-version-helper = { path="../../version-helper" }
|
||||
|
||||
[features]
|
||||
static = []
|
||||
capi = []
|
||||
|
||||
[package.metadata.capi]
|
||||
min_version = "0.8.0"
|
||||
|
||||
[package.metadata.capi.header]
|
||||
enabled = false
|
||||
|
||||
[package.metadata.capi.library]
|
||||
install_subdir = "gstreamer-1.0"
|
||||
versioning = false
|
||||
|
||||
[package.metadata.capi.pkg_config]
|
||||
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0"
|
373
generic/gopbuffer/LICENSE-MPL-2.0
Normal file
373
generic/gopbuffer/LICENSE-MPL-2.0
Normal file
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
3
generic/gopbuffer/build.rs
Normal file
3
generic/gopbuffer/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
gst_plugin_version_helper::info()
|
||||
}
|
880
generic/gopbuffer/src/gopbuffer/imp.rs
Normal file
880
generic/gopbuffer/src/gopbuffer/imp.rs
Normal file
|
@ -0,0 +1,880 @@
|
|||
// Copyright (C) 2023 Matthew Waters <matthew@centricular.com>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
|
||||
// If a copy of the MPL was not distributed with this file, You can obtain one at
|
||||
// <https://mozilla.org/MPL/2.0/>.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
/**
|
||||
* SECTION:element-gopbuffer
|
||||
*
|
||||
* #gopbuffer is an element that can be used to store a minimum duration of data delimited by
|
||||
* discrete GOPs (Group of Picture). It does this in by differentiation on the DELTA_UNIT
|
||||
* flag on each input buffer.
|
||||
*
|
||||
* One example of the usefulness of #gopbuffer is its ability to store a backlog of data starting
|
||||
* on a key frame boundary if say the previous 10s seconds of a stream would like to be recorded to
|
||||
* disk.
|
||||
*
|
||||
* ## Example pipeline
|
||||
*
|
||||
* |[
|
||||
* gst-launch videotestsrc ! vp8enc ! gopbuffer minimum-duration=10000000000 ! fakesink
|
||||
* ]|
|
||||
*
|
||||
* Since: plugins-rs-0.13.0
|
||||
*/
|
||||
use gst::glib;
|
||||
use gst::prelude::*;
|
||||
use gst::subclass::prelude::*;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
"gopbuffer",
|
||||
gst::DebugColorFlags::empty(),
|
||||
Some("GopBuffer Element"),
|
||||
)
|
||||
});
|
||||
|
||||
const DEFAULT_MIN_TIME: gst::ClockTime = gst::ClockTime::from_seconds(1);
|
||||
const DEFAULT_MAX_TIME: Option<gst::ClockTime> = None;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Settings {
|
||||
min_time: gst::ClockTime,
|
||||
max_time: Option<gst::ClockTime>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
min_time: DEFAULT_MIN_TIME,
|
||||
max_time: DEFAULT_MAX_TIME,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) enum DeltaFrames {
|
||||
/// Only single completely decodable frames
|
||||
IntraOnly,
|
||||
/// Frames may depend on past frames
|
||||
PredictiveOnly,
|
||||
/// Frames may depend on past or future frames
|
||||
Bidirectional,
|
||||
}
|
||||
|
||||
impl DeltaFrames {
|
||||
/// Whether dts is required to order buffers differently from presentation order
|
||||
pub(crate) fn requires_dts(&self) -> bool {
|
||||
matches!(self, Self::Bidirectional)
|
||||
}
|
||||
/// Whether this coding structure does not allow delta flags on buffers
|
||||
pub(crate) fn intra_only(&self) -> bool {
|
||||
matches!(self, Self::IntraOnly)
|
||||
}
|
||||
|
||||
pub(crate) fn from_caps(caps: &gst::CapsRef) -> Option<Self> {
|
||||
let s = caps.structure(0)?;
|
||||
Some(match s.name().as_str() {
|
||||
"video/x-h264" | "video/x-h265" => DeltaFrames::Bidirectional,
|
||||
"video/x-vp8" | "video/x-vp9" | "video/x-av1" => DeltaFrames::PredictiveOnly,
|
||||
"image/jpeg" | "image/png" | "video/x-raw" => DeltaFrames::IntraOnly,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add buffer list support
|
||||
#[derive(Debug)]
|
||||
enum GopItem {
|
||||
Buffer(gst::Buffer),
|
||||
Event(gst::Event),
|
||||
}
|
||||
|
||||
struct Gop {
|
||||
// all times are in running time
|
||||
start_pts: gst::ClockTime,
|
||||
start_dts: Option<gst::Signed<gst::ClockTime>>,
|
||||
earliest_pts: gst::ClockTime,
|
||||
final_earliest_pts: bool,
|
||||
end_pts: gst::ClockTime,
|
||||
end_dts: Option<gst::Signed<gst::ClockTime>>,
|
||||
final_end_pts: bool,
|
||||
// Buffer or event
|
||||
data: VecDeque<GopItem>,
|
||||
}
|
||||
|
||||
impl Gop {
|
||||
fn push_on_pad(mut self, pad: &gst::Pad) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let mut iter = self.data.iter().filter_map(|item| match item {
|
||||
GopItem::Buffer(buffer) => buffer.pts(),
|
||||
_ => None,
|
||||
});
|
||||
let first_pts = iter.next();
|
||||
let last_pts = iter.last();
|
||||
gst::debug!(
|
||||
CAT,
|
||||
"pushing gop with start pts {} end pts {}",
|
||||
first_pts.display(),
|
||||
last_pts.display(),
|
||||
);
|
||||
for item in self.data.drain(..) {
|
||||
match item {
|
||||
GopItem::Buffer(buffer) => {
|
||||
pad.push(buffer)?;
|
||||
}
|
||||
GopItem::Event(event) => {
|
||||
pad.push_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
struct Stream {
|
||||
sinkpad: gst::Pad,
|
||||
srcpad: gst::Pad,
|
||||
|
||||
sink_segment: Option<gst::FormattedSegment<gst::ClockTime>>,
|
||||
|
||||
delta_frames: DeltaFrames,
|
||||
|
||||
queued_gops: VecDeque<Gop>,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
fn queue_buffer(
|
||||
&mut self,
|
||||
buffer: gst::Buffer,
|
||||
segment: &gst::FormattedSegment<gst::ClockTime>,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let pts_position = buffer.pts().unwrap();
|
||||
let end_pts_position = pts_position
|
||||
.opt_add(buffer.duration())
|
||||
.unwrap_or(pts_position);
|
||||
|
||||
let pts = segment
|
||||
.to_running_time_full(pts_position)
|
||||
.ok_or_else(|| {
|
||||
gst::error!(CAT, obj: self.sinkpad, "Couldn't convert PTS to running time");
|
||||
gst::FlowError::Error
|
||||
})?
|
||||
.positive()
|
||||
.unwrap_or_else(|| {
|
||||
gst::warning!(CAT, obj: self.sinkpad, "Negative PTSs are not supported");
|
||||
gst::ClockTime::ZERO
|
||||
});
|
||||
let end_pts = segment
|
||||
.to_running_time_full(end_pts_position)
|
||||
.ok_or_else(|| {
|
||||
gst::error!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Couldn't convert end PTS to running time"
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?
|
||||
.positive()
|
||||
.unwrap_or_else(|| {
|
||||
gst::warning!(CAT, obj: self.sinkpad, "Negative PTSs are not supported");
|
||||
gst::ClockTime::ZERO
|
||||
});
|
||||
|
||||
let (dts, end_dts) = if !self.delta_frames.requires_dts() {
|
||||
(None, None)
|
||||
} else {
|
||||
let dts_position = buffer.dts().expect("No dts");
|
||||
let end_dts_position = buffer
|
||||
.duration()
|
||||
.opt_add(dts_position)
|
||||
.unwrap_or(dts_position);
|
||||
|
||||
let dts = segment.to_running_time_full(dts_position).ok_or_else(|| {
|
||||
gst::error!(CAT, obj: self.sinkpad, "Couldn't convert DTS to running time");
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let end_dts = segment
|
||||
.to_running_time_full(end_dts_position)
|
||||
.ok_or_else(|| {
|
||||
gst::error!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Couldn't convert end DTS to running time"
|
||||
);
|
||||
gst::FlowError::Error
|
||||
})?;
|
||||
|
||||
let end_dts = std::cmp::max(end_dts, dts);
|
||||
|
||||
(Some(dts), Some(end_dts))
|
||||
};
|
||||
|
||||
if !buffer.flags().contains(gst::BufferFlags::DELTA_UNIT) {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
"New GOP detected with buffer pts {} dts {}",
|
||||
buffer.pts().display(),
|
||||
buffer.dts().display()
|
||||
);
|
||||
let gop = Gop {
|
||||
start_pts: pts,
|
||||
start_dts: dts,
|
||||
earliest_pts: pts,
|
||||
final_earliest_pts: false,
|
||||
end_pts: pts,
|
||||
end_dts,
|
||||
final_end_pts: false,
|
||||
data: VecDeque::from([GopItem::Buffer(buffer)]),
|
||||
};
|
||||
self.queued_gops.push_front(gop);
|
||||
if let Some(prev_gop) = self.queued_gops.get_mut(1) {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Updating previous GOP starting at PTS {} to end PTS {}",
|
||||
prev_gop.earliest_pts,
|
||||
pts,
|
||||
);
|
||||
|
||||
prev_gop.end_pts = std::cmp::max(prev_gop.end_pts, pts);
|
||||
prev_gop.end_dts = std::cmp::max(prev_gop.end_dts, dts);
|
||||
|
||||
if !self.delta_frames.requires_dts() {
|
||||
prev_gop.final_end_pts = true;
|
||||
}
|
||||
|
||||
if !prev_gop.final_earliest_pts {
|
||||
// Don't bother logging this for intra-only streams as it would be for every
|
||||
// single buffer.
|
||||
if self.delta_frames.requires_dts() {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Previous GOP has final earliest PTS at {}",
|
||||
prev_gop.earliest_pts
|
||||
);
|
||||
}
|
||||
|
||||
prev_gop.final_earliest_pts = true;
|
||||
if let Some(prev_prev_gop) = self.queued_gops.get_mut(2) {
|
||||
prev_prev_gop.final_end_pts = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(gop) = self.queued_gops.front_mut() {
|
||||
gop.end_pts = std::cmp::max(gop.end_pts, end_pts);
|
||||
gop.end_dts = gop.end_dts.opt_max(end_dts);
|
||||
gop.data.push_back(GopItem::Buffer(buffer));
|
||||
|
||||
if self.delta_frames.requires_dts() {
|
||||
let dts = dts.unwrap();
|
||||
|
||||
if gop.earliest_pts > pts && !gop.final_earliest_pts {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Updating current GOP earliest PTS from {} to {}",
|
||||
gop.earliest_pts,
|
||||
pts
|
||||
);
|
||||
gop.earliest_pts = pts;
|
||||
|
||||
if let Some(prev_gop) = self.queued_gops.get_mut(1) {
|
||||
if prev_gop.end_pts < pts {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Updating previous GOP starting PTS {} end time from {} to {}",
|
||||
pts,
|
||||
prev_gop.end_pts,
|
||||
pts
|
||||
);
|
||||
prev_gop.end_pts = pts;
|
||||
}
|
||||
}
|
||||
}
|
||||
let gop = self.queued_gops.front_mut().unwrap();
|
||||
|
||||
// The earliest PTS is known when the current DTS is bigger or equal to the first
|
||||
// PTS that was observed in this GOP. If there was another frame later that had a
|
||||
// lower PTS then it wouldn't be possible to display it in time anymore, i.e. the
|
||||
// stream would be invalid.
|
||||
if gop.start_pts <= dts && !gop.final_earliest_pts {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"GOP has final earliest PTS at {}",
|
||||
gop.earliest_pts
|
||||
);
|
||||
gop.final_earliest_pts = true;
|
||||
|
||||
if let Some(prev_gop) = self.queued_gops.get_mut(1) {
|
||||
prev_gop.final_end_pts = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
"dropping buffer before first GOP with pts {} dts {}",
|
||||
buffer.pts().display(),
|
||||
buffer.dts().display()
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((prev_gop, first_gop)) = Option::zip(
|
||||
self.queued_gops.iter().find(|gop| gop.final_end_pts),
|
||||
self.queued_gops.back(),
|
||||
) {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Queued full GOPs duration updated to {}",
|
||||
prev_gop.end_pts.saturating_sub(first_gop.earliest_pts),
|
||||
);
|
||||
}
|
||||
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: self.sinkpad,
|
||||
"Queued duration updated to {}",
|
||||
Option::zip(self.queued_gops.front(), self.queued_gops.back())
|
||||
.map(|(end, start)| end.end_pts.saturating_sub(start.start_pts))
|
||||
.unwrap_or(gst::ClockTime::ZERO)
|
||||
);
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
|
||||
fn oldest_gop(&mut self) -> Option<Gop> {
|
||||
self.queued_gops.pop_back()
|
||||
}
|
||||
|
||||
fn peek_oldest_gop(&self) -> Option<&Gop> {
|
||||
self.queued_gops.back()
|
||||
}
|
||||
|
||||
fn peek_second_oldest_gop(&self) -> Option<&Gop> {
|
||||
if self.queued_gops.len() <= 1 {
|
||||
return None;
|
||||
}
|
||||
self.queued_gops.get(self.queued_gops.len() - 2)
|
||||
}
|
||||
|
||||
fn drain_all(&mut self) -> impl Iterator<Item = Gop> + '_ {
|
||||
self.queued_gops.drain(..)
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
self.queued_gops.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
streams: Vec<Stream>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn stream_from_sink_pad(&self, pad: &gst::Pad) -> Option<&Stream> {
|
||||
self.streams.iter().find(|stream| &stream.sinkpad == pad)
|
||||
}
|
||||
fn stream_from_sink_pad_mut(&mut self, pad: &gst::Pad) -> Option<&mut Stream> {
|
||||
self.streams
|
||||
.iter_mut()
|
||||
.find(|stream| &stream.sinkpad == pad)
|
||||
}
|
||||
fn stream_from_src_pad(&self, pad: &gst::Pad) -> Option<&Stream> {
|
||||
self.streams.iter().find(|stream| &stream.srcpad == pad)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct GopBuffer {
|
||||
state: Mutex<State>,
|
||||
settings: Mutex<Settings>,
|
||||
}
|
||||
|
||||
impl GopBuffer {
|
||||
fn sink_chain(
|
||||
&self,
|
||||
pad: &gst::Pad,
|
||||
buffer: gst::Buffer,
|
||||
) -> Result<gst::FlowSuccess, gst::FlowError> {
|
||||
let obj = self.obj();
|
||||
if buffer.pts().is_none() {
|
||||
gst::error!(CAT, obj: obj, "Require timestamped buffers!");
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
|
||||
let settings = self.settings.lock().unwrap().clone();
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let stream = state
|
||||
.stream_from_sink_pad_mut(pad)
|
||||
.expect("pad without an internal Stream");
|
||||
|
||||
let Some(segment) = stream.sink_segment.clone() else {
|
||||
gst::element_imp_error!(self, gst::CoreError::Clock, ["Got buffer before segment"]);
|
||||
return Err(gst::FlowError::Error);
|
||||
};
|
||||
|
||||
if stream.delta_frames.intra_only() && buffer.flags().contains(gst::BufferFlags::DELTA_UNIT)
|
||||
{
|
||||
gst::error!(CAT, obj: pad, "Intra-only stream with delta units");
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
|
||||
if stream.delta_frames.requires_dts() && buffer.dts().is_none() {
|
||||
gst::error!(CAT, obj: pad, "Require DTS for video streams");
|
||||
return Err(gst::FlowError::Error);
|
||||
}
|
||||
|
||||
let srcpad = stream.srcpad.clone();
|
||||
stream.queue_buffer(buffer, &segment)?;
|
||||
let mut gops_to_push = vec![];
|
||||
|
||||
let Some(newest_gop) = stream.queued_gops.front() else {
|
||||
return Ok(gst::FlowSuccess::Ok);
|
||||
};
|
||||
// we are looking for the latest pts value here (which should be the largest)
|
||||
let newest_ts = if stream.delta_frames.requires_dts() {
|
||||
newest_gop.end_dts.unwrap()
|
||||
} else {
|
||||
gst::Signed::Positive(newest_gop.end_pts)
|
||||
};
|
||||
|
||||
loop {
|
||||
// check stored times as though the oldest GOP doesn't exist.
|
||||
let Some(second_oldest_gop) = stream.peek_second_oldest_gop() else {
|
||||
break;
|
||||
};
|
||||
// we are looking for the oldest pts here (with the largest value). This is our potentially
|
||||
// new end time.
|
||||
let oldest_ts = if stream.delta_frames.requires_dts() {
|
||||
second_oldest_gop.start_dts.unwrap()
|
||||
} else {
|
||||
gst::Signed::Positive(second_oldest_gop.start_pts)
|
||||
};
|
||||
|
||||
let stored_duration_without_oldest = newest_ts.saturating_sub(oldest_ts);
|
||||
gst::trace!(
|
||||
CAT,
|
||||
obj: obj,
|
||||
"newest_pts {}, second oldest_pts {}, stored_duration_without_oldest_gop {}, min-time {}",
|
||||
newest_ts.display(),
|
||||
oldest_ts.display(),
|
||||
stored_duration_without_oldest.display(),
|
||||
settings.min_time.display()
|
||||
);
|
||||
if stored_duration_without_oldest < settings.min_time {
|
||||
break;
|
||||
}
|
||||
gops_to_push.push(stream.oldest_gop().unwrap());
|
||||
}
|
||||
|
||||
if let Some(max_time) = settings.max_time {
|
||||
while let Some(oldest_gop) = stream.peek_oldest_gop() {
|
||||
let oldest_ts = oldest_gop.data.iter().rev().find_map(|item| match item {
|
||||
GopItem::Buffer(buffer) => {
|
||||
if stream.delta_frames.requires_dts() {
|
||||
Some(gst::Signed::Positive(buffer.dts().unwrap()))
|
||||
} else {
|
||||
Some(gst::Signed::Positive(buffer.pts().unwrap()))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if newest_ts
|
||||
.opt_saturating_sub(oldest_ts)
|
||||
.map_or(false, |diff| diff > gst::Signed::Positive(max_time))
|
||||
{
|
||||
gst::warning!(CAT, obj: obj, "Stored data has overflowed the maximum allowed stored time {}, pushing oldest GOP", max_time.display());
|
||||
gops_to_push.push(stream.oldest_gop().unwrap());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(state);
|
||||
for gop in gops_to_push.into_iter() {
|
||||
gop.push_on_pad(&srcpad)?;
|
||||
}
|
||||
|
||||
Ok(gst::FlowSuccess::Ok)
|
||||
}
|
||||
|
||||
fn sink_event(&self, pad: &gst::Pad, event: gst::Event) -> bool {
|
||||
let obj = self.obj();
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let stream = state
|
||||
.stream_from_sink_pad_mut(pad)
|
||||
.expect("pad without an internal Stream!");
|
||||
match event.view() {
|
||||
gst::EventView::Caps(caps) => {
|
||||
let Some(delta_frames) = DeltaFrames::from_caps(caps.caps()) else {
|
||||
return false;
|
||||
};
|
||||
stream.delta_frames = delta_frames;
|
||||
}
|
||||
gst::EventView::FlushStop(_flush) => {
|
||||
gst::debug!(CAT, obj: obj, "flushing stored data");
|
||||
stream.flush();
|
||||
}
|
||||
gst::EventView::Eos(_eos) => {
|
||||
gst::debug!(CAT, obj: obj, "draining data at EOS");
|
||||
let gops = stream.drain_all().collect::<Vec<_>>();
|
||||
let srcpad = stream.srcpad.clone();
|
||||
drop(state);
|
||||
for gop in gops.into_iter() {
|
||||
let _ = gop.push_on_pad(&srcpad);
|
||||
}
|
||||
// once we've pushed all the data, we can push the corresponding eos
|
||||
gst::Pad::event_default(pad, Some(&*obj), event);
|
||||
return true;
|
||||
}
|
||||
gst::EventView::Segment(segment) => {
|
||||
let Ok(segment) = segment.segment().clone().downcast::<gst::ClockTime>() else {
|
||||
gst::error!(CAT, "Non TIME segments are not supported");
|
||||
return false;
|
||||
};
|
||||
stream.sink_segment = Some(segment);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
if event.is_serialized() {
|
||||
if stream.peek_oldest_gop().is_none() {
|
||||
// if there is nothing queued, the event can go straight through
|
||||
gst::trace!(CAT, obj: obj, "nothing queued, event {:?} passthrough", event.structure().map(|s| s.name().as_str()));
|
||||
drop(state);
|
||||
return gst::Pad::event_default(pad, Some(&*obj), event);
|
||||
}
|
||||
let gop = stream.queued_gops.front_mut().unwrap();
|
||||
gop.data.push_back(GopItem::Event(event));
|
||||
true
|
||||
} else {
|
||||
// non-serialized events can be pushed directly
|
||||
drop(state);
|
||||
gst::Pad::event_default(pad, Some(&*obj), event)
|
||||
}
|
||||
}
|
||||
|
||||
fn sink_query(&self, pad: &gst::Pad, query: &mut gst::QueryRef) -> bool {
|
||||
let obj = self.obj();
|
||||
if query.is_serialized() {
|
||||
// TODO: serialized queries somehow?
|
||||
gst::warning!(CAT, obj: pad, "Serialized queries are currently not supported");
|
||||
return false;
|
||||
}
|
||||
gst::Pad::query_default(pad, Some(&*obj), query)
|
||||
}
|
||||
|
||||
fn src_query(&self, pad: &gst::Pad, query: &mut gst::QueryRef) -> bool {
|
||||
let obj = self.obj();
|
||||
match query.view_mut() {
|
||||
gst::QueryViewMut::Latency(latency) => {
|
||||
let mut upstream_query = gst::query::Latency::new();
|
||||
let otherpad = {
|
||||
let state = self.state.lock().unwrap();
|
||||
let Some(stream) = state.stream_from_src_pad(pad) else {
|
||||
return false;
|
||||
};
|
||||
stream.sinkpad.clone()
|
||||
};
|
||||
let ret = otherpad.peer_query(&mut upstream_query);
|
||||
|
||||
if ret {
|
||||
let (live, mut min, mut max) = upstream_query.result();
|
||||
|
||||
let settings = self.settings.lock().unwrap();
|
||||
min += settings.max_time.unwrap_or(settings.min_time);
|
||||
max = max.opt_max(settings.max_time);
|
||||
|
||||
latency.set(live, min, max);
|
||||
|
||||
gst::debug!(
|
||||
CAT,
|
||||
obj: pad,
|
||||
"Latency query response: live {} min {} max {}",
|
||||
live,
|
||||
min,
|
||||
max.display()
|
||||
);
|
||||
}
|
||||
ret
|
||||
}
|
||||
_ => gst::Pad::query_default(pad, Some(&*obj), query),
|
||||
}
|
||||
}
|
||||
|
||||
fn iterate_internal_links(&self, pad: &gst::Pad) -> gst::Iterator<gst::Pad> {
|
||||
let state = self.state.lock().unwrap();
|
||||
let otherpad = match pad.direction() {
|
||||
gst::PadDirection::Src => state
|
||||
.stream_from_src_pad(pad)
|
||||
.map(|stream| stream.sinkpad.clone()),
|
||||
gst::PadDirection::Sink => state
|
||||
.stream_from_sink_pad(pad)
|
||||
.map(|stream| stream.srcpad.clone()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if let Some(otherpad) = otherpad {
|
||||
gst::Iterator::from_vec(vec![otherpad])
|
||||
} else {
|
||||
gst::Iterator::from_vec(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for GopBuffer {
|
||||
const NAME: &'static str = "GstGopBuffer";
|
||||
type Type = super::GopBuffer;
|
||||
type ParentType = gst::Element;
|
||||
}
|
||||
|
||||
impl ObjectImpl for GopBuffer {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecUInt64::builder("minimum-duration")
|
||||
.nick("Minimum Duration")
|
||||
.blurb("The minimum duration to store")
|
||||
.default_value(DEFAULT_MIN_TIME.nseconds())
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
glib::ParamSpecUInt64::builder("max-size-time")
|
||||
.nick("Maximum Duration")
|
||||
.blurb("The maximum duration to store (0=disable)")
|
||||
.default_value(0)
|
||||
.mutable_ready()
|
||||
.build(),
|
||||
]
|
||||
});
|
||||
|
||||
&PROPERTIES
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||
match pspec.name() {
|
||||
"minimum-duration" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
let min_time = value.get().expect("type checked upstream");
|
||||
if settings.min_time != min_time {
|
||||
settings.min_time = min_time;
|
||||
drop(settings);
|
||||
self.post_message(gst::message::Latency::builder().src(&*self.obj()).build());
|
||||
}
|
||||
}
|
||||
"max-size-time" => {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
let max_time = value
|
||||
.get::<Option<gst::ClockTime>>()
|
||||
.expect("type checked upstream");
|
||||
let max_time = if matches!(max_time, Some(gst::ClockTime::ZERO) | None) {
|
||||
None
|
||||
} else {
|
||||
max_time
|
||||
};
|
||||
if settings.max_time != max_time {
|
||||
settings.max_time = max_time;
|
||||
drop(settings);
|
||||
self.post_message(gst::message::Latency::builder().src(&*self.obj()).build());
|
||||
}
|
||||
}
|
||||
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"minimum-duration" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.min_time.to_value()
|
||||
}
|
||||
"max-size-time" => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
settings.max_time.unwrap_or(gst::ClockTime::ZERO).to_value()
|
||||
}
|
||||
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
let class = obj.class();
|
||||
let templ = class.pad_template("video_sink").unwrap();
|
||||
let sinkpad = gst::Pad::builder_from_template(&templ)
|
||||
.name("video_sink")
|
||||
.chain_function(|pad, parent, buffer| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| Err(gst::FlowError::Error),
|
||||
|gopbuffer| gopbuffer.sink_chain(pad, buffer),
|
||||
)
|
||||
})
|
||||
.event_function(|pad, parent, event| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|gopbuffer| gopbuffer.sink_event(pad, event),
|
||||
)
|
||||
})
|
||||
.query_function(|pad, parent, query| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|gopbuffer| gopbuffer.sink_query(pad, query),
|
||||
)
|
||||
})
|
||||
.iterate_internal_links_function(|pad, parent| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| gst::Pad::iterate_internal_links_default(pad, parent),
|
||||
|gopbuffer| gopbuffer.iterate_internal_links(pad),
|
||||
)
|
||||
})
|
||||
.flags(gst::PadFlags::PROXY_CAPS)
|
||||
.build();
|
||||
obj.add_pad(&sinkpad).unwrap();
|
||||
|
||||
let templ = class.pad_template("video_src").unwrap();
|
||||
let srcpad = gst::Pad::builder_from_template(&templ)
|
||||
.name("video_src")
|
||||
.query_function(|pad, parent, query| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| false,
|
||||
|gopbuffer| gopbuffer.src_query(pad, query),
|
||||
)
|
||||
})
|
||||
.iterate_internal_links_function(|pad, parent| {
|
||||
GopBuffer::catch_panic_pad_function(
|
||||
parent,
|
||||
|| gst::Pad::iterate_internal_links_default(pad, parent),
|
||||
|gopbuffer| gopbuffer.iterate_internal_links(pad),
|
||||
)
|
||||
})
|
||||
.build();
|
||||
obj.add_pad(&srcpad).unwrap();
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.streams.push(Stream {
|
||||
sinkpad,
|
||||
srcpad,
|
||||
sink_segment: None,
|
||||
delta_frames: DeltaFrames::IntraOnly,
|
||||
queued_gops: VecDeque::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl GstObjectImpl for GopBuffer {}
|
||||
|
||||
impl ElementImpl for GopBuffer {
|
||||
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
|
||||
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
|
||||
gst::subclass::ElementMetadata::new(
|
||||
"GopBuffer",
|
||||
"Video",
|
||||
"GOP Buffer",
|
||||
"Matthew Waters <matthew@centricular.com>",
|
||||
)
|
||||
});
|
||||
|
||||
Some(&*ELEMENT_METADATA)
|
||||
}
|
||||
|
||||
fn pad_templates() -> &'static [gst::PadTemplate] {
|
||||
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
|
||||
// This element is designed to implement multiple streams but it has not been
|
||||
// implemented.
|
||||
//
|
||||
// The things missing for multiple (audio or video) streams are:
|
||||
// 1. More pad templates
|
||||
// 2. Choosing a main stream to drive the timestamp logic between all input streams
|
||||
// 3. Allowing either the main stream to cause other streams to push data
|
||||
// regardless of it's GOP state, or allow each stream to be individually delimited
|
||||
// by GOP but all still within the minimum duration.
|
||||
let video_caps = [
|
||||
gst::Structure::builder("video/x-h264")
|
||||
.field("stream-format", gst::List::new(["avc", "avc3"]))
|
||||
.field("alignment", "au")
|
||||
.build(),
|
||||
gst::Structure::builder("video/x-h265")
|
||||
.field("stream-format", gst::List::new(["hvc1", "hev1"]))
|
||||
.field("alignment", "au")
|
||||
.build(),
|
||||
gst::Structure::builder("video/x-vp8").build(),
|
||||
gst::Structure::builder("video/x-vp9").build(),
|
||||
gst::Structure::builder("video/x-av1")
|
||||
.field("stream-format", "obu-stream")
|
||||
.field("alignment", "tu")
|
||||
.build(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<gst::Caps>();
|
||||
|
||||
let src_pad_template = gst::PadTemplate::new(
|
||||
"video_src",
|
||||
gst::PadDirection::Src,
|
||||
gst::PadPresence::Always,
|
||||
&video_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sink_pad_template = gst::PadTemplate::new(
|
||||
"video_sink",
|
||||
gst::PadDirection::Sink,
|
||||
gst::PadPresence::Always,
|
||||
&video_caps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![src_pad_template, sink_pad_template]
|
||||
});
|
||||
|
||||
PAD_TEMPLATES.as_ref()
|
||||
}
|
||||
|
||||
fn change_state(
|
||||
&self,
|
||||
transition: gst::StateChange,
|
||||
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
|
||||
#[allow(clippy::single_match)]
|
||||
match transition {
|
||||
gst::StateChange::NullToReady => {
|
||||
let settings = self.settings.lock().unwrap();
|
||||
if let Some(max_time) = settings.max_time {
|
||||
if max_time < settings.min_time {
|
||||
gst::element_imp_error!(
|
||||
self,
|
||||
gst::CoreError::StateChange,
|
||||
["Configured maximum time is less than the minimum time"]
|
||||
);
|
||||
return Err(gst::StateChangeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.parent_change_state(transition)?;
|
||||
|
||||
Ok(gst::StateChangeSuccess::Success)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue