Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a2fdbf0c0 | ||
|
|
91f95f13ae | ||
|
|
ebf46042ba | ||
|
|
8547cee1b9 | ||
|
|
56729469bf | ||
|
|
e619d31622 | ||
|
|
370cc76dbe | ||
|
|
1ac2f9e923 | ||
|
|
8caf360671 | ||
|
|
59b1dcfd13 | ||
|
|
34a115706e | ||
|
|
b6c029df38 | ||
|
|
0582655954 | ||
|
|
c8f61b0b87 | ||
|
|
a2c4d440db | ||
|
|
b64ad64524 | ||
|
|
e9e6f1deb9 | ||
|
|
6f381362dd | ||
|
|
5d1bed4d25 | ||
|
|
9b9104b502 | ||
|
|
facbc01a9c | ||
|
|
3ba0e35032 | ||
|
|
958b391afa | ||
|
|
678c8054d7 | ||
|
|
d90657b05a | ||
|
|
9e56547435 | ||
|
|
bcb4d0a905 | ||
|
|
225b66cb06 | ||
|
|
cd1ff0da97 | ||
|
|
1d1e553191 | ||
|
|
e10691e2b7 | ||
|
|
9e20efff4b | ||
|
|
9e32c1da3d | ||
|
|
39b2621e30 | ||
|
|
4e44136f51 | ||
|
|
62bd695138 | ||
|
|
0b9454896c | ||
|
|
51c1c6db33 | ||
|
|
0c1fc66be8 | ||
|
|
ee67ff56c7 | ||
|
|
ea1a2f3cd0 | ||
|
|
25ca131da1 | ||
|
|
05f6dd86b1 | ||
|
|
2b052fef1e | ||
|
|
d6aa64b816 | ||
|
|
b3664d0e2d | ||
|
|
34eb2ccd31 | ||
|
|
0d32b8f06f | ||
|
|
73316ab5b8 | ||
|
|
69ed345191 | ||
|
|
78188527a9 | ||
|
|
2d57c3f7c1 | ||
|
|
4bfa6d270d | ||
|
|
85e6eb9eb6 | ||
|
|
83593a0ea7 | ||
|
|
dd6dbdf1aa | ||
|
|
6a2c3d7b28 | ||
|
|
15161ede06 | ||
|
|
ee370ec978 | ||
|
|
e844b5df65 | ||
|
|
087e38c9d7 | ||
|
|
2bf39aeb2e | ||
|
|
ccc85772d3 | ||
|
|
9d2f7704c7 | ||
|
|
6e20c126c3 | ||
|
|
9b149a1eb5 | ||
|
|
6010182592 | ||
|
|
868288e70f | ||
|
|
d73c0ee319 | ||
|
|
5cf9ea74f4 | ||
|
|
2ab705efdf | ||
|
|
1f4865c468 | ||
|
|
8388076b71 | ||
|
|
7a37d11aea | ||
|
|
cdd5802a72 | ||
|
|
951b7bcfb0 | ||
|
|
4cb060f496 | ||
|
|
c5da9e9bf7 | ||
|
|
386516ade9 | ||
|
|
211f90307f | ||
|
|
d6558c3619 | ||
|
|
bb265a51fd | ||
|
|
4bc8934b1b | ||
|
|
1bc15a1ba3 | ||
|
|
5026a6fbcf | ||
|
|
628ce0f12e | ||
|
|
8d05e8e670 | ||
|
|
2b11f12763 | ||
|
|
3536bf06ba | ||
|
|
88c0e358aa | ||
|
|
7b5d9cde2e | ||
|
|
c5fc72cc2c | ||
|
|
ab503875cb | ||
|
|
a9c4417434 | ||
|
|
679c0a2b4c | ||
|
|
5c1d2de9e5 | ||
|
|
1442495c9e | ||
|
|
22ce2edfa3 | ||
|
|
69026cbcd6 | ||
|
|
76f765ce5f | ||
|
|
b8ba716f34 | ||
|
|
131d115761 | ||
|
|
9b9f4f7603 | ||
|
|
c32f2d3f50 | ||
|
|
c2d3028668 | ||
|
|
338523e216 | ||
|
|
54bf701495 | ||
|
|
c1bf40d825 | ||
|
|
905e3eaf73 | ||
|
|
900a4cca14 | ||
|
|
a44e9c87d7 | ||
|
|
7b9bbba3fd | ||
|
|
fc6591f26f | ||
|
|
f50d1bd488 | ||
|
|
0c386ef337 | ||
|
|
245613162a | ||
|
|
d2eb674bb5 | ||
|
|
d5af136da2 | ||
|
|
3e48df9756 | ||
|
|
fb04850ae8 | ||
|
|
f62a405979 | ||
|
|
58180ae3fa | ||
|
|
e416dd5f78 | ||
|
|
e37c22b259 | ||
|
|
238d17020b | ||
|
|
96f11a4a69 | ||
|
|
5fcfa84766 | ||
|
|
04ecd45f7c | ||
|
|
dd9ad9728a | ||
|
|
6a3e1d8a88 | ||
|
|
d2e0cc779c | ||
|
|
ee4c1defa0 | ||
|
|
49417e7bc9 | ||
|
|
e301f2af16 | ||
|
|
0df5135f0b | ||
|
|
ab7ccc7732 | ||
|
|
02c6a12a82 | ||
|
|
ed1efdd102 | ||
|
|
e51701ef00 | ||
|
|
2b0fb3844b | ||
|
|
323613e4db | ||
|
|
80f9118c0d | ||
|
|
074a31606c | ||
|
|
3bb78db2da | ||
|
|
e5fc439da2 | ||
|
|
87ad97c00f | ||
|
|
ce21c398b5 | ||
|
|
ed56d26b1c | ||
|
|
08a2312099 | ||
|
|
619e936c39 | ||
|
|
75e88ee31e | ||
|
|
2e7abf0eb2 | ||
|
|
6941f006b3 | ||
|
|
e0d03baf1c | ||
|
|
08ef392fca | ||
|
|
b7d7b9a058 | ||
|
|
a7fc2a65e0 | ||
|
|
a707b21446 | ||
|
|
0e8980da95 | ||
|
|
ff991dd617 | ||
|
|
a8c1451a28 | ||
|
|
9198960cc7 | ||
|
|
b8478bfedc | ||
|
|
0f443af355 | ||
|
|
e63ce4d101 | ||
|
|
e57f0cd723 | ||
|
|
4fb3f7fa97 | ||
|
|
5759c122ed | ||
|
|
55ac533718 | ||
|
|
282b3d6545 | ||
|
|
eb41536749 | ||
|
|
0728959ebe | ||
|
|
b118f80b52 | ||
|
|
25073803ba | ||
|
|
1046cfe9a0 | ||
|
|
2250cba35c | ||
|
|
1e24731b60 | ||
|
|
034ee52eec | ||
|
|
13a1a4d97b | ||
|
|
f22d05a34e | ||
|
|
0d3149a696 | ||
|
|
11e5028020 | ||
|
|
2b96bb1c77 | ||
|
|
f843acf12f | ||
|
|
05d3567489 | ||
|
|
25c4ca01de | ||
|
|
ac66ffe302 | ||
|
|
f0b4e2e2ad | ||
|
|
d72930877a | ||
|
|
4d45c538cc | ||
|
|
f368076aaf | ||
|
|
61c5b958f2 | ||
|
|
334788c26f | ||
|
|
12ad168e13 | ||
|
|
48fbec02e4 | ||
|
|
ba453f9405 | ||
|
|
00491d3bce | ||
|
|
98f73928fa | ||
|
|
3dccec3739 | ||
|
|
5e40c9f9ad | ||
|
|
5bcc452c9f | ||
|
|
5bf218d930 | ||
|
|
5df330f608 | ||
|
|
bd9e2e3215 | ||
|
|
d1ecc894b3 | ||
|
|
5a2d5af887 | ||
|
|
45895fa8bf | ||
|
|
fa7710ce44 | ||
|
|
cd7d560e63 | ||
|
|
fcf0ec98e5 | ||
|
|
7691d2c0eb | ||
|
|
686418997b | ||
|
|
f4b6fd709d | ||
|
|
8126173128 | ||
|
|
5e4ba088be | ||
|
|
6682e64519 | ||
|
|
2140cc7820 | ||
|
|
7bafa39960 | ||
|
|
37379220b4 | ||
|
|
db6906d7a4 | ||
|
|
b262ae47e1 | ||
|
|
797b1a0b71 | ||
|
|
77955f6bd6 | ||
|
|
17a9fa39f5 | ||
|
|
f6032969c3 | ||
|
|
686696a6d9 | ||
|
|
c629acfa4b | ||
|
|
02ede90d77 | ||
|
|
84cb37f6f3 | ||
|
|
46526603cc | ||
|
|
80c6d488cf | ||
|
|
6ea23d94cb | ||
|
|
29c8382825 | ||
|
|
b83811cc62 | ||
|
|
47a590e92a | ||
|
|
e2ab307da7 | ||
|
|
a83274ab28 | ||
|
|
da8a210fcc | ||
|
|
0f8ba42975 | ||
|
|
35a2c1111d | ||
|
|
d263482f7e | ||
|
|
19beac795b | ||
|
|
e61e96b352 | ||
|
|
4b8691b943 | ||
|
|
487f987c25 | ||
|
|
a69d7eaede | ||
|
|
a87969d5b8 | ||
|
|
8f25081bbb | ||
|
|
6a36e73463 | ||
|
|
d69688678d | ||
|
|
56c589f042 | ||
|
|
df2b67ac99 | ||
|
|
0473233775 | ||
|
|
57b12d7a54 | ||
|
|
6c6d7b73c1 | ||
|
|
10dcdc31d8 | ||
|
|
7c725a4d80 | ||
|
|
4640ec86ed | ||
|
|
03cabd0506 | ||
|
|
879c2f368c | ||
|
|
fc73e7a3b2 | ||
|
|
b9bd1c8c44 | ||
|
|
b5ce7a8ff7 | ||
|
|
51e915b598 | ||
|
|
0addca3876 | ||
|
|
09349ebdef | ||
|
|
e93d031f86 | ||
|
|
c491132c09 | ||
|
|
0fef5a389d | ||
|
|
0047bfe51f | ||
|
|
994a60c8a4 | ||
|
|
50d53cd826 | ||
|
|
1d6906e5e7 | ||
|
|
2c8f2ee408 | ||
|
|
4810451e6a | ||
|
|
60801e3553 | ||
|
|
9bdd92aeb3 | ||
|
|
835c9c1166 | ||
|
|
9da143ed81 | ||
|
|
f7a032a918 | ||
|
|
eacdda384a | ||
|
|
1f13fd691b | ||
|
|
73de0877a1 | ||
|
|
616c7673a4 | ||
|
|
489e60d2b1 | ||
|
|
af2a2728f8 | ||
|
|
91f012ee9f | ||
|
|
c4bc77db05 | ||
|
|
6579ea681e | ||
|
|
07b6aa099b | ||
|
|
e10a296305 | ||
|
|
c42fe94b46 | ||
|
|
1d0743cda6 | ||
|
|
0f6f105ec1 | ||
|
|
578ad81b8a | ||
|
|
3cbdba78d6 | ||
|
|
1de7592e86 | ||
|
|
6a9891adee | ||
|
|
29700e6f18 | ||
|
|
7426e3e78c | ||
|
|
724a11ae4f | ||
|
|
03d08a2c65 | ||
|
|
f24e68d0d9 | ||
|
|
a07e94ca74 | ||
|
|
14a384436c | ||
|
|
6be5aa6507 | ||
|
|
97d17f505b | ||
|
|
c2ed8c5517 | ||
|
|
ac299287e4 | ||
|
|
ec5aec2601 | ||
|
|
59033ff8d7 | ||
|
|
88edc7f263 | ||
|
|
19882a9373 | ||
|
|
620385501f | ||
|
|
d0eb864814 | ||
|
|
1920735efa | ||
|
|
7370fa5c94 | ||
|
|
a875a65a7f | ||
|
|
88414cd3db | ||
|
|
00aff53e35 | ||
|
|
62561ac423 | ||
|
|
d26cef7ef4 | ||
|
|
527fb16428 | ||
|
|
0f7ba20e65 | ||
|
|
94e9bc35b1 | ||
|
|
b3fd9e51d7 | ||
|
|
bef2456bea | ||
|
|
25f65c3596 | ||
|
|
8260624a41 | ||
|
|
7ab1c43588 | ||
|
|
7d0477204b | ||
|
|
8f1dda44b9 | ||
|
|
ee03f6b866 | ||
|
|
9e41537554 | ||
|
|
8baa2e9067 | ||
|
|
556db88ff7 | ||
|
|
9063331d38 | ||
|
|
594906c121 | ||
|
|
357a907838 | ||
|
|
6bc142a07b | ||
|
|
cd6e08956c | ||
|
|
9937b67509 | ||
|
|
c478d641c6 | ||
|
|
1702f37d3e | ||
|
|
e1920ddb01 | ||
|
|
9b5b93dc31 | ||
|
|
cd4c5bbc56 | ||
|
|
47bae92dd5 | ||
|
|
a5bbca55a2 | ||
|
|
a4f5c4f211 | ||
|
|
9c61e5725f | ||
|
|
8c4f2e46f7 | ||
|
|
76253a9583 | ||
|
|
5653215f68 | ||
|
|
1b9e9575f1 | ||
|
|
dfde770274 | ||
|
|
1737eae39b | ||
|
|
fb32ef3629 | ||
|
|
5734a0a0b7 | ||
|
|
758a888a53 | ||
|
|
ed0045afec | ||
|
|
de5fb7a19b | ||
|
|
e2bebbcc61 | ||
|
|
24fd69e7dc | ||
|
|
16b1f9164f | ||
|
|
51a62ce1d8 | ||
|
|
23b68b2d15 | ||
|
|
7ee449c0e4 | ||
|
|
2891beef5f | ||
|
|
da47a6ae46 | ||
|
|
96145baa7a | ||
|
|
ab31ab393a | ||
|
|
38ee17a852 | ||
|
|
78be18ed89 | ||
|
|
5fd8a7c587 | ||
|
|
79a075e338 | ||
|
|
5c3a6de2b7 | ||
|
|
5a77571d6f | ||
|
|
1e6a380f99 | ||
|
|
8e00552c61 | ||
|
|
fabd01ca9e | ||
|
|
ab2efcf05a | ||
|
|
dfaa4005bb | ||
|
|
a64a29ce07 | ||
|
|
68f21fa8e5 | ||
|
|
d9dbb2bcee | ||
|
|
782ac4c362 | ||
|
|
ff68114fe3 | ||
|
|
cbf48bdf0a | ||
|
|
fcb68bc54f | ||
|
|
a591380d60 | ||
|
|
822dfc433b | ||
|
|
17ecd52755 | ||
|
|
6fd35deaa4 | ||
|
|
d80c2e35ca | ||
|
|
7ed6c559e0 | ||
|
|
e5b1eda3a1 | ||
|
|
3120cd785b | ||
|
|
b036e01afc | ||
|
|
01e379eefe | ||
|
|
17a00a1604 | ||
|
|
59660840e4 | ||
|
|
2775ca09c7 | ||
|
|
8c8fea0d4b | ||
|
|
7f23c4a508 | ||
|
|
fda5c8610c | ||
|
|
049034e5a9 | ||
|
|
6ae3beb2a5 | ||
|
|
13853449d2 | ||
|
|
2679ba75af | ||
|
|
561080568c | ||
|
|
4edf1e2cf5 | ||
|
|
e7998c3559 | ||
|
|
8f674d6a3d | ||
|
|
f1a9fd2b4f | ||
|
|
46f75f0676 | ||
|
|
d3b62e38fc | ||
|
|
c8ebdc0a88 | ||
|
|
238eb816d0 | ||
|
|
0f054bb96f | ||
|
|
711996b478 | ||
|
|
a43fe609d3 | ||
|
|
2712922ce5 | ||
|
|
49b685b3ec | ||
|
|
302606564c | ||
|
|
1fc1b5bc60 | ||
|
|
f260d75372 | ||
|
|
a03a50d238 | ||
|
|
928284990b | ||
|
|
03046988c7 | ||
|
|
c860433065 | ||
|
|
5d66c17bc1 | ||
|
|
16b5df7dd1 | ||
|
|
589c025951 | ||
|
|
7fbfe94b71 | ||
|
|
27bb8fd737 | ||
|
|
cc285d5488 | ||
|
|
c8522de476 | ||
|
|
94f2e39367 | ||
|
|
bd83af8bf4 | ||
|
|
420eebf831 | ||
|
|
e0691dcd97 | ||
|
|
24442c29bb | ||
|
|
871315c2bc | ||
|
|
7b116b936c | ||
|
|
9aa84c03d7 | ||
|
|
471325a358 | ||
|
|
978ab18cee | ||
|
|
ea0c16e334 | ||
|
|
dd0ff298d5 | ||
|
|
eca0b30728 | ||
|
|
9ade4b5ae0 | ||
|
|
4f8c5189e0 | ||
|
|
0f2d54e8c0 | ||
|
|
c0fc1becb7 | ||
|
|
70e5b3520c | ||
|
|
ebec9877b0 | ||
|
|
d9a5d8aaac | ||
|
|
e58d05a85b | ||
|
|
3efddab070 | ||
|
|
a17a27005d | ||
|
|
a8f7f4679b | ||
|
|
11c0119944 | ||
|
|
80804ee7ac | ||
|
|
06e80823b8 | ||
|
|
ecda390dc0 | ||
|
|
481c443211 | ||
|
|
80b7b8eaec | ||
|
|
1e33fc7935 | ||
|
|
6b42d271ba | ||
|
|
7823d1a60b | ||
|
|
d23c4a5795 | ||
|
|
c91c40e79f | ||
|
|
5302f418db | ||
|
|
6370bc187a | ||
|
|
3e2dc4989e | ||
|
|
715cb86ab9 | ||
|
|
cf375eae84 | ||
|
|
4d4ab333f4 | ||
|
|
ca04c62ecd | ||
|
|
34f632e7e4 | ||
|
|
4f952b5daa | ||
|
|
edcb9cf275 | ||
|
|
ac44efbbbe | ||
|
|
e0a81dbd34 | ||
|
|
301c8580c8 | ||
|
|
9b9b278a14 | ||
|
|
31c6569910 | ||
|
|
ed3a837200 | ||
|
|
9cfa5f35cb | ||
|
|
cc13b60411 | ||
|
|
caa4fccabd | ||
|
|
4ba33002dd | ||
|
|
ef418be2df | ||
|
|
b7d8889180 | ||
|
|
18fe545533 | ||
|
|
87208b07a5 | ||
|
|
c713eafac0 | ||
|
|
d493cba990 | ||
|
|
ae188fd999 | ||
|
|
5747656be7 | ||
|
|
4e85d3d079 | ||
|
|
e2bd8b1512 | ||
|
|
422c34f945 | ||
|
|
8d0b646fbe | ||
|
|
2c4a7cc055 | ||
|
|
54a31689f7 | ||
|
|
6bf967b9d2 | ||
|
|
b6abd71392 | ||
|
|
8506640d7f | ||
|
|
592b7acfc4 | ||
|
|
858843ff9a | ||
|
|
9ffeba72fe | ||
|
|
4074823e4e | ||
|
|
b99172d932 | ||
|
|
017b03d200 | ||
|
|
ea12dd3c47 | ||
|
|
2b98543125 | ||
|
|
a0f13881bf | ||
|
|
62ae63d7f6 | ||
|
|
589ca19e4f | ||
|
|
b1785b1fa5 | ||
|
|
3fc26d279a | ||
|
|
bde477558f | ||
|
|
b5afe1fe29 | ||
|
|
8b2470faf8 | ||
|
|
7fcc3d0013 | ||
|
|
153b6ce6fe | ||
|
|
777c825cca | ||
|
|
33a8e02c5f | ||
|
|
c797c9db1a | ||
|
|
766e30a066 | ||
|
|
3894d92f1b | ||
|
|
2be2ffbcb6 | ||
|
|
570dff2696 | ||
|
|
be13295096 | ||
|
|
ea89d0a822 | ||
|
|
d7bb9483e4 | ||
|
|
7699e47157 | ||
|
|
978c50c271 | ||
|
|
0f5e644041 | ||
|
|
c5e891eceb | ||
|
|
147b2ce045 | ||
|
|
9553ec3d96 | ||
|
|
890d3fbfe1 | ||
|
|
b9ea8cc1a9 | ||
|
|
141c9ee2ab | ||
|
|
f1811ca53d | ||
|
|
5bf163f40c | ||
|
|
395647cff9 | ||
|
|
3f3ffdca13 | ||
|
|
34659cac70 | ||
|
|
d8820df26e | ||
|
|
6495bad6fe | ||
|
|
f918779fc8 | ||
|
|
8999fd625d | ||
|
|
113aaa1b81 | ||
|
|
ec29679c02 | ||
|
|
962d19f3cb | ||
|
|
3bec2d6e65 | ||
|
|
571812062f | ||
|
|
c4801f9889 | ||
|
|
ada2e6f480 | ||
|
|
fadf023563 | ||
|
|
5db6aa0695 | ||
|
|
1ca71bbc11 | ||
|
|
39b8429998 | ||
|
|
e05b626d51 | ||
|
|
146ca2353d | ||
|
|
6d5c5fb0c2 | ||
|
|
3d5885f5bd | ||
|
|
888ecad4d3 | ||
|
|
b3a64fdf0f | ||
|
|
dc8e361b6b | ||
|
|
15bc488f71 | ||
|
|
22b4cc6222 | ||
|
|
f5c03f8045 | ||
|
|
0a47becdc6 | ||
|
|
e4d264cc83 | ||
|
|
7c455b0daa | ||
|
|
d2a1add105 | ||
|
|
dcdfbc7394 | ||
|
|
2b5a1c0551 | ||
|
|
cb034b96a4 | ||
|
|
e741f83c06 | ||
|
|
b192360ef3 | ||
|
|
a028191c82 | ||
|
|
c6b27ad0e0 | ||
|
|
1d1d4cbddf | ||
|
|
747997f46c | ||
|
|
805cf517bb | ||
|
|
ea78b434d5 | ||
|
|
69f5fbef31 | ||
|
|
db75cac9bc | ||
|
|
ba4af74a9d | ||
|
|
fb474237ee | ||
|
|
d476f5d75b | ||
|
|
8720847f54 | ||
|
|
a12bd80cc1 | ||
|
|
ab3c551357 | ||
|
|
a1a7047370 | ||
|
|
29e43979a7 | ||
|
|
6b25de1361 | ||
|
|
53e68258f3 | ||
|
|
6e3c982a22 | ||
|
|
f0e812e9e2 | ||
|
|
9ff10135a8 | ||
|
|
9618b7bb94 | ||
|
|
fd768db70b | ||
|
|
3c5b6ba795 | ||
|
|
add09210ba | ||
|
|
6065b4f742 | ||
|
|
d90bf747fc | ||
|
|
ec8095ea9d | ||
|
|
b11849e45b | ||
|
|
e2f09d711d | ||
|
|
04b68e3a61 | ||
|
|
b21b93ff71 | ||
|
|
9f9df09c61 | ||
|
|
c7700904e0 | ||
|
|
a8b45851cc | ||
|
|
bf1a30505f | ||
|
|
7a55fee258 | ||
|
|
abc7805aab | ||
|
|
98db99b3ff | ||
|
|
07d08918a3 | ||
|
|
8371c9fedd | ||
|
|
b106e60880 | ||
|
|
81c04bc2b2 | ||
|
|
945a4c67e4 | ||
|
|
95644075ec | ||
|
|
19adb5e106 | ||
|
|
ff789200b9 | ||
|
|
b53b0784aa | ||
|
|
69056332d3 | ||
|
|
f5342caca6 | ||
|
|
039bf549a9 | ||
|
|
ea9e335a36 | ||
|
|
46e3f85903 | ||
|
|
b00d836c13 | ||
|
|
b587279450 | ||
|
|
279ef707d5 | ||
|
|
7e8be94530 | ||
|
|
c8262da5d7 | ||
|
|
81beb35809 | ||
|
|
eb302e5d19 | ||
|
|
9c54168173 | ||
|
|
3eb233ffe2 | ||
|
|
cf21ca9503 | ||
|
|
b6ecdcd0ea | ||
|
|
a975b0e24a | ||
|
|
84b198ed29 | ||
|
|
4cff75c531 | ||
|
|
916e9b65a6 | ||
|
|
eb6adea47c | ||
|
|
458daddf38 | ||
|
|
ea98fe1fbf | ||
|
|
c3b7814542 | ||
|
|
abe414765e | ||
|
|
9a93022478 | ||
|
|
033265d58e | ||
|
|
2bcdd25cc2 | ||
|
|
c38dfc2c20 | ||
|
|
ef5c77d8df | ||
|
|
b05c3c7e4a | ||
|
|
606ec51108 | ||
|
|
3dbc97701d | ||
|
|
31b3df08cc | ||
|
|
01718282ff | ||
|
|
1b5f414c89 | ||
|
|
a441670591 | ||
|
|
7c0f9b65f1 | ||
|
|
837a75c477 | ||
|
|
0a449c25eb | ||
|
|
88d5038f94 | ||
|
|
2de3197ca9 | ||
|
|
c72accb3cb | ||
|
|
00d89f12e8 | ||
|
|
565e31983c | ||
|
|
06aa772a02 | ||
|
|
a5b1cd6c99 | ||
|
|
d84be68f4c | ||
|
|
e01b51bf37 | ||
|
|
9c6bf5c16f | ||
|
|
9bf4ba9af9 | ||
|
|
a37c7ed8f8 | ||
|
|
36ba82dd16 | ||
|
|
802f8714ef | ||
|
|
bf6cf21084 | ||
|
|
30df8aec8a | ||
|
|
84bffdd287 | ||
|
|
6c4b5321e1 | ||
|
|
14ef07f072 | ||
|
|
52c2a831d9 | ||
|
|
d427270699 | ||
|
|
b25f2d0916 | ||
|
|
56fc18c378 | ||
|
|
38343c20cb | ||
|
|
6f1773d512 | ||
|
|
d9fc1fc5c9 | ||
|
|
3ad8b0db71 | ||
|
|
a021784d48 | ||
|
|
072f7bdffb | ||
|
|
b8a3aa5d8c | ||
|
|
ee91177ca6 | ||
|
|
3f02a76c3d | ||
|
|
c88753c7e9 | ||
|
|
94addc69f7 | ||
|
|
b4cfe21074 | ||
|
|
8437c8ada1 | ||
|
|
0a5109823c | ||
|
|
e863a5df6a | ||
|
|
547eb1c85d | ||
|
|
63c853d4ce | ||
|
|
329c9b7b92 | ||
|
|
0789a2f27e | ||
|
|
382116ab21 | ||
|
|
7290bff594 | ||
|
|
8b57c448ef | ||
|
|
e5cf54e92f | ||
|
|
34d9875ff3 | ||
|
|
070fbc8b52 | ||
|
|
9d57c22757 | ||
|
|
93f1a2b4fb | ||
|
|
a156962259 | ||
|
|
9d8fbf789f | ||
|
|
0d91be217b | ||
|
|
91a8fdc392 | ||
|
|
b79a762ce1 | ||
|
|
46770f4588 | ||
|
|
aae87d1684 | ||
|
|
0bac5f1ae9 | ||
|
|
457a4e8725 | ||
|
|
a5c8224023 | ||
|
|
001deeee66 | ||
|
|
7847299c77 | ||
|
|
ac77447066 | ||
|
|
f09c7be18d | ||
|
|
ee4eb35fe3 | ||
|
|
8834f492b4 | ||
|
|
f2cf788d86 | ||
|
|
c3c7e670fc | ||
|
|
c3d44647f9 | ||
|
|
115d540b7d | ||
|
|
ec4299fd88 | ||
|
|
c6785e3728 | ||
|
|
b2e4d45025 | ||
|
|
4170376945 | ||
|
|
01d2a937ca | ||
|
|
4e3be8513d | ||
|
|
c8f0d8bf0b | ||
|
|
bfe02ed6a3 | ||
|
|
3442df0ca4 | ||
|
|
b85a1e9c07 | ||
|
|
e7fd911fb8 | ||
|
|
d673c4900c | ||
|
|
49fb6dbbd6 | ||
|
|
5c88787401 | ||
|
|
873269e410 | ||
|
|
6740a7130b |
@@ -0,0 +1,21 @@
|
||||
BasedOnStyle: Chromium
|
||||
AlignAfterOpenBracket: true
|
||||
AlignOperands: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeTernaryOperators: true
|
||||
ColumnLimit: 160
|
||||
ContinuationIndentWidth: 4
|
||||
DerivePointerAlignment: false
|
||||
IndentWidth: 4
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCSpaceAfterProperty: true
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PointerBindsToType: false
|
||||
PointerAlignment: Right
|
||||
SpacesInContainerLiterals: true
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Build VoidLink IPA
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "feat/frame_pacing_metal" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build VoidLink IPA
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Checkout submodules
|
||||
run: "git submodule update --init --recursive"
|
||||
|
||||
- name: Build VoidLink
|
||||
run: |
|
||||
xcodebuild -verbose -scheme VoidLink -sdk iphoneos -configuration Debug archive -archivePath $PWD/build/VoidLink.xcarchive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED="NO" CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
|
||||
mkdir -p ./build/IPA && mkdir -p ./build/IPA/Payload
|
||||
cp -r ./build/VoidLink.xcarchive/Products/Applications/VoidLink.app ./build/IPA/Payload/VoidLink.app
|
||||
cd ./build/IPA && zip -r ../../build/VoidLink.ipa ./Payload/* && cd ../..
|
||||
|
||||
- name: Upload VoidLink IPA to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: VoidLink.ipa
|
||||
path: build/VoidLink.ipa
|
||||
@@ -1,5 +1,9 @@
|
||||
**/xcuserdata/
|
||||
**/Moonlight.xcscmblueprint
|
||||
**/xcschemes/*.xcscheme
|
||||
.DS_Store
|
||||
Build
|
||||
DerivedData
|
||||
*.xccheckout
|
||||
.github
|
||||
README.md
|
||||
@@ -1,6 +1,9 @@
|
||||
[submodule "moonlight-common/moonlight-common-c"]
|
||||
path = moonlight-common/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
url=https://github.com/TrueZhuangJia/voidlink-c.git
|
||||
[submodule "X1Kit"]
|
||||
path = X1Kit
|
||||
url = https://github.com/cgutman/X1Kit.git
|
||||
[submodule "third_party/imgui"]
|
||||
path = third_party/imgui
|
||||
url = https://github.com/ocornut/imgui.git
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// dear imgui: Renderer Backend for Metal
|
||||
// This needs to be used along with a Platform Backend (e.g. OSX)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
|
||||
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// ObjC API
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifdef __OBJC__
|
||||
|
||||
@class MTLRenderPassDescriptor;
|
||||
@protocol MTLDevice, MTLCommandBuffer, MTLRenderCommandEncoder;
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplMetal_Init(id<MTLDevice> device);
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor);
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* drawData,
|
||||
id<MTLCommandBuffer> commandBuffer,
|
||||
id<MTLRenderCommandEncoder> commandEncoder);
|
||||
|
||||
// Called by Init/NewFrame/Shutdown
|
||||
IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device);
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects();
|
||||
|
||||
// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = NULL to handle this manually.
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex);
|
||||
|
||||
#endif
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// C++ API
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// Enable Metal C++ binding support with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file
|
||||
// More info about using Metal from C++: https://developer.apple.com/metal/cpp/
|
||||
|
||||
#ifdef IMGUI_IMPL_METAL_CPP
|
||||
#include <Metal/Metal.hpp>
|
||||
#ifndef __OBJC__
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplMetal_Init(MTL::Device* device);
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor);
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data,
|
||||
MTL::CommandBuffer* commandBuffer,
|
||||
MTL::RenderCommandEncoder* commandEncoder);
|
||||
|
||||
// Called by Init/NewFrame/Shutdown
|
||||
IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device);
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects();
|
||||
|
||||
// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = NULL to handle this manually.
|
||||
IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex);
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
@@ -0,0 +1,657 @@
|
||||
// dear imgui: Renderer Backend for Metal
|
||||
// This needs to be used along with a Platform Backend (e.g. OSX)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
|
||||
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2025-06-11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas. Removed ImGui_ImplMetal_CreateFontsTexture() and ImGui_ImplMetal_DestroyFontsTexture().
|
||||
// 2025-02-03: Metal: Crash fix. (#8367)
|
||||
// 2024-01-08: Metal: Fixed memory leaks when using metal-cpp (#8276, #8166) or when using multiple contexts (#7419).
|
||||
// 2022-08-23: Metal: Update deprecated property 'sampleCount'->'rasterSampleCount'.
|
||||
// 2022-07-05: Metal: Add dispatch synchronization.
|
||||
// 2022-06-30: Metal: Use __bridge for ARC based systems.
|
||||
// 2022-06-01: Metal: Fixed null dereference on exit inside command buffer completion handler.
|
||||
// 2022-04-27: Misc: Store backend data in a per-context struct, allowing to use this backend with multiple contexts.
|
||||
// 2022-01-03: Metal: Ignore ImDrawCmd where ElemCount == 0 (very rare but can technically be manufactured by user code).
|
||||
// 2021-12-30: Metal: Added Metal C++ support. Enable with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file.
|
||||
// 2021-08-24: Metal: Fixed a crash when clipping rect larger than framebuffer is submitted. (#4464)
|
||||
// 2021-05-19: Metal: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
|
||||
// 2021-02-18: Metal: Change blending equation to preserve alpha in output buffer.
|
||||
// 2021-01-25: Metal: Fixed texture storage mode when building on Mac Catalyst.
|
||||
// 2019-05-29: Metal: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
|
||||
// 2019-04-30: Metal: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
|
||||
// 2019-02-11: Metal: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display.
|
||||
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
|
||||
// 2018-07-05: Metal: Added new Metal backend implementation.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_impl_metal.h"
|
||||
#import <time.h>
|
||||
#import <Metal/Metal.h>
|
||||
|
||||
#pragma mark - Support classes
|
||||
|
||||
// A wrapper around a MTLBuffer object that knows the last time it was reused
|
||||
@interface MetalBuffer : NSObject
|
||||
@property (nonatomic, strong) id<MTLBuffer> buffer;
|
||||
@property (nonatomic, assign) double lastReuseTime;
|
||||
- (instancetype)initWithBuffer:(id<MTLBuffer>)buffer;
|
||||
@end
|
||||
|
||||
// An object that encapsulates the data necessary to uniquely identify a
|
||||
// render pipeline state. These are used as cache keys.
|
||||
@interface FramebufferDescriptor : NSObject<NSCopying>
|
||||
@property (nonatomic, assign) unsigned long sampleCount;
|
||||
@property (nonatomic, assign) MTLPixelFormat colorPixelFormat;
|
||||
@property (nonatomic, assign) MTLPixelFormat depthPixelFormat;
|
||||
@property (nonatomic, assign) MTLPixelFormat stencilPixelFormat;
|
||||
- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor;
|
||||
@end
|
||||
|
||||
@interface MetalTexture : NSObject
|
||||
@property (nonatomic, strong) id<MTLTexture> metalTexture;
|
||||
- (instancetype)initWithTexture:(id<MTLTexture>)metalTexture;
|
||||
@end
|
||||
|
||||
// A singleton that stores long-lived objects that are needed by the Metal
|
||||
// renderer backend. Stores the render pipeline state cache and the default
|
||||
// font texture, and manages the reusable buffer cache.
|
||||
@interface MetalContext : NSObject
|
||||
@property (nonatomic, strong) id<MTLDevice> device;
|
||||
@property (nonatomic, strong) id<MTLDepthStencilState> depthStencilState;
|
||||
@property (nonatomic, strong) FramebufferDescriptor* framebufferDescriptor; // framebuffer descriptor for current frame; transient
|
||||
@property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache; // pipeline cache; keyed on framebuffer descriptors
|
||||
@property (nonatomic, strong) NSMutableArray<MetalBuffer*>* bufferCache;
|
||||
@property (nonatomic, assign) double lastBufferCachePurge;
|
||||
- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device;
|
||||
- (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device;
|
||||
@end
|
||||
|
||||
struct ImGui_ImplMetal_Data
|
||||
{
|
||||
MetalContext* SharedMetalContext;
|
||||
|
||||
ImGui_ImplMetal_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
static ImGui_ImplMetal_Data* ImGui_ImplMetal_GetBackendData() { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; }
|
||||
static void ImGui_ImplMetal_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal_GetBackendData()); }
|
||||
|
||||
// XXX Work around missing clock symbols on iOS. The NSDate version is the method this function used previously.
|
||||
#if defined(CLOCK_UPTIME_RAW)
|
||||
static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return (CFTimeInterval)(double)(clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1e9); }
|
||||
#else
|
||||
static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return (CFTimeInterval)[NSDate date].timeIntervalSince1970; }
|
||||
#endif
|
||||
|
||||
#ifdef IMGUI_IMPL_METAL_CPP
|
||||
|
||||
#pragma mark - Dear ImGui Metal C++ Backend API
|
||||
|
||||
bool ImGui_ImplMetal_Init(MTL::Device* device)
|
||||
{
|
||||
return ImGui_ImplMetal_Init((__bridge id<MTLDevice>)(device));
|
||||
}
|
||||
|
||||
void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor)
|
||||
{
|
||||
ImGui_ImplMetal_NewFrame((__bridge MTLRenderPassDescriptor*)(renderPassDescriptor));
|
||||
}
|
||||
|
||||
void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data,
|
||||
MTL::CommandBuffer* commandBuffer,
|
||||
MTL::RenderCommandEncoder* commandEncoder)
|
||||
{
|
||||
ImGui_ImplMetal_RenderDrawData(draw_data,
|
||||
(__bridge id<MTLCommandBuffer>)(commandBuffer),
|
||||
(__bridge id<MTLRenderCommandEncoder>)(commandEncoder));
|
||||
|
||||
}
|
||||
|
||||
bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device)
|
||||
{
|
||||
return ImGui_ImplMetal_CreateDeviceObjects((__bridge id<MTLDevice>)(device));
|
||||
}
|
||||
|
||||
#endif // #ifdef IMGUI_IMPL_METAL_CPP
|
||||
|
||||
#pragma mark - Dear ImGui Metal Backend API
|
||||
|
||||
bool ImGui_ImplMetal_Init(id<MTLDevice> device)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
IMGUI_CHECKVERSION();
|
||||
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
|
||||
|
||||
ImGui_ImplMetal_Data* bd = IM_NEW(ImGui_ImplMetal_Data)();
|
||||
io.BackendRendererUserData = (void*)bd;
|
||||
io.BackendRendererName = "imgui_impl_metal";
|
||||
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
|
||||
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
|
||||
|
||||
bd->SharedMetalContext = [[MetalContext alloc] init];
|
||||
bd->SharedMetalContext.device = device;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplMetal_Shutdown()
|
||||
{
|
||||
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||
IM_UNUSED(bd);
|
||||
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
|
||||
ImGui_ImplMetal_DestroyDeviceObjects();
|
||||
ImGui_ImplMetal_DestroyBackendData();
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.BackendRendererName = nullptr;
|
||||
io.BackendRendererUserData = nullptr;
|
||||
io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures);
|
||||
}
|
||||
|
||||
void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor)
|
||||
{
|
||||
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||
IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal_Init()?");
|
||||
#ifdef IMGUI_IMPL_METAL_CPP
|
||||
bd->SharedMetalContext.framebufferDescriptor = [[[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]autorelease];
|
||||
#else
|
||||
bd->SharedMetalContext.framebufferDescriptor = [[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor];
|
||||
#endif
|
||||
if (bd->SharedMetalContext.depthStencilState == nil)
|
||||
ImGui_ImplMetal_CreateDeviceObjects(bd->SharedMetalContext.device);
|
||||
}
|
||||
|
||||
static void ImGui_ImplMetal_SetupRenderState(ImDrawData* draw_data, id<MTLCommandBuffer> commandBuffer,
|
||||
id<MTLRenderCommandEncoder> commandEncoder, id<MTLRenderPipelineState> renderPipelineState,
|
||||
MetalBuffer* vertexBuffer, size_t vertexBufferOffset)
|
||||
{
|
||||
IM_UNUSED(commandBuffer);
|
||||
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||
[commandEncoder setCullMode:MTLCullModeNone];
|
||||
[commandEncoder setDepthStencilState:bd->SharedMetalContext.depthStencilState];
|
||||
|
||||
// Setup viewport, orthographic projection matrix
|
||||
// Our visible imgui space lies from draw_data->DisplayPos (top left) to
|
||||
// draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayMin is typically (0,0) for single viewport apps.
|
||||
MTLViewport viewport =
|
||||
{
|
||||
.originX = 0.0,
|
||||
.originY = 0.0,
|
||||
.width = (double)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x),
|
||||
.height = (double)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y),
|
||||
.znear = 0.0,
|
||||
.zfar = 1.0
|
||||
};
|
||||
[commandEncoder setViewport:viewport];
|
||||
|
||||
float L = draw_data->DisplayPos.x;
|
||||
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
|
||||
float T = draw_data->DisplayPos.y;
|
||||
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
|
||||
float N = (float)viewport.znear;
|
||||
float F = (float)viewport.zfar;
|
||||
const float ortho_projection[4][4] =
|
||||
{
|
||||
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
|
||||
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
|
||||
{ 0.0f, 0.0f, 1/(F-N), 0.0f },
|
||||
{ (R+L)/(L-R), (T+B)/(B-T), N/(F-N), 1.0f },
|
||||
};
|
||||
[commandEncoder setVertexBytes:&ortho_projection length:sizeof(ortho_projection) atIndex:1];
|
||||
|
||||
[commandEncoder setRenderPipelineState:renderPipelineState];
|
||||
|
||||
[commandEncoder setVertexBuffer:vertexBuffer.buffer offset:0 atIndex:0];
|
||||
[commandEncoder setVertexBufferOffset:vertexBufferOffset atIndex:0];
|
||||
}
|
||||
|
||||
// Metal Render function.
|
||||
void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id<MTLCommandBuffer> commandBuffer, id<MTLRenderCommandEncoder> commandEncoder)
|
||||
{
|
||||
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||
MetalContext* ctx = bd->SharedMetalContext;
|
||||
|
||||
// Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
|
||||
int fb_width = (int)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x);
|
||||
int fb_height = (int)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y);
|
||||
if (fb_width <= 0 || fb_height <= 0 || draw_data->CmdListsCount == 0)
|
||||
return;
|
||||
|
||||
// Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do.
|
||||
// (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates).
|
||||
if (draw_data->Textures != nullptr)
|
||||
for (ImTextureData* tex : *draw_data->Textures)
|
||||
if (tex->Status != ImTextureStatus_OK)
|
||||
ImGui_ImplMetal_UpdateTexture(tex);
|
||||
|
||||
// Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame
|
||||
// The hit rate for this cache should be very near 100%.
|
||||
id<MTLRenderPipelineState> renderPipelineState = ctx.renderPipelineStateCache[ctx.framebufferDescriptor];
|
||||
if (renderPipelineState == nil)
|
||||
{
|
||||
// No luck; make a new render pipeline state
|
||||
renderPipelineState = [ctx renderPipelineStateForFramebufferDescriptor:ctx.framebufferDescriptor device:commandBuffer.device];
|
||||
|
||||
// Cache render pipeline state for later reuse
|
||||
ctx.renderPipelineStateCache[ctx.framebufferDescriptor] = renderPipelineState;
|
||||
}
|
||||
|
||||
size_t vertexBufferLength = (size_t)draw_data->TotalVtxCount * sizeof(ImDrawVert);
|
||||
size_t indexBufferLength = (size_t)draw_data->TotalIdxCount * sizeof(ImDrawIdx);
|
||||
MetalBuffer* vertexBuffer = [ctx dequeueReusableBufferOfLength:vertexBufferLength device:commandBuffer.device];
|
||||
MetalBuffer* indexBuffer = [ctx dequeueReusableBufferOfLength:indexBufferLength device:commandBuffer.device];
|
||||
|
||||
ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0);
|
||||
|
||||
// Will project scissor/clipping rectangles into framebuffer space
|
||||
ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
|
||||
ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2)
|
||||
|
||||
// Render command lists
|
||||
size_t vertexBufferOffset = 0;
|
||||
size_t indexBufferOffset = 0;
|
||||
for (int n = 0; n < draw_data->CmdListsCount; n++)
|
||||
{
|
||||
const ImDrawList* draw_list = draw_data->CmdLists[n];
|
||||
|
||||
memcpy((char*)vertexBuffer.buffer.contents + vertexBufferOffset, draw_list->VtxBuffer.Data, (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert));
|
||||
memcpy((char*)indexBuffer.buffer.contents + indexBufferOffset, draw_list->IdxBuffer.Data, (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
|
||||
|
||||
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
|
||||
{
|
||||
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
|
||||
if (pcmd->UserCallback)
|
||||
{
|
||||
// User callback, registered via ImDrawList::AddCallback()
|
||||
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
|
||||
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
|
||||
ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset);
|
||||
else
|
||||
pcmd->UserCallback(draw_list, pcmd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Project scissor/clipping rectangles into framebuffer space
|
||||
ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
|
||||
ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y);
|
||||
|
||||
// Clamp to viewport as setScissorRect() won't accept values that are off bounds
|
||||
if (clip_min.x < 0.0f) { clip_min.x = 0.0f; }
|
||||
if (clip_min.y < 0.0f) { clip_min.y = 0.0f; }
|
||||
if (clip_max.x > fb_width) { clip_max.x = (float)fb_width; }
|
||||
if (clip_max.y > fb_height) { clip_max.y = (float)fb_height; }
|
||||
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
|
||||
continue;
|
||||
if (pcmd->ElemCount == 0) // drawIndexedPrimitives() validation doesn't accept this
|
||||
continue;
|
||||
|
||||
// Apply scissor/clipping rectangle
|
||||
MTLScissorRect scissorRect =
|
||||
{
|
||||
.x = NSUInteger(clip_min.x),
|
||||
.y = NSUInteger(clip_min.y),
|
||||
.width = NSUInteger(clip_max.x - clip_min.x),
|
||||
.height = NSUInteger(clip_max.y - clip_min.y)
|
||||
};
|
||||
[commandEncoder setScissorRect:scissorRect];
|
||||
|
||||
// Bind texture, Draw
|
||||
if (ImTextureID tex_id = pcmd->GetTexID())
|
||||
[commandEncoder setFragmentTexture:(__bridge id<MTLTexture>)(void*)(intptr_t)(tex_id) atIndex:0];
|
||||
|
||||
[commandEncoder setVertexBufferOffset:(vertexBufferOffset + pcmd->VtxOffset * sizeof(ImDrawVert)) atIndex:0];
|
||||
[commandEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
|
||||
indexCount:pcmd->ElemCount
|
||||
indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32
|
||||
indexBuffer:indexBuffer.buffer
|
||||
indexBufferOffset:indexBufferOffset + pcmd->IdxOffset * sizeof(ImDrawIdx)];
|
||||
}
|
||||
}
|
||||
|
||||
vertexBufferOffset += (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert);
|
||||
indexBufferOffset += (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx);
|
||||
}
|
||||
|
||||
MetalContext* sharedMetalContext = bd->SharedMetalContext;
|
||||
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer>)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@synchronized(sharedMetalContext.bufferCache)
|
||||
{
|
||||
[sharedMetalContext.bufferCache addObject:vertexBuffer];
|
||||
[sharedMetalContext.bufferCache addObject:indexBuffer];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
static void ImGui_ImplMetal_DestroyTexture(ImTextureData* tex)
|
||||
{
|
||||
MetalTexture* backend_tex = (__bridge_transfer MetalTexture*)(tex->BackendUserData);
|
||||
if (backend_tex == nullptr)
|
||||
return;
|
||||
IM_ASSERT(backend_tex.metalTexture == (__bridge id<MTLTexture>)(void*)(intptr_t)tex->TexID);
|
||||
backend_tex.metalTexture = nil;
|
||||
|
||||
// Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running)
|
||||
tex->SetTexID(ImTextureID_Invalid);
|
||||
tex->SetStatus(ImTextureStatus_Destroyed);
|
||||
tex->BackendUserData = nullptr;
|
||||
}
|
||||
|
||||
void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex)
|
||||
{
|
||||
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||
if (tex->Status == ImTextureStatus_WantCreate)
|
||||
{
|
||||
// Create and upload new texture to graphics system
|
||||
//IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height);
|
||||
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr);
|
||||
IM_ASSERT(tex->Format == ImTextureFormat_RGBA32);
|
||||
|
||||
// We are retrieving and uploading the font atlas as a 4-channels RGBA texture here.
|
||||
// In theory we could call GetTexDataAsAlpha8() and upload a 1-channel texture to save on memory access bandwidth.
|
||||
// However, using a shader designed for 1-channel texture would make it less obvious to use the ImTextureID facility to render users own textures.
|
||||
// You can make that change in your implementation.
|
||||
MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
||||
width:(NSUInteger)tex->Width
|
||||
height:(NSUInteger)tex->Height
|
||||
mipmapped:NO];
|
||||
textureDescriptor.usage = MTLTextureUsageShaderRead;
|
||||
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
|
||||
textureDescriptor.storageMode = MTLStorageModeManaged;
|
||||
#else
|
||||
textureDescriptor.storageMode = MTLStorageModeShared;
|
||||
#endif
|
||||
id <MTLTexture> texture = [bd->SharedMetalContext.device newTextureWithDescriptor:textureDescriptor];
|
||||
[texture replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)tex->Width, (NSUInteger)tex->Height) mipmapLevel:0 withBytes:tex->Pixels bytesPerRow:(NSUInteger)tex->Width * 4];
|
||||
MetalTexture* backend_tex = [[MetalTexture alloc] initWithTexture:texture];
|
||||
|
||||
// Store identifiers
|
||||
tex->SetTexID((ImTextureID)(intptr_t)texture);
|
||||
tex->SetStatus(ImTextureStatus_OK);
|
||||
tex->BackendUserData = (__bridge_retained void*)(backend_tex);
|
||||
}
|
||||
else if (tex->Status == ImTextureStatus_WantUpdates)
|
||||
{
|
||||
// Update selected blocks. We only ever write to textures regions which have never been used before!
|
||||
// This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region.
|
||||
MetalTexture* backend_tex = (__bridge MetalTexture*)(tex->BackendUserData);
|
||||
for (ImTextureRect& r : tex->Updates)
|
||||
{
|
||||
[backend_tex.metalTexture replaceRegion:MTLRegionMake2D((NSUInteger)r.x, (NSUInteger)r.y, (NSUInteger)r.w, (NSUInteger)r.h)
|
||||
mipmapLevel:0
|
||||
withBytes:tex->GetPixelsAt(r.x, r.y)
|
||||
bytesPerRow:(NSUInteger)tex->Width * 4];
|
||||
}
|
||||
tex->SetStatus(ImTextureStatus_OK);
|
||||
}
|
||||
else if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0)
|
||||
{
|
||||
ImGui_ImplMetal_DestroyTexture(tex);
|
||||
}
|
||||
}
|
||||
|
||||
bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
|
||||
{
|
||||
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||
MTLDepthStencilDescriptor* depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init];
|
||||
depthStencilDescriptor.depthWriteEnabled = NO;
|
||||
depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionAlways;
|
||||
bd->SharedMetalContext.depthStencilState = [device newDepthStencilStateWithDescriptor:depthStencilDescriptor];
|
||||
#ifdef IMGUI_IMPL_METAL_CPP
|
||||
[depthStencilDescriptor release];
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplMetal_DestroyDeviceObjects()
|
||||
{
|
||||
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||
|
||||
// Destroy all textures
|
||||
for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
|
||||
if (tex->RefCount == 1)
|
||||
ImGui_ImplMetal_DestroyTexture(tex);
|
||||
|
||||
[bd->SharedMetalContext.renderPipelineStateCache removeAllObjects];
|
||||
}
|
||||
|
||||
#pragma mark - MetalBuffer implementation
|
||||
|
||||
@implementation MetalBuffer
|
||||
- (instancetype)initWithBuffer:(id<MTLBuffer>)buffer
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
_buffer = buffer;
|
||||
_lastReuseTime = GetMachAbsoluteTimeInSeconds();
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
|
||||
#pragma mark - FramebufferDescriptor implementation
|
||||
|
||||
@implementation FramebufferDescriptor
|
||||
- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
_sampleCount = renderPassDescriptor.colorAttachments[0].texture.sampleCount;
|
||||
_colorPixelFormat = renderPassDescriptor.colorAttachments[0].texture.pixelFormat;
|
||||
_depthPixelFormat = renderPassDescriptor.depthAttachment.texture.pixelFormat;
|
||||
_stencilPixelFormat = renderPassDescriptor.stencilAttachment.texture.pixelFormat;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nonnull id)copyWithZone:(nullable NSZone*)zone
|
||||
{
|
||||
FramebufferDescriptor* copy = [[FramebufferDescriptor allocWithZone:zone] init];
|
||||
copy.sampleCount = self.sampleCount;
|
||||
copy.colorPixelFormat = self.colorPixelFormat;
|
||||
copy.depthPixelFormat = self.depthPixelFormat;
|
||||
copy.stencilPixelFormat = self.stencilPixelFormat;
|
||||
return copy;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
NSUInteger sc = _sampleCount & 0x3;
|
||||
NSUInteger cf = _colorPixelFormat & 0x3FF;
|
||||
NSUInteger df = _depthPixelFormat & 0x3FF;
|
||||
NSUInteger sf = _stencilPixelFormat & 0x3FF;
|
||||
NSUInteger hash = (sf << 22) | (df << 12) | (cf << 2) | sc;
|
||||
return hash;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
FramebufferDescriptor* other = object;
|
||||
if (![other isKindOfClass:[FramebufferDescriptor class]])
|
||||
return NO;
|
||||
return other.sampleCount == self.sampleCount &&
|
||||
other.colorPixelFormat == self.colorPixelFormat &&
|
||||
other.depthPixelFormat == self.depthPixelFormat &&
|
||||
other.stencilPixelFormat == self.stencilPixelFormat;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - MetalTexture implementation
|
||||
|
||||
@implementation MetalTexture
|
||||
- (instancetype)initWithTexture:(id<MTLTexture>)metalTexture
|
||||
{
|
||||
if ((self = [super init]))
|
||||
self.metalTexture = metalTexture;
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - MetalContext implementation
|
||||
|
||||
@implementation MetalContext
|
||||
- (instancetype)init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
self.renderPipelineStateCache = [NSMutableDictionary dictionary];
|
||||
self.bufferCache = [NSMutableArray array];
|
||||
_lastBufferCachePurge = GetMachAbsoluteTimeInSeconds();
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device
|
||||
{
|
||||
uint64_t now = GetMachAbsoluteTimeInSeconds();
|
||||
|
||||
@synchronized(self.bufferCache)
|
||||
{
|
||||
// Purge old buffers that haven't been useful for a while
|
||||
if (now - self.lastBufferCachePurge > 1.0)
|
||||
{
|
||||
NSMutableArray* survivors = [NSMutableArray array];
|
||||
for (MetalBuffer* candidate in self.bufferCache)
|
||||
if (candidate.lastReuseTime > self.lastBufferCachePurge)
|
||||
[survivors addObject:candidate];
|
||||
self.bufferCache = [survivors mutableCopy];
|
||||
self.lastBufferCachePurge = now;
|
||||
}
|
||||
|
||||
// See if we have a buffer we can reuse
|
||||
MetalBuffer* bestCandidate = nil;
|
||||
for (MetalBuffer* candidate in self.bufferCache)
|
||||
if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime))
|
||||
bestCandidate = candidate;
|
||||
|
||||
if (bestCandidate != nil)
|
||||
{
|
||||
[self.bufferCache removeObject:bestCandidate];
|
||||
bestCandidate.lastReuseTime = now;
|
||||
return bestCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
// No luck; make a new buffer
|
||||
id<MTLBuffer> backing = [device newBufferWithLength:length options:MTLResourceStorageModeShared];
|
||||
return [[MetalBuffer alloc] initWithBuffer:backing];
|
||||
}
|
||||
|
||||
// Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling.
|
||||
- (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device
|
||||
{
|
||||
NSError* error = nil;
|
||||
|
||||
NSString* shaderSource = @""
|
||||
"#include <metal_stdlib>\n"
|
||||
"using namespace metal;\n"
|
||||
"\n"
|
||||
"struct Uniforms {\n"
|
||||
" float4x4 projectionMatrix;\n"
|
||||
"};\n"
|
||||
"\n"
|
||||
"struct VertexIn {\n"
|
||||
" float2 position [[attribute(0)]];\n"
|
||||
" float2 texCoords [[attribute(1)]];\n"
|
||||
" uchar4 color [[attribute(2)]];\n"
|
||||
"};\n"
|
||||
"\n"
|
||||
"struct VertexOut {\n"
|
||||
" float4 position [[position]];\n"
|
||||
" float2 texCoords;\n"
|
||||
" float4 color;\n"
|
||||
"};\n"
|
||||
"\n"
|
||||
"vertex VertexOut vertex_main(VertexIn in [[stage_in]],\n"
|
||||
" constant Uniforms &uniforms [[buffer(1)]]) {\n"
|
||||
" VertexOut out;\n"
|
||||
" out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);\n"
|
||||
" out.texCoords = in.texCoords;\n"
|
||||
" out.color = float4(in.color) / float4(255.0);\n"
|
||||
" return out;\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"fragment half4 fragment_main(VertexOut in [[stage_in]],\n"
|
||||
" texture2d<half, access::sample> texture [[texture(0)]]) {\n"
|
||||
" constexpr sampler linearSampler(coord::normalized, min_filter::linear, mag_filter::linear, mip_filter::linear);\n"
|
||||
" half4 texColor = texture.sample(linearSampler, in.texCoords);\n"
|
||||
" return half4(in.color) * texColor;\n"
|
||||
"}\n";
|
||||
|
||||
id<MTLLibrary> library = [device newLibraryWithSource:shaderSource options:nil error:&error];
|
||||
if (library == nil)
|
||||
{
|
||||
NSLog(@"Error: failed to create Metal library: %@", error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
id<MTLFunction> vertexFunction = [library newFunctionWithName:@"vertex_main"];
|
||||
id<MTLFunction> fragmentFunction = [library newFunctionWithName:@"fragment_main"];
|
||||
|
||||
if (vertexFunction == nil || fragmentFunction == nil)
|
||||
{
|
||||
NSLog(@"Error: failed to find Metal shader functions in library: %@", error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
MTLVertexDescriptor* vertexDescriptor = [MTLVertexDescriptor vertexDescriptor];
|
||||
vertexDescriptor.attributes[0].offset = offsetof(ImDrawVert, pos);
|
||||
vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position
|
||||
vertexDescriptor.attributes[0].bufferIndex = 0;
|
||||
vertexDescriptor.attributes[1].offset = offsetof(ImDrawVert, uv);
|
||||
vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; // texCoords
|
||||
vertexDescriptor.attributes[1].bufferIndex = 0;
|
||||
vertexDescriptor.attributes[2].offset = offsetof(ImDrawVert, col);
|
||||
vertexDescriptor.attributes[2].format = MTLVertexFormatUChar4; // color
|
||||
vertexDescriptor.attributes[2].bufferIndex = 0;
|
||||
vertexDescriptor.layouts[0].stepRate = 1;
|
||||
vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
|
||||
vertexDescriptor.layouts[0].stride = sizeof(ImDrawVert);
|
||||
|
||||
MTLRenderPipelineDescriptor* pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
|
||||
pipelineDescriptor.vertexFunction = vertexFunction;
|
||||
pipelineDescriptor.fragmentFunction = fragmentFunction;
|
||||
pipelineDescriptor.vertexDescriptor = vertexDescriptor;
|
||||
pipelineDescriptor.rasterSampleCount = self.framebufferDescriptor.sampleCount;
|
||||
pipelineDescriptor.colorAttachments[0].pixelFormat = self.framebufferDescriptor.colorPixelFormat;
|
||||
pipelineDescriptor.colorAttachments[0].blendingEnabled = YES;
|
||||
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
|
||||
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
|
||||
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
||||
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
|
||||
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
|
||||
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
||||
pipelineDescriptor.depthAttachmentPixelFormat = self.framebufferDescriptor.depthPixelFormat;
|
||||
pipelineDescriptor.stencilAttachmentPixelFormat = self.framebufferDescriptor.stencilPixelFormat;
|
||||
|
||||
id<MTLRenderPipelineState> renderPipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error];
|
||||
if (error != nil)
|
||||
NSLog(@"Error: failed to create Metal pipeline state: %@", error);
|
||||
|
||||
return renderPipelineState;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// ComputerScrollView.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 9/30/15.
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface ComputerScrollView : UIScrollView
|
||||
|
||||
@end
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// ComputerScrollView.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 9/30/15.
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ComputerScrollView.h"
|
||||
|
||||
@implementation ComputerScrollView
|
||||
|
||||
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
|
||||
if ([view isKindOfClass:[UIButton class]]) {
|
||||
return YES;
|
||||
}
|
||||
return [super touchesShouldCancelInContentView:view];
|
||||
}
|
||||
|
||||
/*
|
||||
// Only override drawRect: if you perform custom drawing.
|
||||
// An empty implementation adversely affects performance during animation.
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
// Drawing code
|
||||
}
|
||||
*/
|
||||
|
||||
@end
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// DataManager.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/28/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "TemporaryHost.h"
|
||||
#import "TemporaryApp.h"
|
||||
#import "TemporarySettings.h"
|
||||
|
||||
@interface DataManager : NSObject
|
||||
|
||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
||||
framerate:(NSInteger)framerate
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
audioConfig:(NSInteger)audioConfig
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
swapABXYButtons:(BOOL)swapABXYButtons
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
preferredCodec:(uint32_t)preferredCodec
|
||||
useFramePacing:(BOOL)useFramePacing
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
statsOverlay:(BOOL)statsOverlay;
|
||||
|
||||
- (NSArray*) getHosts;
|
||||
- (void) updateHost:(TemporaryHost*)host;
|
||||
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
|
||||
- (void) removeHost:(TemporaryHost*)host;
|
||||
- (void) removeApp:(TemporaryApp*)app;
|
||||
|
||||
- (TemporarySettings*) getSettings;
|
||||
|
||||
- (void) updateUniqueId:(NSString*)uniqueId;
|
||||
- (NSString*) getUniqueId;
|
||||
|
||||
@end
|
||||
@@ -1,40 +0,0 @@
|
||||
//
|
||||
// TemporarySettings.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 12/1/15.
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Settings+CoreDataClass.h"
|
||||
|
||||
@interface TemporarySettings : NSObject
|
||||
|
||||
@property (nonatomic, retain) Settings * parent;
|
||||
|
||||
@property (nonatomic, retain) NSNumber * bitrate;
|
||||
@property (nonatomic, retain) NSNumber * framerate;
|
||||
@property (nonatomic, retain) NSNumber * height;
|
||||
@property (nonatomic, retain) NSNumber * width;
|
||||
@property (nonatomic, retain) NSNumber * audioConfig;
|
||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
||||
@property (nonatomic, retain) NSString * uniqueId;
|
||||
@property (nonatomic) enum {
|
||||
CODEC_PREF_AUTO,
|
||||
CODEC_PREF_H264,
|
||||
CODEC_PREF_HEVC,
|
||||
CODEC_PREF_AV1,
|
||||
} preferredCodec;
|
||||
@property (nonatomic) BOOL useFramePacing;
|
||||
@property (nonatomic) BOOL multiController;
|
||||
@property (nonatomic) BOOL swapABXYButtons;
|
||||
@property (nonatomic) BOOL playAudioOnPC;
|
||||
@property (nonatomic) BOOL optimizeGames;
|
||||
@property (nonatomic) BOOL enableHdr;
|
||||
@property (nonatomic) BOOL btMouseSupport;
|
||||
@property (nonatomic) BOOL absoluteTouchMode;
|
||||
@property (nonatomic) BOOL statsOverlay;
|
||||
|
||||
- (id) initFromSettings:(Settings*)settings;
|
||||
|
||||
@end
|
||||
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,4 +0,0 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// OnScreenControls.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 12/28/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class ControllerSupport;
|
||||
@class StreamConfiguration;
|
||||
|
||||
@interface OnScreenControls : NSObject
|
||||
|
||||
typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
OnScreenControlsLevelOff,
|
||||
OnScreenControlsLevelAuto,
|
||||
OnScreenControlsLevelSimple,
|
||||
OnScreenControlsLevelFull,
|
||||
|
||||
// Internal levels selected by ControllerSupport
|
||||
OnScreenControlsLevelAutoGCGamepad,
|
||||
OnScreenControlsLevelAutoGCExtendedGamepad,
|
||||
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
|
||||
};
|
||||
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig;
|
||||
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||
- (void) setLevel:(OnScreenControlsLevel)level;
|
||||
- (OnScreenControlsLevel) getLevel;
|
||||
- (void) show;
|
||||
|
||||
@end
|
||||
@@ -1,985 +0,0 @@
|
||||
//
|
||||
// OnScreenControls.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 12/28/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OnScreenControls.h"
|
||||
#import "StreamView.h"
|
||||
#import "ControllerSupport.h"
|
||||
#import "Controller.h"
|
||||
#include "Limelight.h"
|
||||
|
||||
#define UPDATE_BUTTON(x, y) (buttonFlags = \
|
||||
(y) ? (buttonFlags | (x)) : (buttonFlags & ~(x)))
|
||||
|
||||
@implementation OnScreenControls {
|
||||
CALayer* _aButton;
|
||||
CALayer* _bButton;
|
||||
CALayer* _xButton;
|
||||
CALayer* _yButton;
|
||||
CALayer* _upButton;
|
||||
CALayer* _downButton;
|
||||
CALayer* _leftButton;
|
||||
CALayer* _rightButton;
|
||||
CALayer* _leftStickBackground;
|
||||
CALayer* _leftStick;
|
||||
CALayer* _rightStickBackground;
|
||||
CALayer* _rightStick;
|
||||
CALayer* _startButton;
|
||||
CALayer* _selectButton;
|
||||
CALayer* _r1Button;
|
||||
CALayer* _r2Button;
|
||||
CALayer* _r3Button;
|
||||
CALayer* _l1Button;
|
||||
CALayer* _l2Button;
|
||||
CALayer* _l3Button;
|
||||
|
||||
UITouch* _aTouch;
|
||||
UITouch* _bTouch;
|
||||
UITouch* _xTouch;
|
||||
UITouch* _yTouch;
|
||||
UITouch* _dpadTouch;
|
||||
UITouch* _lsTouch;
|
||||
UITouch* _rsTouch;
|
||||
UITouch* _startTouch;
|
||||
UITouch* _selectTouch;
|
||||
UITouch* _r1Touch;
|
||||
UITouch* _r2Touch;
|
||||
UITouch* _r3Touch;
|
||||
UITouch* _l1Touch;
|
||||
UITouch* _l2Touch;
|
||||
UITouch* _l3Touch;
|
||||
|
||||
NSDate* l3TouchStart;
|
||||
NSDate* r3TouchStart;
|
||||
|
||||
BOOL l3Set;
|
||||
BOOL r3Set;
|
||||
|
||||
BOOL _iPad;
|
||||
CGRect _controlArea;
|
||||
UIView* _view;
|
||||
OnScreenControlsLevel _level;
|
||||
BOOL _visible;
|
||||
|
||||
ControllerSupport *_controllerSupport;
|
||||
Controller *_controller;
|
||||
NSMutableArray* _deadTouches;
|
||||
BOOL _swapABXY;
|
||||
}
|
||||
|
||||
static const float EDGE_WIDTH = .05;
|
||||
|
||||
//static const float BUTTON_SIZE = 50;
|
||||
static const float BUTTON_DIST = 20;
|
||||
static float BUTTON_CENTER_X;
|
||||
static float BUTTON_CENTER_Y;
|
||||
|
||||
static const float D_PAD_DIST = 10;
|
||||
static float D_PAD_CENTER_X;
|
||||
static float D_PAD_CENTER_Y;
|
||||
|
||||
static const float DEAD_ZONE_PADDING = 15;
|
||||
|
||||
static const double STICK_CLICK_RATE = 100;
|
||||
static const float STICK_DEAD_ZONE = .1;
|
||||
static float STICK_INNER_SIZE;
|
||||
static float STICK_OUTER_SIZE;
|
||||
static float LS_CENTER_X;
|
||||
static float LS_CENTER_Y;
|
||||
static float RS_CENTER_X;
|
||||
static float RS_CENTER_Y;
|
||||
|
||||
static float START_X;
|
||||
static float START_Y;
|
||||
|
||||
static float SELECT_X;
|
||||
static float SELECT_Y;
|
||||
|
||||
static float R1_X;
|
||||
static float R1_Y;
|
||||
static float R2_X;
|
||||
static float R2_Y;
|
||||
static float R3_X;
|
||||
static float R3_Y;
|
||||
static float L1_X;
|
||||
static float L1_Y;
|
||||
static float L2_X;
|
||||
static float L2_Y;
|
||||
static float L3_X;
|
||||
static float L3_Y;
|
||||
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig {
|
||||
self = [self init];
|
||||
_view = view;
|
||||
_controllerSupport = controllerSupport;
|
||||
_controller = [controllerSupport getOscController];
|
||||
_deadTouches = [[NSMutableArray alloc] init];
|
||||
_swapABXY = streamConfig.swapABXYButtons;
|
||||
|
||||
_iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
|
||||
_controlArea = CGRectMake(0, 0, _view.frame.size.width, _view.frame.size.height);
|
||||
if (_iPad)
|
||||
{
|
||||
// Cut down the control area on an iPad so the controls are more reachable
|
||||
_controlArea.size.height = _view.frame.size.height / 2.0;
|
||||
_controlArea.origin.y = _view.frame.size.height - _controlArea.size.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
_controlArea.origin.x = _controlArea.size.width * EDGE_WIDTH;
|
||||
_controlArea.size.width -= _controlArea.origin.x * 2;
|
||||
}
|
||||
|
||||
_aButton = [CALayer layer];
|
||||
_bButton = [CALayer layer];
|
||||
_xButton = [CALayer layer];
|
||||
_yButton = [CALayer layer];
|
||||
_upButton = [CALayer layer];
|
||||
_downButton = [CALayer layer];
|
||||
_leftButton = [CALayer layer];
|
||||
_rightButton = [CALayer layer];
|
||||
_l1Button = [CALayer layer];
|
||||
_r1Button = [CALayer layer];
|
||||
_l2Button = [CALayer layer];
|
||||
_r2Button = [CALayer layer];
|
||||
_l3Button = [CALayer layer];
|
||||
_r3Button = [CALayer layer];
|
||||
_startButton = [CALayer layer];
|
||||
_selectButton = [CALayer layer];
|
||||
_leftStickBackground = [CALayer layer];
|
||||
_rightStickBackground = [CALayer layer];
|
||||
_leftStick = [CALayer layer];
|
||||
_rightStick = [CALayer layer];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) show {
|
||||
_visible = YES;
|
||||
|
||||
[self updateControls];
|
||||
}
|
||||
|
||||
- (void) setLevel:(OnScreenControlsLevel)level {
|
||||
_level = level;
|
||||
|
||||
// Only update controls if we're showing, otherwise
|
||||
// show will do it for us.
|
||||
if (_visible) {
|
||||
[self updateControls];
|
||||
}
|
||||
}
|
||||
|
||||
- (OnScreenControlsLevel) getLevel {
|
||||
return _level;
|
||||
}
|
||||
|
||||
- (void) updateControls {
|
||||
switch (_level) {
|
||||
case OnScreenControlsLevelOff:
|
||||
[self hideButtons];
|
||||
[self hideBumpers];
|
||||
[self hideTriggers];
|
||||
[self hideStartSelect];
|
||||
[self hideSticks];
|
||||
[self hideL3R3];
|
||||
break;
|
||||
case OnScreenControlsLevelAutoGCGamepad:
|
||||
// GCGamepad is missing triggers, both analog sticks,
|
||||
// and the select button
|
||||
[self setupGamepadControls];
|
||||
|
||||
[self hideButtons];
|
||||
[self hideBumpers];
|
||||
[self hideL3R3];
|
||||
[self drawTriggers];
|
||||
[self drawStartSelect];
|
||||
[self drawSticks];
|
||||
break;
|
||||
case OnScreenControlsLevelAutoGCExtendedGamepad:
|
||||
// GCExtendedGamepad is missing R3, L3, and select
|
||||
[self setupExtendedGamepadControls];
|
||||
|
||||
[self hideButtons];
|
||||
[self hideBumpers];
|
||||
[self hideTriggers];
|
||||
[self drawStartSelect];
|
||||
[self hideSticks];
|
||||
[self drawL3R3];
|
||||
break;
|
||||
case OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons:
|
||||
// This variant of GCExtendedGamepad has L3 and R3 but
|
||||
// is still missing Select
|
||||
[self setupExtendedGamepadControls];
|
||||
|
||||
[self hideButtons];
|
||||
[self hideBumpers];
|
||||
[self hideTriggers];
|
||||
[self hideL3R3];
|
||||
[self drawStartSelect];
|
||||
[self hideSticks];
|
||||
break;
|
||||
case OnScreenControlsLevelSimple:
|
||||
[self setupSimpleControls];
|
||||
|
||||
[self hideTriggers];
|
||||
[self hideL3R3];
|
||||
[self hideBumpers];
|
||||
[self hideSticks];
|
||||
[self drawStartSelect];
|
||||
[self drawButtons];
|
||||
break;
|
||||
case OnScreenControlsLevelFull:
|
||||
[self setupComplexControls];
|
||||
|
||||
[self drawButtons];
|
||||
[self drawStartSelect];
|
||||
[self drawBumpers];
|
||||
[self drawTriggers];
|
||||
[self drawSticks];
|
||||
[self hideL3R3]; // Full controls don't need these they have the sticks
|
||||
break;
|
||||
default:
|
||||
Log(LOG_W, @"Unknown on-screen controls level: %d", (int)_level);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// For GCExtendedGamepad controls we move start, select, L3, and R3 to the button
|
||||
- (void) setupExtendedGamepadControls {
|
||||
// Start with the default complex layout
|
||||
[self setupComplexControls];
|
||||
|
||||
START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
SELECT_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
L3_Y = _controlArea.size.height * .85 + _controlArea.origin.y;
|
||||
R3_Y = _controlArea.size.height * .85 + _controlArea.origin.y;
|
||||
|
||||
if (_iPad) {
|
||||
L3_X = _controlArea.size.width * .15 + _controlArea.origin.x;
|
||||
R3_X = _controlArea.size.width * .85 + _controlArea.origin.x;
|
||||
}
|
||||
else {
|
||||
L3_X = _controlArea.size.width * .25 + _controlArea.origin.x;
|
||||
R3_X = _controlArea.size.width * .75 + _controlArea.origin.x;
|
||||
}
|
||||
}
|
||||
|
||||
// For GCGamepad controls we move triggers, start, and select
|
||||
// to sit right above the analog sticks
|
||||
- (void) setupGamepadControls {
|
||||
// Start with the default complex layout
|
||||
[self setupComplexControls];
|
||||
|
||||
L2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
|
||||
R2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
R2_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
|
||||
START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .95 + _controlArea.origin.y;
|
||||
SELECT_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .95 + _controlArea.origin.y;
|
||||
|
||||
if (_iPad) {
|
||||
// The analog sticks are kept closer to the sides on iPad
|
||||
LS_CENTER_X = _controlArea.size.width * .15 + _controlArea.origin.x;
|
||||
RS_CENTER_X = _controlArea.size.width * .85 + _controlArea.origin.x;
|
||||
}
|
||||
}
|
||||
|
||||
// For simple controls we move the triggers and buttons to the bottom
|
||||
- (void) setupSimpleControls {
|
||||
// Start with the default complex layout
|
||||
[self setupComplexControls];
|
||||
|
||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
|
||||
R2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
|
||||
if (_iPad) {
|
||||
// Lower the D-pad and buttons on iPad
|
||||
D_PAD_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
BUTTON_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
|
||||
// Move Start and Select closer to sides
|
||||
SELECT_X = _controlArea.size.width * .2 + _controlArea.origin.x;
|
||||
START_X = _controlArea.size.width * .8 + _controlArea.origin.x;
|
||||
}
|
||||
else {
|
||||
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
|
||||
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setupComplexControls
|
||||
{
|
||||
D_PAD_CENTER_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
D_PAD_CENTER_Y = _controlArea.size.height * .60 + _controlArea.origin.y;
|
||||
BUTTON_CENTER_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
BUTTON_CENTER_Y = _controlArea.size.height * .60 + _controlArea.origin.y;
|
||||
|
||||
if (_iPad)
|
||||
{
|
||||
// The analog sticks are kept closer to the sides on iPad
|
||||
LS_CENTER_X = _controlArea.size.width * .22 + _controlArea.origin.x;
|
||||
LS_CENTER_Y = _controlArea.size.height * .80 + _controlArea.origin.y;
|
||||
RS_CENTER_X = _controlArea.size.width * .77 + _controlArea.origin.x;
|
||||
RS_CENTER_Y = _controlArea.size.height * .80 + _controlArea.origin.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
LS_CENTER_X = _controlArea.size.width * .35 + _controlArea.origin.x;
|
||||
LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
RS_CENTER_X = _controlArea.size.width * .65 + _controlArea.origin.x;
|
||||
RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
}
|
||||
|
||||
START_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
SELECT_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
L1_Y = _controlArea.size.height * .27 + _controlArea.origin.y;
|
||||
L2_Y = _controlArea.size.height * .1 + _controlArea.origin.y;
|
||||
R1_Y = _controlArea.size.height * .27 + _controlArea.origin.y;
|
||||
R2_Y = _controlArea.size.height * .1 + _controlArea.origin.y;
|
||||
|
||||
if (_iPad) {
|
||||
// Move L/R buttons closer to the side on iPad
|
||||
L1_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
R1_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
R2_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
}
|
||||
else {
|
||||
L1_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
R1_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) drawButtons {
|
||||
UIImage* aButtonImage = [UIImage imageNamed:@"AButton"];
|
||||
UIImage* bButtonImage = [UIImage imageNamed:@"BButton"];
|
||||
UIImage* xButtonImage = [UIImage imageNamed:@"XButton"];
|
||||
UIImage* yButtonImage = [UIImage imageNamed:@"YButton"];
|
||||
|
||||
CGRect aButtonFrame = CGRectMake(BUTTON_CENTER_X - aButtonImage.size.width / 2, BUTTON_CENTER_Y + BUTTON_DIST, aButtonImage.size.width, aButtonImage.size.height);
|
||||
CGRect bButtonFrame = CGRectMake(BUTTON_CENTER_X + BUTTON_DIST, BUTTON_CENTER_Y - bButtonImage.size.height / 2, bButtonImage.size.width, bButtonImage.size.height);
|
||||
CGRect xButtonFrame = CGRectMake(BUTTON_CENTER_X - BUTTON_DIST - xButtonImage.size.width, BUTTON_CENTER_Y - xButtonImage.size.height/ 2, xButtonImage.size.width, xButtonImage.size.height);
|
||||
CGRect yButtonFrame = CGRectMake(BUTTON_CENTER_X - yButtonImage.size.width / 2, BUTTON_CENTER_Y - BUTTON_DIST - yButtonImage.size.height, yButtonImage.size.width, yButtonImage.size.height);
|
||||
|
||||
// create A button
|
||||
_aButton.contents = (id) aButtonImage.CGImage;
|
||||
_aButton.frame = _swapABXY ? bButtonFrame : aButtonFrame;
|
||||
[_view.layer addSublayer:_aButton];
|
||||
|
||||
// create B button
|
||||
_bButton.frame = _swapABXY ? aButtonFrame : bButtonFrame;
|
||||
_bButton.contents = (id) bButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_bButton];
|
||||
|
||||
// create X Button
|
||||
_xButton.frame = _swapABXY ? yButtonFrame : xButtonFrame;
|
||||
_xButton.contents = (id) xButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_xButton];
|
||||
|
||||
// create Y Button
|
||||
_yButton.frame = _swapABXY ? xButtonFrame : yButtonFrame;
|
||||
_yButton.contents = (id) yButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_yButton];
|
||||
|
||||
// create Down button
|
||||
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
|
||||
_downButton.frame = CGRectMake(D_PAD_CENTER_X - downButtonImage.size.width / 2, D_PAD_CENTER_Y + D_PAD_DIST, downButtonImage.size.width, downButtonImage.size.height);
|
||||
_downButton.contents = (id) downButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_downButton];
|
||||
|
||||
// create Right button
|
||||
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
|
||||
_rightButton.frame = CGRectMake(D_PAD_CENTER_X + D_PAD_DIST, D_PAD_CENTER_Y - rightButtonImage.size.height / 2, rightButtonImage.size.width, rightButtonImage.size.height);
|
||||
_rightButton.contents = (id) rightButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_rightButton];
|
||||
|
||||
// create Up button
|
||||
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
|
||||
_upButton.frame = CGRectMake(D_PAD_CENTER_X - upButtonImage.size.width / 2, D_PAD_CENTER_Y - D_PAD_DIST - upButtonImage.size.height, upButtonImage.size.width, upButtonImage.size.height);
|
||||
_upButton.contents = (id) upButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_upButton];
|
||||
|
||||
// create Left button
|
||||
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
|
||||
_leftButton.frame = CGRectMake(D_PAD_CENTER_X - D_PAD_DIST - leftButtonImage.size.width, D_PAD_CENTER_Y - leftButtonImage.size.height / 2, leftButtonImage.size.width, leftButtonImage.size.height);
|
||||
_leftButton.contents = (id) leftButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_leftButton];
|
||||
}
|
||||
|
||||
- (void) drawStartSelect {
|
||||
// create Start button
|
||||
UIImage* startButtonImage = [UIImage imageNamed:@"StartButton"];
|
||||
_startButton.frame = CGRectMake(START_X - startButtonImage.size.width / 2, START_Y - startButtonImage.size.height / 2, startButtonImage.size.width, startButtonImage.size.height);
|
||||
_startButton.contents = (id) startButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_startButton];
|
||||
|
||||
// create Select button
|
||||
UIImage* selectButtonImage = [UIImage imageNamed:@"SelectButton"];
|
||||
_selectButton.frame = CGRectMake(SELECT_X - selectButtonImage.size.width / 2, SELECT_Y - selectButtonImage.size.height / 2, selectButtonImage.size.width, selectButtonImage.size.height);
|
||||
_selectButton.contents = (id) selectButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_selectButton];
|
||||
}
|
||||
|
||||
- (void) drawBumpers {
|
||||
// create L1 button
|
||||
UIImage* l1ButtonImage = [UIImage imageNamed:@"L1"];
|
||||
_l1Button.frame = CGRectMake(L1_X - l1ButtonImage.size.width / 2, L1_Y - l1ButtonImage.size.height / 2, l1ButtonImage.size.width, l1ButtonImage.size.height);
|
||||
_l1Button.contents = (id) l1ButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_l1Button];
|
||||
|
||||
// create R1 button
|
||||
UIImage* r1ButtonImage = [UIImage imageNamed:@"R1"];
|
||||
_r1Button.frame = CGRectMake(R1_X - r1ButtonImage.size.width / 2, R1_Y - r1ButtonImage.size.height / 2, r1ButtonImage.size.width, r1ButtonImage.size.height);
|
||||
_r1Button.contents = (id) r1ButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_r1Button];
|
||||
}
|
||||
|
||||
- (void) drawTriggers {
|
||||
// create L2 button
|
||||
UIImage* l2ButtonImage = [UIImage imageNamed:@"L2"];
|
||||
_l2Button.frame = CGRectMake(L2_X - l2ButtonImage.size.width / 2, L2_Y - l2ButtonImage.size.height / 2, l2ButtonImage.size.width, l2ButtonImage.size.height);
|
||||
_l2Button.contents = (id) l2ButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_l2Button];
|
||||
|
||||
// create R2 button
|
||||
UIImage* r2ButtonImage = [UIImage imageNamed:@"R2"];
|
||||
_r2Button.frame = CGRectMake(R2_X - r2ButtonImage.size.width / 2, R2_Y - r2ButtonImage.size.height / 2, r2ButtonImage.size.width, r2ButtonImage.size.height);
|
||||
_r2Button.contents = (id) r2ButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_r2Button];
|
||||
}
|
||||
|
||||
- (void) drawSticks {
|
||||
// create left analog stick
|
||||
UIImage* leftStickBgImage = [UIImage imageNamed:@"StickOuter"];
|
||||
_leftStickBackground.frame = CGRectMake(LS_CENTER_X - leftStickBgImage.size.width / 2, LS_CENTER_Y - leftStickBgImage.size.height / 2, leftStickBgImage.size.width, leftStickBgImage.size.height);
|
||||
_leftStickBackground.contents = (id) leftStickBgImage.CGImage;
|
||||
[_view.layer addSublayer:_leftStickBackground];
|
||||
|
||||
UIImage* leftStickImage = [UIImage imageNamed:@"StickInner"];
|
||||
_leftStick.frame = CGRectMake(LS_CENTER_X - leftStickImage.size.width / 2, LS_CENTER_Y - leftStickImage.size.height / 2, leftStickImage.size.width, leftStickImage.size.height);
|
||||
_leftStick.contents = (id) leftStickImage.CGImage;
|
||||
[_view.layer addSublayer:_leftStick];
|
||||
|
||||
// create right analog stick
|
||||
UIImage* rightStickBgImage = [UIImage imageNamed:@"StickOuter"];
|
||||
_rightStickBackground.frame = CGRectMake(RS_CENTER_X - rightStickBgImage.size.width / 2, RS_CENTER_Y - rightStickBgImage.size.height / 2, rightStickBgImage.size.width, rightStickBgImage.size.height);
|
||||
_rightStickBackground.contents = (id) rightStickBgImage.CGImage;
|
||||
[_view.layer addSublayer:_rightStickBackground];
|
||||
|
||||
UIImage* rightStickImage = [UIImage imageNamed:@"StickInner"];
|
||||
_rightStick.frame = CGRectMake(RS_CENTER_X - rightStickImage.size.width / 2, RS_CENTER_Y - rightStickImage.size.height / 2, rightStickImage.size.width, rightStickImage.size.height);
|
||||
_rightStick.contents = (id) rightStickImage.CGImage;
|
||||
[_view.layer addSublayer:_rightStick];
|
||||
|
||||
STICK_INNER_SIZE = rightStickImage.size.width;
|
||||
STICK_OUTER_SIZE = rightStickBgImage.size.width;
|
||||
}
|
||||
|
||||
- (void) drawL3R3 {
|
||||
UIImage* l3ButtonImage = [UIImage imageNamed:@"L3"];
|
||||
_l3Button.frame = CGRectMake(L3_X - l3ButtonImage.size.width / 2, L3_Y - l3ButtonImage.size.height / 2, l3ButtonImage.size.width, l3ButtonImage.size.height);
|
||||
_l3Button.contents = (id) l3ButtonImage.CGImage;
|
||||
_l3Button.cornerRadius = l3ButtonImage.size.width / 2;
|
||||
_l3Button.borderColor = [UIColor colorWithRed:15.f/255 green:160.f/255 blue:40.f/255 alpha:1.f].CGColor;
|
||||
[_view.layer addSublayer:_l3Button];
|
||||
|
||||
UIImage* r3ButtonImage = [UIImage imageNamed:@"R3"];
|
||||
_r3Button.frame = CGRectMake(R3_X - r3ButtonImage.size.width / 2, R3_Y - r3ButtonImage.size.height / 2, r3ButtonImage.size.width, r3ButtonImage.size.height);
|
||||
_r3Button.contents = (id) r3ButtonImage.CGImage;
|
||||
_r3Button.cornerRadius = r3ButtonImage.size.width / 2;
|
||||
_r3Button.borderColor = [UIColor colorWithRed:15.f/255 green:160.f/255 blue:40.f/255 alpha:1.f].CGColor;
|
||||
[_view.layer addSublayer:_r3Button];
|
||||
}
|
||||
|
||||
- (void) hideButtons {
|
||||
[_aButton removeFromSuperlayer];
|
||||
[_bButton removeFromSuperlayer];
|
||||
[_xButton removeFromSuperlayer];
|
||||
[_yButton removeFromSuperlayer];
|
||||
[_upButton removeFromSuperlayer];
|
||||
[_downButton removeFromSuperlayer];
|
||||
[_leftButton removeFromSuperlayer];
|
||||
[_rightButton removeFromSuperlayer];
|
||||
}
|
||||
|
||||
- (void) hideStartSelect {
|
||||
[_startButton removeFromSuperlayer];
|
||||
[_selectButton removeFromSuperlayer];
|
||||
}
|
||||
|
||||
- (void) hideBumpers {
|
||||
[_l1Button removeFromSuperlayer];
|
||||
[_r1Button removeFromSuperlayer];
|
||||
}
|
||||
|
||||
- (void) hideTriggers {
|
||||
[_l2Button removeFromSuperlayer];
|
||||
[_r2Button removeFromSuperlayer];
|
||||
}
|
||||
|
||||
- (void) hideSticks {
|
||||
[_leftStickBackground removeFromSuperlayer];
|
||||
[_rightStickBackground removeFromSuperlayer];
|
||||
[_leftStick removeFromSuperlayer];
|
||||
[_rightStick removeFromSuperlayer];
|
||||
}
|
||||
|
||||
- (void) hideL3R3 {
|
||||
[_l3Button removeFromSuperlayer];
|
||||
[_r3Button removeFromSuperlayer];
|
||||
}
|
||||
|
||||
- (BOOL) handleTouchMovedEvent:touches {
|
||||
BOOL updated = false;
|
||||
BOOL buttonTouch = false;
|
||||
float rsMaxX = RS_CENTER_X + STICK_OUTER_SIZE / 2;
|
||||
float rsMaxY = RS_CENTER_Y + STICK_OUTER_SIZE / 2;
|
||||
float rsMinX = RS_CENTER_X - STICK_OUTER_SIZE / 2;
|
||||
float rsMinY = RS_CENTER_Y - STICK_OUTER_SIZE / 2;
|
||||
float lsMaxX = LS_CENTER_X + STICK_OUTER_SIZE / 2;
|
||||
float lsMaxY = LS_CENTER_Y + STICK_OUTER_SIZE / 2;
|
||||
float lsMinX = LS_CENTER_X - STICK_OUTER_SIZE / 2;
|
||||
float lsMinY = LS_CENTER_Y - STICK_OUTER_SIZE / 2;
|
||||
|
||||
for (UITouch* touch in touches) {
|
||||
CGPoint touchLocation = [touch locationInView:_view];
|
||||
float xLoc = touchLocation.x;
|
||||
float yLoc = touchLocation.y;
|
||||
if (touch == _lsTouch) {
|
||||
if (xLoc > lsMaxX) xLoc = lsMaxX;
|
||||
if (xLoc < lsMinX) xLoc = lsMinX;
|
||||
if (yLoc > lsMaxY) yLoc = lsMaxY;
|
||||
if (yLoc < lsMinY) yLoc = lsMinY;
|
||||
|
||||
_leftStick.frame = CGRectMake(xLoc - STICK_INNER_SIZE / 2, yLoc - STICK_INNER_SIZE / 2, STICK_INNER_SIZE, STICK_INNER_SIZE);
|
||||
|
||||
float xStickVal = (xLoc - LS_CENTER_X) / (lsMaxX - LS_CENTER_X);
|
||||
float yStickVal = (yLoc - LS_CENTER_Y) / (lsMaxY - LS_CENTER_Y);
|
||||
|
||||
if (fabsf(xStickVal) < STICK_DEAD_ZONE) xStickVal = 0;
|
||||
if (fabsf(yStickVal) < STICK_DEAD_ZONE) yStickVal = 0;
|
||||
|
||||
[_controllerSupport updateLeftStick:_controller x:0x7FFE * xStickVal y:0x7FFE * -yStickVal];
|
||||
|
||||
updated = true;
|
||||
} else if (touch == _rsTouch) {
|
||||
if (xLoc > rsMaxX) xLoc = rsMaxX;
|
||||
if (xLoc < rsMinX) xLoc = rsMinX;
|
||||
if (yLoc > rsMaxY) yLoc = rsMaxY;
|
||||
if (yLoc < rsMinY) yLoc = rsMinY;
|
||||
|
||||
_rightStick.frame = CGRectMake(xLoc - STICK_INNER_SIZE / 2, yLoc - STICK_INNER_SIZE / 2, STICK_INNER_SIZE, STICK_INNER_SIZE);
|
||||
|
||||
float xStickVal = (xLoc - RS_CENTER_X) / (rsMaxX - RS_CENTER_X);
|
||||
float yStickVal = (yLoc - RS_CENTER_Y) / (rsMaxY - RS_CENTER_Y);
|
||||
|
||||
if (fabsf(xStickVal) < STICK_DEAD_ZONE) xStickVal = 0;
|
||||
if (fabsf(yStickVal) < STICK_DEAD_ZONE) yStickVal = 0;
|
||||
|
||||
[_controllerSupport updateRightStick:_controller x:0x7FFE * xStickVal y:0x7FFE * -yStickVal];
|
||||
|
||||
updated = true;
|
||||
} else if (touch == _dpadTouch) {
|
||||
[_controllerSupport clearButtonFlag:_controller
|
||||
flags:UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG];
|
||||
|
||||
// Allow the user to slide their finger to another d-pad button
|
||||
if ([_upButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:UP_FLAG];
|
||||
updated = true;
|
||||
} else if ([_downButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:DOWN_FLAG];
|
||||
updated = true;
|
||||
} else if ([_leftButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:LEFT_FLAG];
|
||||
updated = true;
|
||||
} else if ([_rightButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:RIGHT_FLAG];
|
||||
updated = true;
|
||||
}
|
||||
|
||||
buttonTouch = true;
|
||||
} else if (touch == _aTouch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _bTouch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _xTouch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _yTouch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _startTouch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _selectTouch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _l1Touch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _r1Touch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _l2Touch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _r2Touch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _l3Touch) {
|
||||
buttonTouch = true;
|
||||
} else if (touch == _r3Touch) {
|
||||
buttonTouch = true;
|
||||
}
|
||||
if ([_deadTouches containsObject:touch]) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
[_controllerSupport updateFinished:_controller];
|
||||
}
|
||||
return updated || buttonTouch;
|
||||
}
|
||||
|
||||
- (BOOL)handleTouchDownEvent:touches {
|
||||
BOOL updated = false;
|
||||
BOOL stickTouch = false;
|
||||
for (UITouch* touch in touches) {
|
||||
CGPoint touchLocation = [touch locationInView:_view];
|
||||
|
||||
if (_aButton.superlayer != nil && [_aButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:A_FLAG];
|
||||
_aTouch = touch;
|
||||
updated = true;
|
||||
} else if (_bButton.superlayer != nil && [_bButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:B_FLAG];
|
||||
_bTouch = touch;
|
||||
updated = true;
|
||||
} else if (_xButton.superlayer != nil && [_xButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:X_FLAG];
|
||||
_xTouch = touch;
|
||||
updated = true;
|
||||
} else if (_yButton.superlayer != nil && [_yButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:Y_FLAG];
|
||||
_yTouch = touch;
|
||||
updated = true;
|
||||
} else if (_upButton.superlayer != nil && [_upButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:UP_FLAG];
|
||||
_dpadTouch = touch;
|
||||
updated = true;
|
||||
} else if (_downButton.superlayer != nil && [_downButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:DOWN_FLAG];
|
||||
_dpadTouch = touch;
|
||||
updated = true;
|
||||
} else if (_leftButton.superlayer != nil && [_leftButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:LEFT_FLAG];
|
||||
_dpadTouch = touch;
|
||||
updated = true;
|
||||
} else if (_rightButton.superlayer != nil && [_rightButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:RIGHT_FLAG];
|
||||
_dpadTouch = touch;
|
||||
updated = true;
|
||||
} else if (_startButton.superlayer != nil && [_startButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:PLAY_FLAG];
|
||||
_startTouch = touch;
|
||||
updated = true;
|
||||
} else if (_selectButton.superlayer != nil && [_selectButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:BACK_FLAG];
|
||||
_selectTouch = touch;
|
||||
updated = true;
|
||||
} else if (_l1Button.superlayer != nil && [_l1Button.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:LB_FLAG];
|
||||
_l1Touch = touch;
|
||||
updated = true;
|
||||
} else if (_r1Button.superlayer != nil && [_r1Button.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:RB_FLAG];
|
||||
_r1Touch = touch;
|
||||
updated = true;
|
||||
} else if (_l2Button.superlayer != nil && [_l2Button.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport updateLeftTrigger:_controller left:0xFF];
|
||||
_l2Touch = touch;
|
||||
updated = true;
|
||||
} else if (_r2Button.superlayer != nil && [_r2Button.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport updateRightTrigger:_controller right:0xFF];
|
||||
_r2Touch = touch;
|
||||
updated = true;
|
||||
} else if (_l3Button.superlayer != nil && [_l3Button.presentationLayer hitTest:touchLocation]) {
|
||||
if (l3Set) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:LS_CLK_FLAG];
|
||||
_l3Button.borderWidth = 0.0f;
|
||||
} else {
|
||||
[_controllerSupport setButtonFlag:_controller flags:LS_CLK_FLAG];
|
||||
_l3Button.borderWidth = 2.0f;
|
||||
}
|
||||
l3Set = !l3Set;
|
||||
_l3Touch = touch;
|
||||
updated = true;
|
||||
} else if (_r3Button.superlayer != nil && [_r3Button.presentationLayer hitTest:touchLocation]) {
|
||||
if (r3Set) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:RS_CLK_FLAG];
|
||||
_r3Button.borderWidth = 0.0f;
|
||||
} else {
|
||||
[_controllerSupport setButtonFlag:_controller flags:RS_CLK_FLAG];
|
||||
_r3Button.borderWidth = 2.0f;
|
||||
}
|
||||
r3Set = !r3Set;
|
||||
_r3Touch = touch;
|
||||
updated = true;
|
||||
} else if (_leftStick.superlayer != nil && [_leftStick.presentationLayer hitTest:touchLocation]) {
|
||||
if (l3TouchStart != nil) {
|
||||
// Find elapsed time and convert to milliseconds
|
||||
// Use (-) modifier to conversion since receiver is earlier than now
|
||||
double l3TouchTime = [l3TouchStart timeIntervalSinceNow] * -1000.0;
|
||||
if (l3TouchTime < STICK_CLICK_RATE) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:LS_CLK_FLAG];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
_lsTouch = touch;
|
||||
stickTouch = true;
|
||||
} else if (_rightStick.superlayer != nil && [_rightStick.presentationLayer hitTest:touchLocation]) {
|
||||
if (r3TouchStart != nil) {
|
||||
// Find elapsed time and convert to milliseconds
|
||||
// Use (-) modifier to conversion since receiver is earlier than now
|
||||
double r3TouchTime = [r3TouchStart timeIntervalSinceNow] * -1000.0;
|
||||
if (r3TouchTime < STICK_CLICK_RATE) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:RS_CLK_FLAG];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
_rsTouch = touch;
|
||||
stickTouch = true;
|
||||
}
|
||||
if (!updated && !stickTouch && [self isInDeadZone:touch]) {
|
||||
[_deadTouches addObject:touch];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
[_controllerSupport updateFinished:_controller];
|
||||
}
|
||||
return updated || stickTouch;
|
||||
}
|
||||
|
||||
- (BOOL)handleTouchUpEvent:touches {
|
||||
BOOL updated = false;
|
||||
BOOL touched = false;
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch == _aTouch) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:A_FLAG];
|
||||
_aTouch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _bTouch) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:B_FLAG];
|
||||
_bTouch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _xTouch) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:X_FLAG];
|
||||
_xTouch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _yTouch) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:Y_FLAG];
|
||||
_yTouch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _dpadTouch) {
|
||||
[_controllerSupport clearButtonFlag:_controller
|
||||
flags:UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG];
|
||||
_dpadTouch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _startTouch) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:PLAY_FLAG];
|
||||
_startTouch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _selectTouch) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:BACK_FLAG];
|
||||
_selectTouch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _l1Touch) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:LB_FLAG];
|
||||
_l1Touch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _r1Touch) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:RB_FLAG];
|
||||
_r1Touch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _l2Touch) {
|
||||
[_controllerSupport updateLeftTrigger:_controller left:0];
|
||||
_l2Touch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _r2Touch) {
|
||||
[_controllerSupport updateRightTrigger:_controller right:0];
|
||||
_r2Touch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _lsTouch) {
|
||||
_leftStick.frame = CGRectMake(LS_CENTER_X - STICK_INNER_SIZE / 2, LS_CENTER_Y - STICK_INNER_SIZE / 2, STICK_INNER_SIZE, STICK_INNER_SIZE);
|
||||
[_controllerSupport updateLeftStick:_controller x:0 y:0];
|
||||
[_controllerSupport clearButtonFlag:_controller flags:LS_CLK_FLAG];
|
||||
l3TouchStart = [NSDate date];
|
||||
_lsTouch = nil;
|
||||
updated = true;
|
||||
} else if (touch == _rsTouch) {
|
||||
_rightStick.frame = CGRectMake(RS_CENTER_X - STICK_INNER_SIZE / 2, RS_CENTER_Y - STICK_INNER_SIZE / 2, STICK_INNER_SIZE, STICK_INNER_SIZE);
|
||||
[_controllerSupport updateRightStick:_controller x:0 y:0];
|
||||
[_controllerSupport clearButtonFlag:_controller flags:RS_CLK_FLAG];
|
||||
r3TouchStart = [NSDate date];
|
||||
_rsTouch = nil;
|
||||
updated = true;
|
||||
}
|
||||
else if (touch == _l3Touch) {
|
||||
_l3Touch = nil;
|
||||
touched = true;
|
||||
}
|
||||
else if (touch == _r3Touch) {
|
||||
_r3Touch = nil;
|
||||
touched = true;
|
||||
}
|
||||
if ([_deadTouches containsObject:touch]) {
|
||||
[_deadTouches removeObject:touch];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
[_controllerSupport updateFinished:_controller];
|
||||
}
|
||||
|
||||
return updated || touched;
|
||||
}
|
||||
|
||||
- (BOOL) isInDeadZone:(UITouch*) touch {
|
||||
// Dynamically evaluate deadzones based on the controls
|
||||
// on screen at the time
|
||||
if (_leftButton.superlayer != nil && [self isDpadDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_aButton.superlayer != nil && [self isAbxyDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_l2Button.superlayer != nil && [self isTriggerDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_l1Button.superlayer != nil && [self isBumperDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_startButton.superlayer != nil && [self isStartSelectDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_l3Button.superlayer != nil && [self isL3R3DeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_leftStickBackground.superlayer != nil && [self isStickDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
- (BOOL) isDpadDeadZone:(UITouch*) touch {
|
||||
return [self isDeadZone:touch
|
||||
startX:_view.frame.origin.x
|
||||
startY:_upButton.frame.origin.y
|
||||
endX:_rightButton.frame.origin.x + _rightButton.frame.size.width
|
||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
||||
}
|
||||
|
||||
- (BOOL) isAbxyDeadZone:(UITouch*) touch {
|
||||
return [self isDeadZone:touch
|
||||
startX:_xButton.frame.origin.x
|
||||
startY:_yButton.frame.origin.y
|
||||
endX:_view.frame.origin.x + _view.frame.size.width
|
||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
||||
}
|
||||
|
||||
- (BOOL) isBumperDeadZone:(UITouch*) touch {
|
||||
return [self isDeadZone:touch
|
||||
startX:_view.frame.origin.x
|
||||
startY:_l2Button.frame.origin.y + _l2Button.frame.size.height
|
||||
endX:_l1Button.frame.origin.x + _l1Button.frame.size.width
|
||||
endY:_upButton.frame.origin.y]
|
||||
|| [self isDeadZone:touch
|
||||
startX:_r2Button.frame.origin.x
|
||||
startY:_r2Button.frame.origin.y + _r2Button.frame.size.height
|
||||
endX:_view.frame.origin.x + _view.frame.size.width
|
||||
endY:_yButton.frame.origin.y];
|
||||
}
|
||||
|
||||
- (BOOL) isTriggerDeadZone:(UITouch*) touch {
|
||||
return [self isDeadZone:touch
|
||||
startX:_view.frame.origin.x
|
||||
startY:_l2Button.frame.origin.y
|
||||
endX:_l2Button.frame.origin.x + _l2Button.frame.size.width
|
||||
endY:_view.frame.origin.y + _view.frame.size.height]
|
||||
|| [self isDeadZone:touch
|
||||
startX:_r2Button.frame.origin.x
|
||||
startY:_r2Button.frame.origin.y
|
||||
endX:_view.frame.origin.x + _view.frame.size.width
|
||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
||||
}
|
||||
|
||||
- (BOOL) isL3R3DeadZone:(UITouch*) touch {
|
||||
return [self isDeadZone:touch
|
||||
startX:_view.frame.origin.x
|
||||
startY:_l3Button.frame.origin.y
|
||||
endX:_view.frame.origin.x
|
||||
endY:_view.frame.origin.y + _view.frame.size.height]
|
||||
|| [self isDeadZone:touch
|
||||
startX:_r3Button.frame.origin.x
|
||||
startY:_r3Button.frame.origin.y
|
||||
endX:_view.frame.origin.x + _view.frame.size.width
|
||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
||||
}
|
||||
|
||||
- (BOOL) isStartSelectDeadZone:(UITouch*) touch {
|
||||
return [self isDeadZone:touch
|
||||
startX:_startButton.frame.origin.x
|
||||
startY:_startButton.frame.origin.y
|
||||
endX:_view.frame.origin.x + _view.frame.size.width
|
||||
endY:_view.frame.origin.y + _view.frame.size.height]
|
||||
|| [self isDeadZone:touch
|
||||
startX:_view.frame.origin.x
|
||||
startY:_selectButton.frame.origin.y
|
||||
endX:_selectButton.frame.origin.x + _selectButton.frame.size.width
|
||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
||||
}
|
||||
|
||||
- (BOOL) isStickDeadZone:(UITouch*) touch {
|
||||
return [self isDeadZone:touch
|
||||
startX:_leftStickBackground.frame.origin.x - 15
|
||||
startY:_leftStickBackground.frame.origin.y - 15
|
||||
endX:_leftStickBackground.frame.origin.x + _leftStickBackground.frame.size.width + 15
|
||||
endY:_view.frame.origin.y + _view.frame.size.height]
|
||||
|| [self isDeadZone:touch
|
||||
startX:_rightStickBackground.frame.origin.x - 15
|
||||
startY:_rightStickBackground.frame.origin.y - 15
|
||||
endX:_rightStickBackground.frame.origin.x + _rightStickBackground.frame.size.width + 15
|
||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
||||
}
|
||||
|
||||
- (BOOL) isDeadZone:(UITouch*) touch startX:(float)deadZoneStartX startY:(float)deadZoneStartY endX:(float)deadZoneEndX endY:(float)deadZoneEndY {
|
||||
deadZoneStartX -= DEAD_ZONE_PADDING;
|
||||
deadZoneStartY -= DEAD_ZONE_PADDING;
|
||||
deadZoneEndX += DEAD_ZONE_PADDING;
|
||||
deadZoneEndY += DEAD_ZONE_PADDING;
|
||||
|
||||
CGPoint touchLocation = [touch locationInView:_view];
|
||||
return (touchLocation.x > deadZoneStartX && touchLocation.x < deadZoneEndX
|
||||
&& touchLocation.y > deadZoneStartY && touchLocation.y < deadZoneEndY);
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// RelativeTouchHandler.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RelativeTouchHandler : UIResponder
|
||||
|
||||
-(id)initWithView:(StreamView*)view;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,207 +0,0 @@
|
||||
//
|
||||
// RelativeTouchHandler.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RelativeTouchHandler.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
static const int REFERENCE_WIDTH = 1280;
|
||||
static const int REFERENCE_HEIGHT = 720;
|
||||
|
||||
@implementation RelativeTouchHandler {
|
||||
CGPoint touchLocation, originalLocation;
|
||||
BOOL touchMoved;
|
||||
BOOL isDragging;
|
||||
NSTimer* dragTimer;
|
||||
NSUInteger peakTouchCount;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
UIGestureRecognizer* remotePressRecognizer;
|
||||
UIGestureRecognizer* remoteLongPressRecognizer;
|
||||
#endif
|
||||
|
||||
UIView* view;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view {
|
||||
self = [self init];
|
||||
self->view = view;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
||||
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
|
||||
remoteLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonLongPressed:)];
|
||||
remoteLongPressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
|
||||
[self->view addGestureRecognizer:remotePressRecognizer];
|
||||
[self->view addGestureRecognizer:remoteLongPressRecognizer];
|
||||
#endif
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
|
||||
// Movements of greater than 5 pixels are considered confirmed
|
||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 5;
|
||||
}
|
||||
|
||||
- (void)onDragStart:(NSTimer*)timer {
|
||||
if (!touchMoved && !isDragging){
|
||||
isDragging = true;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
touchMoved = false;
|
||||
peakTouchCount = [[event allTouches] count];
|
||||
if ([[event allTouches] count] == 1) {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
originalLocation = touchLocation = [touch locationInView:view];
|
||||
if (!isDragging) {
|
||||
dragTimer = [NSTimer scheduledTimerWithTimeInterval:0.650
|
||||
target:self
|
||||
selector:@selector(onDragStart:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
}
|
||||
else if ([[event allTouches] count] == 2) {
|
||||
CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:view];
|
||||
CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:view];
|
||||
|
||||
originalLocation = touchLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if ([[event allTouches] count] == 1) {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
CGPoint currentLocation = [touch locationInView:view];
|
||||
|
||||
if (touchLocation.x != currentLocation.x ||
|
||||
touchLocation.y != currentLocation.y)
|
||||
{
|
||||
int deltaX = (currentLocation.x - touchLocation.x) * (REFERENCE_WIDTH / view.bounds.size.width);
|
||||
int deltaY = (currentLocation.y - touchLocation.y) * (REFERENCE_HEIGHT / view.bounds.size.height);
|
||||
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||
touchLocation = currentLocation;
|
||||
|
||||
// If we've moved far enough to confirm this wasn't just human/machine error,
|
||||
// mark it as such.
|
||||
if ([self isConfirmedMove:touchLocation from:originalLocation]) {
|
||||
touchMoved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ([[event allTouches] count] == 2) {
|
||||
CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:view];
|
||||
CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:view];
|
||||
|
||||
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
||||
if (touchLocation.y != avgLocation.y) {
|
||||
LiSendHighResScrollEvent((avgLocation.y - touchLocation.y) * 10);
|
||||
}
|
||||
|
||||
// If we've moved far enough to confirm this wasn't just human/machine error,
|
||||
// mark it as such.
|
||||
if ([self isConfirmedMove:firstLocation from:originalLocation]) {
|
||||
touchMoved = true;
|
||||
}
|
||||
|
||||
touchLocation = avgLocation;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[dragTimer invalidate];
|
||||
dragTimer = nil;
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
} else if (!touchMoved) {
|
||||
if (peakTouchCount == 2) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
});
|
||||
} else if (peakTouchCount == 1) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
if (!self->isDragging){
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
}
|
||||
self->isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We we're moving from 2+ touches to 1. Synchronize the current position
|
||||
// of the active finger so we don't jump unexpectedly on the next touchesMoved
|
||||
// callback when finger 1 switches on us.
|
||||
if ([[event allTouches] count] - [touches count] == 1) {
|
||||
NSMutableSet *activeSet = [[NSMutableSet alloc] initWithCapacity:[[event allTouches] count]];
|
||||
[activeSet unionSet:[event allTouches]];
|
||||
[activeSet minusSet:touches];
|
||||
touchLocation = [[activeSet anyObject] locationInView:view];
|
||||
|
||||
// Mark this touch as moved so we don't send a left mouse click if the user
|
||||
// right clicks without moving their other finger.
|
||||
touchMoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[dragTimer invalidate];
|
||||
dragTimer = nil;
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
}
|
||||
peakTouchCount = 0;
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void)remoteButtonPressed:(id)sender {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
|
||||
// Mark this as touchMoved to avoid a duplicate press on touch up
|
||||
self->touchMoved = true;
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
- (void)remoteButtonLongPressed:(id)sender {
|
||||
Log(LOG_D, @"Holding left mouse button");
|
||||
|
||||
isDragging = true;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -1,37 +0,0 @@
|
||||
//
|
||||
// StreamView.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/19/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ControllerSupport.h"
|
||||
#import "OnScreenControls.h"
|
||||
#import "Moonlight-Swift.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@protocol UserInteractionDelegate <NSObject>
|
||||
|
||||
- (void) userInteractionBegan;
|
||||
- (void) userInteractionEnded;
|
||||
|
||||
@end
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
|
||||
#else
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
||||
#endif
|
||||
|
||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||
config:(StreamConfiguration*)streamConfig;
|
||||
- (void) showOnScreenControls;
|
||||
- (OnScreenControlsLevel) getCurrentOscState;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6254" systemVersion="14C81f" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<entity name="Host" representedClassName="Host" syncable="YES">
|
||||
<attribute name="address" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="2" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="148"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6254" systemVersion="14C81f" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<entity name="Host" representedClassName="Host" syncable="YES">
|
||||
<attribute name="address" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="148"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13772" systemVersion="17D102" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES">
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES">
|
||||
<attribute name="address" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="163"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="150"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="9057" systemVersion="15B42" minimumToolsVersion="Automatic">
|
||||
<entity name="App" representedClassName="App" syncable="YES">
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES">
|
||||
<attribute name="address" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
||||
<attribute name="uniqueId" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="163"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="135"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17F77" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="165"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23A344" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17G65" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="165"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="195"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F132" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E266" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="238"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19H2" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="118"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="268"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="20G224" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="118"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="254"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G72" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="118"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="298"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G72" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="useHevc2" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="118"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="298"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// VideoDecoderRenderer.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/18/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
#import "ConnectionCallbacks.h"
|
||||
|
||||
#include "Limelight.h"
|
||||
|
||||
@interface VideoDecoderRenderer : NSObject
|
||||
|
||||
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing;
|
||||
|
||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate;
|
||||
- (void)start;
|
||||
- (void)stop;
|
||||
- (void)setHdrMode:(BOOL)enabled;
|
||||
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du;
|
||||
|
||||
@end
|
||||
@@ -1,684 +0,0 @@
|
||||
//
|
||||
// VideoDecoderRenderer.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/18/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamView.h"
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavcodec/cbs.h>
|
||||
#include <libavcodec/cbs_av1.h>
|
||||
#include <libavformat/avio.h>
|
||||
#include <libavutil/mem.h>
|
||||
|
||||
// Private libavformat API for writing the AV1 Codec Configuration Box
|
||||
extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
int write_seq_header);
|
||||
|
||||
@implementation VideoDecoderRenderer {
|
||||
StreamView* _view;
|
||||
id<ConnectionCallbacks> _callbacks;
|
||||
float _streamAspectRatio;
|
||||
|
||||
AVSampleBufferDisplayLayer* displayLayer;
|
||||
int videoFormat;
|
||||
int frameRate;
|
||||
|
||||
NSMutableArray *parameterSetBuffers;
|
||||
NSData *masteringDisplayColorVolume;
|
||||
NSData *contentLightLevelInfo;
|
||||
CMVideoFormatDescriptionRef formatDesc;
|
||||
|
||||
CADisplayLink* _displayLink;
|
||||
BOOL framePacing;
|
||||
}
|
||||
|
||||
- (void)reinitializeDisplayLayer
|
||||
{
|
||||
CALayer *oldLayer = displayLayer;
|
||||
|
||||
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
||||
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
|
||||
|
||||
// Ensure the AVSampleBufferDisplayLayer is sized to preserve the aspect ratio
|
||||
// of the video stream. We used to use AVLayerVideoGravityResizeAspect, but that
|
||||
// respects the PAR encoded in the SPS which causes our computed video-relative
|
||||
// touch location to be wrong in StreamView if the aspect ratio of the host
|
||||
// desktop doesn't match the aspect ratio of the stream.
|
||||
CGSize videoSize;
|
||||
if (_view.bounds.size.width > _view.bounds.size.height * _streamAspectRatio) {
|
||||
videoSize = CGSizeMake(_view.bounds.size.height * _streamAspectRatio, _view.bounds.size.height);
|
||||
} else {
|
||||
videoSize = CGSizeMake(_view.bounds.size.width, _view.bounds.size.width / _streamAspectRatio);
|
||||
}
|
||||
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
|
||||
displayLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height);
|
||||
displayLayer.videoGravity = AVLayerVideoGravityResize;
|
||||
|
||||
// Hide the layer until we get an IDR frame. This ensures we
|
||||
// can see the loading progress label as the stream is starting.
|
||||
displayLayer.hidden = YES;
|
||||
|
||||
if (oldLayer != nil) {
|
||||
// Switch out the old display layer with the new one
|
||||
[_view.layer replaceSublayer:oldLayer with:displayLayer];
|
||||
}
|
||||
else {
|
||||
[_view.layer addSublayer:displayLayer];
|
||||
}
|
||||
|
||||
if (formatDesc != nil) {
|
||||
CFRelease(formatDesc);
|
||||
formatDesc = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
_view = view;
|
||||
_callbacks = callbacks;
|
||||
_streamAspectRatio = aspectRatio;
|
||||
framePacing = useFramePacing;
|
||||
|
||||
parameterSetBuffers = [[NSMutableArray alloc] init];
|
||||
|
||||
[self reinitializeDisplayLayer];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate
|
||||
{
|
||||
self->videoFormat = videoFormat;
|
||||
self->frameRate = frameRate;
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
|
||||
if (@available(iOS 15.0, tvOS 15.0, *)) {
|
||||
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(self->frameRate, self->frameRate, self->frameRate);
|
||||
}
|
||||
else {
|
||||
_displayLink.preferredFramesPerSecond = self->frameRate;
|
||||
}
|
||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
}
|
||||
|
||||
// TODO: Refactor this
|
||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
|
||||
- (void)displayLinkCallback:(CADisplayLink *)sender
|
||||
{
|
||||
VIDEO_FRAME_HANDLE handle;
|
||||
PDECODE_UNIT du;
|
||||
|
||||
while (LiPollNextVideoFrame(&handle, &du)) {
|
||||
LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
|
||||
|
||||
if (framePacing) {
|
||||
// Calculate the actual display refresh rate
|
||||
double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
|
||||
|
||||
// Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
|
||||
// Battery saver, accessibility settings, or device thermals can cause the actual
|
||||
// refresh rate of the display to drop below the physical maximum.
|
||||
if (displayRefreshRate >= frameRate * 0.9f) {
|
||||
// Keep one pending frame to smooth out gaps due to
|
||||
// network jitter at the cost of 1 frame of latency
|
||||
if (LiGetPendingVideoFrames() == 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
[_displayLink invalidate];
|
||||
}
|
||||
|
||||
#define NALU_START_PREFIX_SIZE 3
|
||||
#define NAL_LENGTH_PREFIX_SIZE 4
|
||||
|
||||
- (void)updateAnnexBBufferForRange:(CMBlockBufferRef)frameBuffer dataBlock:(CMBlockBufferRef)dataBuffer offset:(int)offset length:(int)nalLength
|
||||
{
|
||||
OSStatus status;
|
||||
size_t oldOffset = CMBlockBufferGetDataLength(frameBuffer);
|
||||
|
||||
// Append a 4 byte buffer to the frame block for the length prefix
|
||||
status = CMBlockBufferAppendMemoryBlock(frameBuffer, NULL,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
kCFAllocatorDefault, NULL, 0,
|
||||
NAL_LENGTH_PREFIX_SIZE, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferAppendMemoryBlock failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the length prefix to the new buffer
|
||||
const int dataLength = nalLength - NALU_START_PREFIX_SIZE;
|
||||
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
|
||||
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
|
||||
status = CMBlockBufferReplaceDataBytes(lengthBytes, frameBuffer,
|
||||
oldOffset, NAL_LENGTH_PREFIX_SIZE);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach the data buffer to the frame buffer by reference
|
||||
status = CMBlockBufferAppendBufferReference(frameBuffer, dataBuffer, offset + NALU_START_PREFIX_SIZE, dataLength, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSData*)getAv1CodecConfigurationBox:(NSData*)frameData {
|
||||
AVIOContext* ioctx = NULL;
|
||||
int err;
|
||||
|
||||
err = avio_open_dyn_buf(&ioctx);
|
||||
if (err < 0) {
|
||||
Log(LOG_E, @"avio_open_dyn_buf() failed: %d", err);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Submit the IDR frame to write the av1C blob
|
||||
err = ff_isom_write_av1c(ioctx, (uint8_t*)frameData.bytes, (int)frameData.length, 1);
|
||||
if (err < 0) {
|
||||
Log(LOG_E, @"ff_isom_write_av1c() failed: %d", err);
|
||||
// Fall-through to close and free buffer
|
||||
}
|
||||
|
||||
// Close the dynbuf and get the underlying buffer back (which we must free)
|
||||
uint8_t* av1cBuf = NULL;
|
||||
int av1cBufLen = avio_close_dyn_buf(ioctx, &av1cBuf);
|
||||
|
||||
Log(LOG_I, @"av1C block is %d bytes", av1cBufLen);
|
||||
|
||||
// Only return data if ff_isom_write_av1c() was successful
|
||||
NSData* data = nil;
|
||||
if (err >= 0 && av1cBufLen > 0) {
|
||||
data = [NSData dataWithBytes:av1cBuf length:av1cBufLen];
|
||||
}
|
||||
else {
|
||||
data = nil;
|
||||
}
|
||||
|
||||
av_free(av1cBuf);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Much of this logic comes from Chrome
|
||||
- (CMVideoFormatDescriptionRef)createAV1FormatDescriptionForIDRFrame:(NSData*)frameData {
|
||||
NSMutableDictionary* extensions = [[NSMutableDictionary alloc] init];
|
||||
|
||||
CodedBitstreamContext* cbsCtx = NULL;
|
||||
int err = ff_cbs_init(&cbsCtx, AV_CODEC_ID_AV1, NULL);
|
||||
if (err < 0) {
|
||||
Log(LOG_E, @"ff_cbs_init() failed: %d", err);
|
||||
return nil;
|
||||
}
|
||||
|
||||
AVPacket avPacket = {};
|
||||
avPacket.data = (uint8_t*)frameData.bytes;
|
||||
avPacket.size = (int)frameData.length;
|
||||
|
||||
// Read the sequence header OBU
|
||||
CodedBitstreamFragment cbsFrag = {};
|
||||
err = ff_cbs_read_packet(cbsCtx, &cbsFrag, &avPacket);
|
||||
if (err < 0) {
|
||||
Log(LOG_E, @"ff_cbs_read_packet() failed: %d", err);
|
||||
ff_cbs_close(&cbsCtx);
|
||||
return nil;
|
||||
}
|
||||
|
||||
#define SET_CFSTR_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (__bridge NSString*)(value)
|
||||
#define SET_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (value)
|
||||
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_FormatName, @"av01");
|
||||
|
||||
// We use the value for YUV without alpha, same as Chrome
|
||||
// https://developer.apple.com/library/archive/qa/qa1183/_index.html
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_Depth, @24);
|
||||
|
||||
CodedBitstreamAV1Context* bitstreamCtx = (CodedBitstreamAV1Context*)cbsCtx->priv_data;
|
||||
AV1RawSequenceHeader* seqHeader = bitstreamCtx->sequence_header;
|
||||
if (seqHeader == NULL) {
|
||||
Log(LOG_E, @"AV1 sequence header not found in IDR frame!");
|
||||
ff_cbs_fragment_free(&cbsFrag);
|
||||
ff_cbs_close(&cbsCtx);
|
||||
return nil;
|
||||
}
|
||||
|
||||
switch (seqHeader->color_config.color_primaries) {
|
||||
case 1: // CP_BT_709
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
||||
kCMFormatDescriptionColorPrimaries_ITU_R_709_2);
|
||||
break;
|
||||
|
||||
case 6: // CP_BT_601
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
||||
kCMFormatDescriptionColorPrimaries_SMPTE_C);
|
||||
break;
|
||||
|
||||
case 9: // CP_BT_2020
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
||||
kCMFormatDescriptionColorPrimaries_ITU_R_2020);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_W, @"Unsupported color_primaries value: %d", seqHeader->color_config.color_primaries);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (seqHeader->color_config.transfer_characteristics) {
|
||||
case 1: // TC_BT_709
|
||||
case 6: // TC_BT_601
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_ITU_R_709_2);
|
||||
break;
|
||||
|
||||
case 7: // TC_SMPTE_240
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_SMPTE_240M_1995);
|
||||
break;
|
||||
|
||||
case 8: // TC_LINEAR
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_Linear);
|
||||
break;
|
||||
|
||||
case 14: // TC_BT_2020_10_BIT
|
||||
case 15: // TC_BT_2020_12_BIT
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_ITU_R_2020);
|
||||
break;
|
||||
|
||||
case 16: // TC_SMPTE_2084
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ);
|
||||
break;
|
||||
|
||||
case 17: // TC_HLG
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_W, @"Unsupported transfer_characteristics value: %d", seqHeader->color_config.transfer_characteristics);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (seqHeader->color_config.matrix_coefficients) {
|
||||
case 1: // MC_BT_709
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2);
|
||||
break;
|
||||
|
||||
case 6: // MC_BT_601
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4);
|
||||
break;
|
||||
|
||||
case 7: // MC_SMPTE_240
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
||||
kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995);
|
||||
break;
|
||||
|
||||
case 9: // MC_BT_2020_NCL
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_2020);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_W, @"Unsupported matrix_coefficients value: %d", seqHeader->color_config.matrix_coefficients);
|
||||
break;
|
||||
}
|
||||
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_FullRangeVideo, @(seqHeader->color_config.color_range == 1));
|
||||
|
||||
// Progressive content
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_FieldCount, @(1));
|
||||
|
||||
switch (seqHeader->color_config.chroma_sample_position) {
|
||||
case 1: // CSP_VERTICAL
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
|
||||
kCMFormatDescriptionChromaLocation_Left);
|
||||
break;
|
||||
|
||||
case 2: // CSP_COLOCATED
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
|
||||
kCMFormatDescriptionChromaLocation_TopLeft);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_W, @"Unsupported chroma_sample_position value: %d", seqHeader->color_config.chroma_sample_position);
|
||||
break;
|
||||
}
|
||||
|
||||
if (contentLightLevelInfo) {
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_ContentLightLevelInfo, contentLightLevelInfo);
|
||||
}
|
||||
|
||||
if (masteringDisplayColorVolume) {
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_MasteringDisplayColorVolume, masteringDisplayColorVolume);
|
||||
}
|
||||
|
||||
// Referenced the VP9 code in Chrome that performs a similar function
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/mac/vt_config_util.mm;drc=977dc02c431b4979e34c7792bc3d646f649dacb4;l=155
|
||||
extensions[(__bridge NSString*)kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms] =
|
||||
@{
|
||||
@"av1C" : [self getAv1CodecConfigurationBox:frameData],
|
||||
};
|
||||
extensions[@"BitsPerComponent"] = @(bitstreamCtx->bit_depth);
|
||||
|
||||
#undef SET_EXTENSION
|
||||
#undef SET_CFSTR_EXTENSION
|
||||
|
||||
// AV1 doesn't have a special format description function like H.264 and HEVC have, so we just use the generic one
|
||||
CMVideoFormatDescriptionRef formatDesc = NULL;
|
||||
OSStatus status = CMVideoFormatDescriptionCreate(kCFAllocatorDefault, kCMVideoCodecType_AV1,
|
||||
bitstreamCtx->frame_width, bitstreamCtx->frame_height,
|
||||
(__bridge CFDictionaryRef)extensions,
|
||||
&formatDesc);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Failed to create AV1 format description: %d", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
|
||||
ff_cbs_fragment_free(&cbsFrag);
|
||||
ff_cbs_close(&cbsCtx);
|
||||
return formatDesc;
|
||||
}
|
||||
|
||||
// This function must free data for bufferType == BUFFER_TYPE_PICDATA
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du
|
||||
{
|
||||
OSStatus status;
|
||||
|
||||
// Construct a new format description object each time we receive an IDR frame
|
||||
if (du->frameType == FRAME_TYPE_IDR) {
|
||||
if (bufferType != BUFFER_TYPE_PICDATA) {
|
||||
if (bufferType == BUFFER_TYPE_VPS || bufferType == BUFFER_TYPE_SPS || bufferType == BUFFER_TYPE_PPS) {
|
||||
// Add new parameter set into the parameter set array
|
||||
int startLen = data[2] == 0x01 ? 3 : 4;
|
||||
[parameterSetBuffers addObject:[NSData dataWithBytes:&data[startLen] length:length - startLen]];
|
||||
}
|
||||
|
||||
// Data is NOT to be freed here. It's a direct usage of the caller's buffer.
|
||||
|
||||
// No frame data to submit for these NALUs
|
||||
return DR_OK;
|
||||
}
|
||||
|
||||
// Create the new format description when we get the first picture data buffer of an IDR frame.
|
||||
// This is the only way we know that there is no more CSD for this frame.
|
||||
//
|
||||
// NB: This logic depends on the fact that we submit all picture data in one buffer!
|
||||
|
||||
// Free the old format description
|
||||
if (formatDesc != NULL) {
|
||||
CFRelease(formatDesc);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
|
||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
// Construct parameter set arrays for the format description
|
||||
size_t parameterSetCount = [parameterSetBuffers count];
|
||||
const uint8_t* parameterSetPointers[parameterSetCount];
|
||||
size_t parameterSetSizes[parameterSetCount];
|
||||
for (int i = 0; i < parameterSetCount; i++) {
|
||||
NSData* parameterSet = parameterSetBuffers[i];
|
||||
parameterSetPointers[i] = parameterSet.bytes;
|
||||
parameterSetSizes[i] = parameterSet.length;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Constructing new H264 format description");
|
||||
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
|
||||
parameterSetCount,
|
||||
parameterSetPointers,
|
||||
parameterSetSizes,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
&formatDesc);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Failed to create H264 format description: %d", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
|
||||
// Free parameter set buffers after submission
|
||||
[parameterSetBuffers removeAllObjects];
|
||||
}
|
||||
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
|
||||
// Construct parameter set arrays for the format description
|
||||
size_t parameterSetCount = [parameterSetBuffers count];
|
||||
const uint8_t* parameterSetPointers[parameterSetCount];
|
||||
size_t parameterSetSizes[parameterSetCount];
|
||||
for (int i = 0; i < parameterSetCount; i++) {
|
||||
NSData* parameterSet = parameterSetBuffers[i];
|
||||
parameterSetPointers[i] = parameterSet.bytes;
|
||||
parameterSetSizes[i] = parameterSet.length;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Constructing new HEVC format description");
|
||||
|
||||
NSMutableDictionary* videoFormatParams = [[NSMutableDictionary alloc] init];
|
||||
|
||||
if (contentLightLevelInfo) {
|
||||
[videoFormatParams setObject:contentLightLevelInfo forKey:(__bridge NSString*)kCMFormatDescriptionExtension_ContentLightLevelInfo];
|
||||
}
|
||||
|
||||
if (masteringDisplayColorVolume) {
|
||||
[videoFormatParams setObject:masteringDisplayColorVolume forKey:(__bridge NSString*)kCMFormatDescriptionExtension_MasteringDisplayColorVolume];
|
||||
}
|
||||
|
||||
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
||||
parameterSetCount,
|
||||
parameterSetPointers,
|
||||
parameterSetSizes,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
(__bridge CFDictionaryRef)videoFormatParams,
|
||||
&formatDesc);
|
||||
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Failed to create HEVC format description: %d", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
|
||||
// Free parameter set buffers after submission
|
||||
[parameterSetBuffers removeAllObjects];
|
||||
}
|
||||
else if (videoFormat & VIDEO_FORMAT_MASK_AV1) {
|
||||
NSData* fullFrameData = [NSData dataWithBytesNoCopy:data length:length freeWhenDone:NO];
|
||||
|
||||
Log(LOG_I, @"Constructing new AV1 format description");
|
||||
formatDesc = [self createAV1FormatDescriptionForIDRFrame:fullFrameData];
|
||||
}
|
||||
else {
|
||||
// Unsupported codec!
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
if (formatDesc == NULL) {
|
||||
// Can't decode if we haven't gotten our parameter sets yet
|
||||
free(data);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
// Check for previous decoder errors before doing anything
|
||||
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
||||
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
|
||||
|
||||
// Recreate the display layer. We are already on the main thread,
|
||||
// so this is safe to do right here.
|
||||
[self reinitializeDisplayLayer];
|
||||
|
||||
// Request an IDR frame to initialize the new decoder
|
||||
free(data);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
// Now we're decoding actual frame data here
|
||||
CMBlockBufferRef frameBlockBuffer;
|
||||
CMBlockBufferRef dataBlockBuffer;
|
||||
|
||||
status = CMBlockBufferCreateWithMemoryBlock(NULL, data, length, kCFAllocatorDefault, NULL, 0, length, 0, &dataBlockBuffer);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferCreateWithMemoryBlock failed: %d", (int)status);
|
||||
free(data);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
// From now on, CMBlockBuffer owns the data pointer and will free it when it's dereferenced
|
||||
|
||||
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &frameBlockBuffer);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
|
||||
CFRelease(dataBlockBuffer);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
// H.264 and HEVC formats require NAL prefix fixups from Annex B to length-delimited
|
||||
if (videoFormat & (VIDEO_FORMAT_MASK_H264 | VIDEO_FORMAT_MASK_H265)) {
|
||||
int lastOffset = -1;
|
||||
for (int i = 0; i < length - NALU_START_PREFIX_SIZE; i++) {
|
||||
// Search for a NALU
|
||||
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
|
||||
// It's the start of a new NALU
|
||||
if (lastOffset != -1) {
|
||||
// We've seen a start before this so enqueue that NALU
|
||||
[self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:i - lastOffset];
|
||||
}
|
||||
|
||||
lastOffset = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastOffset != -1) {
|
||||
// Enqueue the remaining data
|
||||
[self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:length - lastOffset];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// For formats that require no length-changing fixups, just append a reference to the raw data block
|
||||
status = CMBlockBufferAppendBufferReference(frameBlockBuffer, dataBlockBuffer, 0, length, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
}
|
||||
|
||||
CMSampleBufferRef sampleBuffer;
|
||||
|
||||
CMSampleTimingInfo sampleTiming = {kCMTimeInvalid, CMTimeMake(du->presentationTimeMs, 1000), kCMTimeInvalid};
|
||||
|
||||
status = CMSampleBufferCreateReady(kCFAllocatorDefault,
|
||||
frameBlockBuffer,
|
||||
formatDesc, 1, 1,
|
||||
&sampleTiming, 0, NULL,
|
||||
&sampleBuffer);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMSampleBufferCreate failed: %d", (int)status);
|
||||
CFRelease(dataBlockBuffer);
|
||||
CFRelease(frameBlockBuffer);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
// Enqueue the next frame
|
||||
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||
|
||||
if (du->frameType == FRAME_TYPE_IDR) {
|
||||
// Ensure the layer is visible now
|
||||
self->displayLayer.hidden = NO;
|
||||
|
||||
// Tell our parent VC to hide the progress indicator
|
||||
[self->_callbacks videoContentShown];
|
||||
}
|
||||
|
||||
// Dereference the buffers
|
||||
CFRelease(dataBlockBuffer);
|
||||
CFRelease(frameBlockBuffer);
|
||||
CFRelease(sampleBuffer);
|
||||
|
||||
return DR_OK;
|
||||
}
|
||||
|
||||
- (void)setHdrMode:(BOOL)enabled {
|
||||
SS_HDR_METADATA hdrMetadata;
|
||||
|
||||
BOOL hasMetadata = enabled && LiGetHdrMetadata(&hdrMetadata);
|
||||
BOOL metadataChanged = NO;
|
||||
|
||||
if (hasMetadata && hdrMetadata.displayPrimaries[0].x != 0 && hdrMetadata.maxDisplayLuminance != 0) {
|
||||
// This data is all in big-endian
|
||||
struct {
|
||||
vector_ushort2 primaries[3];
|
||||
vector_ushort2 white_point;
|
||||
uint32_t luminance_max;
|
||||
uint32_t luminance_min;
|
||||
} __attribute__((packed, aligned(4))) mdcv;
|
||||
|
||||
// mdcv is in GBR order while SS_HDR_METADATA is in RGB order
|
||||
mdcv.primaries[0].x = __builtin_bswap16(hdrMetadata.displayPrimaries[1].x);
|
||||
mdcv.primaries[0].y = __builtin_bswap16(hdrMetadata.displayPrimaries[1].y);
|
||||
mdcv.primaries[1].x = __builtin_bswap16(hdrMetadata.displayPrimaries[2].x);
|
||||
mdcv.primaries[1].y = __builtin_bswap16(hdrMetadata.displayPrimaries[2].y);
|
||||
mdcv.primaries[2].x = __builtin_bswap16(hdrMetadata.displayPrimaries[0].x);
|
||||
mdcv.primaries[2].y = __builtin_bswap16(hdrMetadata.displayPrimaries[0].y);
|
||||
|
||||
mdcv.white_point.x = __builtin_bswap16(hdrMetadata.whitePoint.x);
|
||||
mdcv.white_point.y = __builtin_bswap16(hdrMetadata.whitePoint.y);
|
||||
|
||||
// These luminance values are in 10000ths of a nit
|
||||
mdcv.luminance_max = __builtin_bswap32((uint32_t)hdrMetadata.maxDisplayLuminance * 10000);
|
||||
mdcv.luminance_min = __builtin_bswap32(hdrMetadata.minDisplayLuminance);
|
||||
|
||||
NSData* newMdcv = [NSData dataWithBytes:&mdcv length:sizeof(mdcv)];
|
||||
if (masteringDisplayColorVolume == nil || ![newMdcv isEqualToData:masteringDisplayColorVolume]) {
|
||||
masteringDisplayColorVolume = newMdcv;
|
||||
metadataChanged = YES;
|
||||
}
|
||||
}
|
||||
else if (masteringDisplayColorVolume != nil) {
|
||||
masteringDisplayColorVolume = nil;
|
||||
metadataChanged = YES;
|
||||
}
|
||||
|
||||
if (hasMetadata && hdrMetadata.maxContentLightLevel != 0 && hdrMetadata.maxFrameAverageLightLevel != 0) {
|
||||
// This data is all in big-endian
|
||||
struct {
|
||||
uint16_t max_content_light_level;
|
||||
uint16_t max_frame_average_light_level;
|
||||
} __attribute__((packed, aligned(2))) cll;
|
||||
|
||||
cll.max_content_light_level = __builtin_bswap16(hdrMetadata.maxContentLightLevel);
|
||||
cll.max_frame_average_light_level = __builtin_bswap16(hdrMetadata.maxFrameAverageLightLevel);
|
||||
|
||||
NSData* newCll = [NSData dataWithBytes:&cll length:sizeof(cll)];
|
||||
if (contentLightLevelInfo == nil || ![newCll isEqualToData:contentLightLevelInfo]) {
|
||||
contentLightLevelInfo = newCll;
|
||||
metadataChanged = YES;
|
||||
}
|
||||
}
|
||||
else if (contentLightLevelInfo != nil) {
|
||||
contentLightLevelInfo = nil;
|
||||
metadataChanged = YES;
|
||||
}
|
||||
|
||||
// If the metadata changed, request an IDR frame to re-create the CMVideoFormatDescription
|
||||
if (metadataChanged) {
|
||||
LiRequestIdrFrame();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// UIComputerView.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/22/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TemporaryHost.h"
|
||||
|
||||
@protocol HostCallback <NSObject>
|
||||
|
||||
- (void) hostClicked:(TemporaryHost*)host view:(UIView*)view;
|
||||
- (void) hostLongClicked:(TemporaryHost*)host view:(UIView*)view;
|
||||
- (void) addHostClicked;
|
||||
|
||||
@end
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface UIComputerView : UIButton <UIContextMenuInteractionDelegate>
|
||||
#else
|
||||
@interface UIComputerView : UIButton
|
||||
#endif
|
||||
|
||||
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback;
|
||||
- (id) initForAddWithCallback:(id<HostCallback>)callback;
|
||||
|
||||
@end
|
||||
@@ -1,239 +0,0 @@
|
||||
//
|
||||
// UIComputerView.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/22/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "UIComputerView.h"
|
||||
|
||||
@implementation UIComputerView {
|
||||
TemporaryHost* _host;
|
||||
UIImageView* _hostIcon;
|
||||
UILabel* _hostLabel;
|
||||
UIImageView* _hostOverlay;
|
||||
UIActivityIndicatorView* _hostSpinner;
|
||||
id<HostCallback> _callback;
|
||||
CGSize _labelSize;
|
||||
}
|
||||
static const float REFRESH_CYCLE = 2.0f;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
static const int ITEM_PADDING = 50;
|
||||
static const int LABEL_DY = 40;
|
||||
#else
|
||||
static const int ITEM_PADDING = 0;
|
||||
static const int LABEL_DY = 20;
|
||||
#endif
|
||||
|
||||
- (id) init {
|
||||
self = [super init];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
self.frame = CGRectMake(0, 0, 400, 400);
|
||||
#else
|
||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
||||
self.frame = CGRectMake(0, 0, 200, 200);
|
||||
} else {
|
||||
self.frame = CGRectMake(0, 0, 100, 100);
|
||||
}
|
||||
#endif
|
||||
|
||||
_hostIcon = [[UIImageView alloc] initWithFrame:self.frame];
|
||||
[_hostIcon setImage:[UIImage imageNamed:@"Computer"]];
|
||||
|
||||
self.layer.shadowColor = [[UIColor blackColor] CGColor];
|
||||
self.layer.shadowOffset = CGSizeMake(5,8);
|
||||
self.layer.shadowOpacity = 0.3;
|
||||
|
||||
[self addTarget:self action:@selector(hostButtonSelected:) forControlEvents:UIControlEventTouchDown];
|
||||
[self addTarget:self action:@selector(hostButtonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
|
||||
|
||||
_hostLabel = [[UILabel alloc] init];
|
||||
_hostLabel.textColor = [UIColor whiteColor];
|
||||
|
||||
_hostOverlay = [[UIImageView alloc] initWithFrame:CGRectMake(self.frame.size.width / 3, _hostIcon.frame.size.height / 4, _hostIcon.frame.size.width / 3, self.frame.size.height / 3)];
|
||||
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
||||
[_hostSpinner setFrame:_hostOverlay.frame];
|
||||
_hostSpinner.userInteractionEnabled = NO;
|
||||
_hostSpinner.hidesWhenStopped = YES;
|
||||
|
||||
[self addSubview:_hostLabel];
|
||||
[self addSubview:_hostIcon];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
_hostIcon.clipsToBounds = NO;
|
||||
_hostIcon.adjustsImageWhenAncestorFocused = YES;
|
||||
_hostIcon.masksFocusEffectToContents = YES;
|
||||
|
||||
self.adjustsImageWhenHighlighted = NO;
|
||||
|
||||
_hostOverlay.masksFocusEffectToContents = YES;
|
||||
_hostOverlay.adjustsImageWhenAncestorFocused = NO;
|
||||
|
||||
[_hostIcon.overlayContentView addSubview:_hostOverlay];
|
||||
[_hostIcon.overlayContentView addSubview:_hostSpinner];
|
||||
#else
|
||||
[self addSubview:_hostOverlay];
|
||||
[self addSubview:_hostSpinner];
|
||||
|
||||
if (@available(iOS 13.4.1, *)) {
|
||||
// Allow the button style to change when moused over
|
||||
self.pointerInteractionEnabled = YES;
|
||||
}
|
||||
#endif
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) hostButtonSelected:(id)sender {
|
||||
_hostIcon.layer.opacity = 0.5f;
|
||||
_hostSpinner.layer.opacity = 0.5f;
|
||||
_hostOverlay.layer.opacity = 0.5f;
|
||||
}
|
||||
- (void) hostButtonDeselected:(id)sender {
|
||||
_hostIcon.layer.opacity = 1.0f;
|
||||
_hostSpinner.layer.opacity = 1.0f;
|
||||
_hostOverlay.layer.opacity = 1.0f;
|
||||
}
|
||||
|
||||
- (id) initForAddWithCallback:(id<HostCallback>)callback {
|
||||
self = [self init];
|
||||
_callback = callback;
|
||||
|
||||
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
|
||||
[_hostLabel setText:@"Add Host Manually"];
|
||||
[_hostLabel sizeToFit];
|
||||
|
||||
[_hostOverlay setImage:[UIImage imageNamed:@"AddOverlayIcon"]];
|
||||
|
||||
[self updateBounds];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback {
|
||||
self = [self init];
|
||||
_host = host;
|
||||
_callback = callback;
|
||||
|
||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
||||
// for tvOS devices and iOS prior to 13.0.
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
|
||||
[self addInteraction:rightClickInteraction];
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
|
||||
[self addGestureRecognizer:longPressRecognizer];
|
||||
}
|
||||
|
||||
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
|
||||
[self updateContentsForHost:host];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview {
|
||||
// Start our update loop when we are added to our cell
|
||||
if (self.superview != nil && _host != nil) {
|
||||
[self updateLoop];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) updateBounds {
|
||||
float x = FLT_MAX;
|
||||
float y = FLT_MAX;
|
||||
float width = 0;
|
||||
float height;
|
||||
|
||||
float iconX = _hostIcon.frame.origin.x + _hostIcon.frame.size.width / 2;
|
||||
_hostLabel.center = CGPointMake(iconX, _hostIcon.frame.origin.y + _hostIcon.frame.size.height + LABEL_DY);
|
||||
|
||||
x = MIN(x, _hostIcon.frame.origin.x);
|
||||
x = MIN(x, _hostLabel.frame.origin.x);
|
||||
|
||||
y = MIN(y, _hostIcon.frame.origin.y);
|
||||
y = MIN(y, _hostLabel.frame.origin.y);
|
||||
|
||||
width = MAX(width, _hostIcon.frame.size.width);
|
||||
width = MAX(width, _hostLabel.frame.size.width);
|
||||
|
||||
height = _hostIcon.frame.size.height +
|
||||
_hostLabel.frame.size.height +
|
||||
LABEL_DY / 2;
|
||||
|
||||
self.bounds = CGRectMake(x - ITEM_PADDING, y - ITEM_PADDING, width + 2 * ITEM_PADDING, height + 2 * ITEM_PADDING);
|
||||
}
|
||||
|
||||
- (void) updateContentsForHost:(TemporaryHost*)host {
|
||||
_hostLabel.text = _host.name;
|
||||
[_hostLabel sizeToFit];
|
||||
|
||||
if (host.state == StateOnline) {
|
||||
[_hostSpinner stopAnimating];
|
||||
|
||||
if (host.pairState == PairStateUnpaired) {
|
||||
[_hostOverlay setImage:[UIImage imageNamed:@"LockedOverlayIcon"]];
|
||||
}
|
||||
else {
|
||||
[_hostOverlay setImage:nil];
|
||||
}
|
||||
}
|
||||
else if (host.state == StateOffline) {
|
||||
[_hostSpinner stopAnimating];
|
||||
[_hostOverlay setImage:[UIImage imageNamed:@"ErrorOverlayIcon"]];
|
||||
}
|
||||
else {
|
||||
[_hostSpinner startAnimating];
|
||||
}
|
||||
|
||||
[self updateBounds];
|
||||
}
|
||||
|
||||
- (void) updateLoop {
|
||||
// Stop immediately if the view has been detached
|
||||
if (self.superview == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self updateContentsForHost:_host];
|
||||
|
||||
// Queue the next refresh cycle
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
}
|
||||
|
||||
- (void) hostLongClicked:(UILongPressGestureRecognizer*)gesture {
|
||||
if (gesture.state == UIGestureRecognizerStateBegan) {
|
||||
[_callback hostLongClicked:_host view:self];
|
||||
}
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
|
||||
configurationForMenuAtLocation:(CGPoint)location {
|
||||
// We don't want to trigger the primary action at this point, so cancel
|
||||
// tracking touch on this view now. This will also have the (intended)
|
||||
// effect of removing the touch highlight on this view.
|
||||
[self cancelTrackingWithEvent:nil];
|
||||
|
||||
[_callback hostLongClicked:_host view:self];
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void) hostClicked {
|
||||
[_callback hostClicked:_host view:self];
|
||||
}
|
||||
|
||||
- (void) addClicked {
|
||||
[_callback addHostClicked];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// Logger.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 2/10/15.
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Limelight_Logger_h
|
||||
#define Limelight_Logger_h
|
||||
|
||||
#import <stdarg.h>
|
||||
|
||||
typedef enum {
|
||||
LOG_D,
|
||||
LOG_I,
|
||||
LOG_W,
|
||||
LOG_E
|
||||
} LogLevel;
|
||||
|
||||
#define PRFX_DEBUG @"<DEBUG>"
|
||||
#define PRFX_INFO @"<INFO>"
|
||||
#define PRFX_WARN @"<WARN>"
|
||||
#define PRFX_ERROR @"<ERROR>"
|
||||
|
||||
void Log(LogLevel level, NSString* fmt, ...);
|
||||
void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...);
|
||||
|
||||
#endif
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// MainFrameViewController.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/17/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "DiscoveryManager.h"
|
||||
#import "PairManager.h"
|
||||
#import "StreamConfiguration.h"
|
||||
#import "UIComputerView.h"
|
||||
#import "UIAppView.h"
|
||||
#import "AppAssetManager.h"
|
||||
#import "SWRevealViewController.h"
|
||||
|
||||
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate>
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem *settingsButton;
|
||||
#if !TARGET_OS_TV
|
||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem *upButton;
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "AppDelegate.h"
|
||||
#import "CustomOSCViewControl/LayoutOnScreenControlsViewController.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "CustomEdgeSlideGestureRecognizer.h"
|
||||
|
||||
@interface SettingsViewController : UIViewController
|
||||
@property (strong, nonatomic) IBOutlet UILabel *bitrateLabel;
|
||||
@@ -15,18 +18,50 @@
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
|
||||
@property (strong, nonatomic) IBOutlet UIView *resolutionDisplayView;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *touchModeLabel;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *onscreenControllerLabel;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *motionModeSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *largerStickLR1Selector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *optimizeSettingsSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *swapABXYButtonsSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *pipSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
|
||||
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *keyboardToggleFingerNumLabel;
|
||||
@property (strong, nonatomic) IBOutlet UISlider *keyboardToggleFingerNumSlider;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *liftStreamViewForKeyboardSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *showKeyboardToolbarSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *slideToSettingsScreenEdgeSelector;
|
||||
|
||||
@property (strong, nonatomic) IBOutlet UILabel *oscTapExlusionAreaSizeLabel;
|
||||
@property (strong, nonatomic) IBOutlet UISlider *oscTapExlusionAreaSizeSlider;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *slideToSettingsScreenEdgeUILabel;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *cmdToolScreenEdgeSelector;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *slideToSettingsDistanceUILabel;
|
||||
@property (strong, nonatomic) IBOutlet UISlider *slideToMenuDistanceSlider;
|
||||
@property (strong, nonatomic) IBOutlet UISlider *pointerVelocityModeDividerSlider;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *pointerVelocityModeDividerUILabel;
|
||||
@property (strong, nonatomic) IBOutlet UISlider *touchPointerVelocityFactorSlider;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *touchPointerVelocityFactorUILabel;
|
||||
@property (strong, nonatomic) IBOutlet UISlider *mousePointerVelocityFactorSlider;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *mousePointerVelocityFactorUILabel;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *unlockDisplayOrientationSelector;
|
||||
@property (strong, nonatomic) IBOutlet UIButton *backToStreamingButton;
|
||||
@property (strong, nonatomic) LayoutOnScreenControlsViewController *layoutOnScreenControlsVC;
|
||||
@property (nonatomic, strong) MainFrameViewController *mainFrameViewController;
|
||||
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||
@@ -37,5 +72,8 @@
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
- (void) saveSettings;
|
||||
+ (bool) isLandscapeNow;
|
||||
- (void)updateResolutionTable;
|
||||
- (void) widget:(UISlider*)widget setEnabled:(bool)enabled;
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,17 +10,23 @@
|
||||
#import "TemporarySettings.h"
|
||||
#import "DataManager.h"
|
||||
|
||||
#import <UIKit/UIGestureRecognizerSubclass.h>
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "LocalizationHelper.h"
|
||||
|
||||
@implementation SettingsViewController {
|
||||
NSInteger _bitrate;
|
||||
NSInteger _lastSelectedResolutionIndex;
|
||||
bool justEnteredSettingsViewDoNotOpenOscLayoutTool;
|
||||
uint16_t oscLayoutFingers;
|
||||
CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer;
|
||||
}
|
||||
|
||||
@dynamic overrideUserInterfaceStyle;
|
||||
|
||||
static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
|
||||
//static NSString* bitrateFormat;
|
||||
static const int bitrateTable[] = {
|
||||
500,
|
||||
1000,
|
||||
@@ -35,19 +41,94 @@ static const int bitrateTable[] = {
|
||||
8000,
|
||||
9000,
|
||||
10000,
|
||||
11000,
|
||||
12000,
|
||||
13000,
|
||||
14000,
|
||||
15000,
|
||||
16000,
|
||||
17000,
|
||||
18000,
|
||||
19000,
|
||||
20000,
|
||||
21000,
|
||||
22000,
|
||||
23000,
|
||||
24000,
|
||||
25000,
|
||||
26000,
|
||||
27000,
|
||||
28000,
|
||||
29000,
|
||||
30000,
|
||||
31000,
|
||||
32000,
|
||||
33000,
|
||||
34000,
|
||||
35000,
|
||||
36000,
|
||||
37000,
|
||||
38000,
|
||||
39000,
|
||||
40000,
|
||||
41000,
|
||||
42000,
|
||||
43000,
|
||||
44000,
|
||||
45000,
|
||||
46000,
|
||||
47000,
|
||||
48000,
|
||||
49000,
|
||||
50000,
|
||||
50000,
|
||||
51000,
|
||||
52000,
|
||||
53000,
|
||||
54000,
|
||||
55000,
|
||||
56000,
|
||||
57000,
|
||||
58000,
|
||||
59000,
|
||||
60000,
|
||||
61000,
|
||||
62000,
|
||||
63000,
|
||||
64000,
|
||||
65000,
|
||||
66000,
|
||||
67000,
|
||||
68000,
|
||||
69000,
|
||||
70000,
|
||||
80000,
|
||||
90000,
|
||||
100000,
|
||||
110000,
|
||||
120000,
|
||||
130000,
|
||||
140000,
|
||||
150000,
|
||||
160000,
|
||||
170000,
|
||||
180000,
|
||||
200000,
|
||||
220000,
|
||||
240000,
|
||||
260000,
|
||||
280000,
|
||||
300000,
|
||||
320000,
|
||||
340000,
|
||||
360000,
|
||||
380000,
|
||||
400000,
|
||||
420000,
|
||||
440000,
|
||||
460000,
|
||||
480000,
|
||||
500000,
|
||||
};
|
||||
|
||||
const int RESOLUTION_TABLE_SIZE = 7;
|
||||
@@ -118,7 +199,8 @@ BOOL isCustomResolution(CGSize res) {
|
||||
}
|
||||
|
||||
for (int i = 0; i < RESOLUTION_TABLE_CUSTOM_INDEX; i++) {
|
||||
if (res.width == resolutionTable[i].width && res.height == resolutionTable[i].height) {
|
||||
|
||||
if ((res.width == resolutionTable[i].width && res.height == resolutionTable[i].height) || (res.height == resolutionTable[i].width && res.width == resolutionTable[i].height)) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
@@ -126,9 +208,154 @@ BOOL isCustomResolution(CGSize res) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
+ (bool)isLandscapeNow {
|
||||
return CGRectGetWidth([[UIScreen mainScreen]bounds]) > CGRectGetHeight([[UIScreen mainScreen]bounds]);
|
||||
}
|
||||
|
||||
- (bool)isFullScreenRequired {
|
||||
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
|
||||
NSNumber *requiresFullScreen = infoDictionary[@"UIRequiresFullScreen"];
|
||||
|
||||
if (requiresFullScreen != nil) {
|
||||
return [requiresFullScreen boolValue];
|
||||
}
|
||||
// Default behavior if the key is not set
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)isAirPlayEnabled{
|
||||
return [self.externalDisplayModeSelector selectedSegmentIndex] == 1;
|
||||
}
|
||||
|
||||
- (void)updateResolutionTable{
|
||||
UIWindow *window = self.view.window;
|
||||
CGFloat screenScale = window.screen.scale;
|
||||
CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
|
||||
CGFloat appWindowWidth = window.frame.size.width * screenScale;
|
||||
CGFloat appWindowHeight = window.frame.size.height * screenScale;
|
||||
if([self isAirPlayEnabled] && UIScreen.screens.count > 1){
|
||||
CGRect bounds = [UIScreen.screens.lastObject bounds];
|
||||
screenScale = [UIScreen.screens.lastObject scale];
|
||||
appWindowWidth = bounds.size.width * screenScale;
|
||||
appWindowHeight = bounds.size.height * screenScale;
|
||||
}
|
||||
bool needSwapWidthAndHeight = appWindowWidth > appWindowHeight;
|
||||
|
||||
resolutionTable[4] = CGSizeMake(safeAreaWidth, appWindowHeight);
|
||||
|
||||
for(uint8_t i=0;i<7;i++){
|
||||
CGFloat longSideLen = resolutionTable[i].height > resolutionTable[i].width ? resolutionTable[i].height : resolutionTable[i].width;
|
||||
CGFloat shortSideLen = resolutionTable[i].height < resolutionTable[i].width ? resolutionTable[i].height : resolutionTable[i].width;
|
||||
if(needSwapWidthAndHeight) resolutionTable[i] = CGSizeMake(longSideLen, shortSideLen);
|
||||
else resolutionTable[i] = CGSizeMake(shortSideLen, longSideLen);
|
||||
}
|
||||
|
||||
resolutionTable[5] = CGSizeMake(appWindowWidth, appWindowHeight);
|
||||
|
||||
[self updateResolutionDisplayViewText];
|
||||
}
|
||||
|
||||
// this will also be called back when device orientation changes
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||
double delayInSeconds = 0.2;
|
||||
// Convert the delay into a dispatch_time_t value
|
||||
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
|
||||
// Perform some task after the delay
|
||||
dispatch_after(delayTime, dispatch_get_main_queue(), ^{// Code to execute after the delay
|
||||
[self updateResolutionTable];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated{
|
||||
[super viewDidAppear:animated];
|
||||
[self updateResolutionTable];
|
||||
NSLog(@"Resolution table updated");
|
||||
}
|
||||
|
||||
|
||||
- (void)deviceOrientationDidChange{
|
||||
double delayInSeconds = 1.0;
|
||||
// Convert the delay into a dispatch_time_t value
|
||||
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
|
||||
// Perform some task after the delay
|
||||
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
|
||||
// Code to execute after the delay
|
||||
// [self updateResolutionTable];
|
||||
// since we have viewWillTransitionToSize being called back both when orientation changed & app window size changed, resoltion update is dprecated here
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)simulateSettingsButtonPress{
|
||||
[self.mainFrameViewController simulateSettingsButtonPress];
|
||||
}
|
||||
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated{
|
||||
[super viewDidDisappear:animated];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"SettingsViewClosedNotification" object:self]; // notify other view that settings view just closed
|
||||
}
|
||||
|
||||
- (void)pushDownExistingWidgets{
|
||||
// Calculate the height needed for the exit button
|
||||
CGFloat exitButtonHeight = 50.0; // Adjust as needed
|
||||
CGFloat topOffset = exitButtonHeight; // Space needed for the exit button
|
||||
// Iterate through subviews of self.view and adjust their frames
|
||||
for (UIView *subview in self.view.subviews) {
|
||||
CGRect frame = subview.frame;
|
||||
frame.origin.y += topOffset;
|
||||
subview.frame = frame;
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)exitButtonTapped:(id)sender {
|
||||
[self->_mainFrameViewController simulateSettingsButtonPress];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"SessionDisconnectedBySettingsViewNotification" object:self];
|
||||
}
|
||||
|
||||
- (IBAction)backToStreamingButtonTapped:(id)sender {
|
||||
[self simulateSettingsButtonPress];
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
- (void)addExitButtonOnTop{
|
||||
UIButton *exitButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[exitButton setTitle:@"Exit" forState:UIControlStateNormal];
|
||||
[exitButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // Set font color
|
||||
exitButton.backgroundColor = [UIColor clearColor]; // Set background color
|
||||
exitButton.titleLabel.font = [UIFont boldSystemFontOfSize:16.0]; // Set font size and style
|
||||
exitButton.layer.cornerRadius = 8.0; // Optional: Round corners if desired
|
||||
exitButton.clipsToBounds = YES; // Ensure rounded corners are applied properly
|
||||
[exitButton addTarget:self action:@selector(exitButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.view addSubview:exitButton];
|
||||
exitButton.frame = CGRectMake(0, 20, 200, 50); // Adjust Y and height as needed
|
||||
}*/
|
||||
|
||||
- (void)edgeSwiped {
|
||||
[self simulateSettingsButtonPress];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
//[self pushDownExistingWidgets];
|
||||
//[self addExitButtonOnTop];
|
||||
|
||||
self->slideToCloseSettingsViewRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
|
||||
slideToCloseSettingsViewRecognizer.edges = UIRectEdgeLeft;
|
||||
slideToCloseSettingsViewRecognizer.normalizedThresholdDistance = 0.0;
|
||||
slideToCloseSettingsViewRecognizer.EDGE_TOLERANCE = 10;
|
||||
slideToCloseSettingsViewRecognizer.immediateTriggering = true;
|
||||
slideToCloseSettingsViewRecognizer.delaysTouchesBegan = NO;
|
||||
slideToCloseSettingsViewRecognizer.delaysTouchesEnded = NO;
|
||||
[self.view addGestureRecognizer:slideToCloseSettingsViewRecognizer];
|
||||
|
||||
justEnteredSettingsViewDoNotOpenOscLayoutTool = true;
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(deviceOrientationDidChange) // handle orientation change since i made portrait mode available
|
||||
name:UIDeviceOrientationDidChangeNotification
|
||||
object:nil];
|
||||
|
||||
// Always run settings in dark mode because we want the light fonts
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
|
||||
@@ -159,6 +386,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
|
||||
resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
|
||||
resolutionTable[6] = CGSizeMake([currentSettings.width integerValue], [currentSettings.height integerValue]); // custom initial value
|
||||
[self updateResolutionTable];
|
||||
|
||||
// Don't populate the custom entry unless we have a custom resolution
|
||||
if (!isCustomResolution(resolutionTable[6])) {
|
||||
@@ -179,13 +407,9 @@ BOOL isCustomResolution(CGSize res) {
|
||||
break;
|
||||
}
|
||||
|
||||
NSInteger resolution = 1;
|
||||
for (int i = 0; i < RESOLUTION_TABLE_SIZE; i++) {
|
||||
if ((int) resolutionTable[i].height == [currentSettings.height intValue]
|
||||
&& (int) resolutionTable[i].width == [currentSettings.width intValue]) {
|
||||
resolution = i;
|
||||
break;
|
||||
}
|
||||
NSInteger resolution = currentSettings.resolutionSelected.integerValue;
|
||||
if(resolution >= RESOLUTION_TABLE_SIZE){
|
||||
resolution = 0;
|
||||
}
|
||||
|
||||
// Only show the 120 FPS option if we have a > 60-ish Hz display
|
||||
@@ -233,41 +457,320 @@ BOOL isCustomResolution(CGSize res) {
|
||||
|
||||
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || !(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10)) {
|
||||
[self.hdrSelector removeAllSegments];
|
||||
[self.hdrSelector insertSegmentWithTitle:@"Unsupported on this device" atIndex:0 animated:NO];
|
||||
[self.hdrSelector insertSegmentWithTitle:[LocalizationHelper localizedStringForKey:@"Unsupported on this device"] atIndex:0 animated:NO];
|
||||
[self.hdrSelector setEnabled:NO];
|
||||
}
|
||||
else {
|
||||
[self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0];
|
||||
}
|
||||
|
||||
[self.touchModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
|
||||
[self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlay ? 1 : 0];
|
||||
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
|
||||
[self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0];
|
||||
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
|
||||
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
|
||||
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
||||
[self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
|
||||
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
|
||||
[self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
|
||||
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
|
||||
NSInteger onscreenControls = [currentSettings.onscreenControls integerValue];
|
||||
_lastSelectedResolutionIndex = resolution;
|
||||
[self.resolutionSelector setSelectedSegmentIndex:resolution];
|
||||
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
|
||||
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
||||
[self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
|
||||
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
|
||||
[self.onscreenControlSelector setEnabled:!currentSettings.absoluteTouchMode];
|
||||
NSInteger motionMode = [currentSettings.motionMode integerValue];
|
||||
[self.motionModeSelector setSelectedSegmentIndex:motionMode];
|
||||
[self.bitrateSlider setMinimumValue:0];
|
||||
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
||||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||||
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
||||
[self updateBitrateText];
|
||||
[self updateResolutionDisplayViewText];
|
||||
|
||||
// Unlock Display Orientation setting
|
||||
bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation woring
|
||||
if(unlockDisplayOrientationSelectorEnabled) [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:currentSettings.unlockDisplayOrientation ? 1 : 0];
|
||||
else [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:1]; // can't lock screen orientation in this mode = Display Orientation always unlocked
|
||||
[self.unlockDisplayOrientationSelector setEnabled:unlockDisplayOrientationSelectorEnabled];
|
||||
|
||||
|
||||
// lift streamview setting
|
||||
[self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:currentSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting
|
||||
|
||||
// showkeyboard toolbar setting
|
||||
[self.showKeyboardToolbarSelector setSelectedSegmentIndex:currentSettings.showKeyboardToolbar ? 1 : 0];// Load old setting
|
||||
|
||||
// reverse mouse wheel direction setting
|
||||
[self.reverseMouseWheelDirectionSelector setSelectedSegmentIndex:currentSettings.reverseMouseWheelDirection ? 1 : 0];// Load old setting
|
||||
|
||||
// slide to menu settings
|
||||
[self.slideToSettingsScreenEdgeSelector setSelectedSegmentIndex:[self getSelectorIndexFromScreenEdge:(uint32_t)currentSettings.slideToSettingsScreenEdge.integerValue]];
|
||||
// Load old setting
|
||||
[self.cmdToolScreenEdgeSelector setEnabled:false];
|
||||
[self.slideToSettingsScreenEdgeSelector addTarget:self action:@selector(slideToSettingsScreenEdgeChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self slideToSettingsScreenEdgeChanged];
|
||||
|
||||
[self.slideToMenuDistanceSlider setValue:currentSettings.slideToSettingsDistance.floatValue];
|
||||
[self.slideToMenuDistanceSlider addTarget:self action:@selector(slideToMenuDistanceSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self slideToMenuDistanceSliderMoved];
|
||||
|
||||
|
||||
|
||||
//TouchMode & OSC Related Settings:
|
||||
|
||||
// pointer veloc setting, will be enable/disabled by touchMode
|
||||
[self.pointerVelocityModeDividerSlider setValue: (uint8_t)(currentSettings.pointerVelocityModeDivider.floatValue * 100) animated:YES]; // Load old setting.
|
||||
[self.pointerVelocityModeDividerSlider addTarget:self action:@selector(pointerVelocityModeDividerSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self pointerVelocityModeDividerSliderMoved];
|
||||
|
||||
// init pointer veloc setting, will be enable/disabled by touchMode
|
||||
[self.touchPointerVelocityFactorSlider setValue: [self map_SliderValue_fromVelocFactor: currentSettings.touchPointerVelocityFactor.floatValue] animated:YES]; // Load old setting.
|
||||
[self.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self touchPointerVelocityFactorSliderMoved];
|
||||
|
||||
// init relative touch mouse pointer veloc setting, will be enable/disabled by touchMode
|
||||
[self.mousePointerVelocityFactorSlider setValue:[self map_SliderValue_fromVelocFactor: currentSettings.mousePointerVelocityFactor.floatValue] animated:YES]; // Load old setting.
|
||||
[self.mousePointerVelocityFactorSlider addTarget:self action:@selector(mousePointerVelocityFactorSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self mousePointerVelocityFactorSliderMoved];
|
||||
|
||||
|
||||
// these settings will be affected by onscreenControl & touchMode, must be loaded before them.
|
||||
// NSLog(@"osc tool fingers setting test: %d", currentSettings.oscLayoutToolFingers.intValue);
|
||||
self->oscLayoutFingers = (uint16_t)currentSettings.oscLayoutToolFingers.intValue; // load old setting of oscLayoutFingers
|
||||
[self.keyboardToggleFingerNumSlider setValue:(CGFloat)currentSettings.keyboardToggleFingers.intValue animated:YES]; // Load old setting. old setting was converted to uint16_t before saving.
|
||||
[self.keyboardToggleFingerNumSlider addTarget:self action:@selector(keyboardToggleFingerNumSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self keyboardToggleFingerNumSliderMoved];
|
||||
|
||||
// this setting will be affected by touchMode, must be loaded before them.
|
||||
NSInteger onscreenControlsLevel = [currentSettings.onscreenControls integerValue];
|
||||
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControlsLevel];
|
||||
[self.onscreenControlSelector addTarget:self action:@selector(onscreenControlChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self onscreenControlChanged];
|
||||
[self.largerStickLR1Selector setSelectedSegmentIndex:currentSettings.largerStickLR1 ? 1 : 0]; // load old setting of largerStickLR1
|
||||
|
||||
// tap exclusion area size for custom OSC, must be loaded before touchMode & osc selector.
|
||||
[self.oscTapExlusionAreaSizeSlider setValue:currentSettings.oscTapExlusionAreaSize.floatValue * 100 animated:YES]; // Load old setting.
|
||||
[self.oscTapExlusionAreaSizeSlider addTarget:self action:@selector(oscTapExlusionAreaSizeSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||||
[self oscTapExlusionAreaSizeSliderMoved];
|
||||
|
||||
|
||||
// [self.touchModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
|
||||
// this part will enable/disable oscSelector & the largerStickLR1 selector
|
||||
[self.touchModeSelector setSelectedSegmentIndex:currentSettings.touchMode.intValue]; //Load old touchMode setting
|
||||
[self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self touchModeChanged];
|
||||
|
||||
|
||||
// init CustomOSC stuff
|
||||
/* sets a reference to the correct 'LayoutOnScreenControlsViewController' depending on whether the user is on an iPhone or iPad */
|
||||
self.layoutOnScreenControlsVC = [[LayoutOnScreenControlsViewController alloc] init];
|
||||
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||||
if (isIPhone) {
|
||||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
|
||||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||||
}
|
||||
else {
|
||||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil];
|
||||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||||
self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
}
|
||||
|
||||
[self.externalDisplayModeSelector setSelectedSegmentIndex:currentSettings.externalDisplayMode.integerValue];
|
||||
[self.localMousePointerModeSelector setSelectedSegmentIndex:currentSettings.localMousePointerMode.integerValue];
|
||||
}
|
||||
|
||||
- (void)slideToSettingsScreenEdgeChanged{
|
||||
if([self.slideToSettingsScreenEdgeSelector selectedSegmentIndex] == 0) [self.cmdToolScreenEdgeSelector setSelectedSegmentIndex:1];
|
||||
else [self.cmdToolScreenEdgeSelector setSelectedSegmentIndex:0];
|
||||
}
|
||||
|
||||
|
||||
- (void)updateTouchModeLabel{
|
||||
NSString* labelText;
|
||||
switch([self.touchModeSelector selectedSegmentIndex]){
|
||||
case RELATIVE_TOUCH:
|
||||
labelText = [LocalizationHelper localizedStringForKey:@"Touch Mode - Double Tap to Drag, OSC Available"];break;
|
||||
case REGULAR_NATIVE_TOUCH:
|
||||
labelText = [LocalizationHelper localizedStringForKey:@"Touch Mode - With OSC & Mouse Support"];break;
|
||||
case PURE_NATIVE_TOUCH:
|
||||
labelText = [LocalizationHelper localizedStringForKey:@"Touch Mode - No OSC & Mouse Support"];break;
|
||||
case ABSOLUTE_TOUCH:
|
||||
labelText = [LocalizationHelper localizedStringForKey:@"Touch Mode - For MacOS Direct Touch"];break;
|
||||
}
|
||||
[self.touchModeLabel setText:labelText];
|
||||
}
|
||||
|
||||
- (void)showCustomOSCTip {
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Rebase in Stream View"]
|
||||
message:[LocalizationHelper localizedStringForKey:@"Tap %d fingers to change layout in stream view, or change the number of fingers required to:", self->oscLayoutFingers]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"%d", self->oscLayoutFingers];
|
||||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
||||
}];
|
||||
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"]
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil];
|
||||
|
||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
UITextField *textField = alertController.textFields.firstObject;
|
||||
NSString *inputText = textField.text;
|
||||
NSInteger fingers = [inputText integerValue];
|
||||
if (inputText.length > 0 && fingers >= 4) {
|
||||
self->oscLayoutFingers = (uint16_t) fingers;
|
||||
NSLog(@"OK button tapped with %d fingers", (uint16_t)fingers);
|
||||
} else {
|
||||
NSLog(@"OK button tapped with no change");
|
||||
}
|
||||
|
||||
// Continue execution after the alert is dismissed
|
||||
if (!self->_mainFrameViewController.settingsExpandedInStreamView) {
|
||||
[self invokeOscLayout]; // Don't open osc layout tool immediately during streaming
|
||||
}
|
||||
|
||||
[self.onscreenControllerLabel setText:[LocalizationHelper localizedStringForKey: @"Tap %d Fingers to Change OSC Layout in Stream View", self->oscLayoutFingers]]; //update the osc label
|
||||
[self keyboardToggleFingerNumSliderMoved]; //update keyboard toggle number;
|
||||
}];
|
||||
|
||||
[alertController addAction:cancelAction];
|
||||
[alertController addAction:okAction];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (bool) isOnScreenControllerOrButtonEnabled{
|
||||
return ([self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == ABSOLUTE_TOUCH) && [self.onscreenControlSelector selectedSegmentIndex] != OnScreenControlsLevelOff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void)onscreenControlChanged{
|
||||
|
||||
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||||
if (isIPhone) {
|
||||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
|
||||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||||
}
|
||||
else {
|
||||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil];
|
||||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||||
self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
}
|
||||
|
||||
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
|
||||
// [self widget:self.keyboardToggleFingerNumSlider setEnabled:!customOscEnabled];
|
||||
[self widget:self.oscTapExlusionAreaSizeSlider setEnabled:customOscEnabled];
|
||||
if(customOscEnabled && !justEnteredSettingsViewDoNotOpenOscLayoutTool) {
|
||||
// [self.keyboardToggleFingerNumSlider setValue:3.0];
|
||||
// [self keyboardToggleFingerNumSliderMoved];
|
||||
[self keyboardToggleFingerNumSliderMoved]; // go to exclude 5 fingers
|
||||
[self.onscreenControllerLabel setText:[LocalizationHelper localizedStringForKey: @"Tap %d Fingers to Change OSC Layout in Stream View", self->oscLayoutFingers]];
|
||||
[self showCustomOSCTip];
|
||||
justEnteredSettingsViewDoNotOpenOscLayoutTool = false;
|
||||
//if (self.layoutOnScreenControlsVC.isBeingPresented == NO)
|
||||
}
|
||||
else{
|
||||
[self.onscreenControllerLabel setText:[LocalizationHelper localizedStringForKey: @"On-Screen Controls & Widgets"]];
|
||||
}
|
||||
justEnteredSettingsViewDoNotOpenOscLayoutTool = false;
|
||||
}
|
||||
|
||||
- (void)invokeOscLayout{
|
||||
[self presentViewController:self.layoutOnScreenControlsVC animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
- (void) pointerVelocityModeDividerSliderMoved {
|
||||
[self.pointerVelocityModeDividerUILabel setText:[LocalizationHelper localizedStringForKey:@"Touch Pointer Velocity: Scaled on %d%% of Right Screen", 100 - (uint8_t)self.pointerVelocityModeDividerSlider.value]];
|
||||
}
|
||||
|
||||
- (void) touchPointerVelocityFactorSliderMoved {
|
||||
[self.touchPointerVelocityFactorUILabel setText:[LocalizationHelper localizedStringForKey: @"Touch Pointer Velocity: %d%%", [self map_velocFactorDisplay_fromSliderValue: self.touchPointerVelocityFactorSlider.value]]]; // Update label display
|
||||
}
|
||||
|
||||
|
||||
// veloc factor upto 700%
|
||||
- (uint16_t) map_velocFactorDisplay_fromSliderValue:(CGFloat)sliderValue{
|
||||
uint16_t velocFactorDisplay = 0;
|
||||
if(sliderValue > 200) velocFactorDisplay = 200 + ((uint16_t)sliderValue % 200) * 5;
|
||||
else velocFactorDisplay = (uint16_t) sliderValue;
|
||||
return velocFactorDisplay;
|
||||
}
|
||||
|
||||
// veloc factor upto 700%
|
||||
|
||||
- (CGFloat) map_SliderValue_fromVelocFactor:(CGFloat)velocFactor{
|
||||
CGFloat sliderValue = 0.0f;
|
||||
if(velocFactor < 2.0f) sliderValue = velocFactor * 100;
|
||||
else sliderValue = (velocFactor - 2.0) * 100 / 5 + 200;
|
||||
return sliderValue;
|
||||
}
|
||||
|
||||
- (void) mousePointerVelocityFactorSliderMoved {
|
||||
[self.mousePointerVelocityFactorUILabel setText:[LocalizationHelper localizedStringForKey: @"Mouse Pointer Velocity: %d%%", [self map_velocFactorDisplay_fromSliderValue: self.mousePointerVelocityFactorSlider.value]]]; // Update label display
|
||||
}
|
||||
|
||||
- (uint32_t) getScreenEdgeFromSelector {
|
||||
switch (self.slideToSettingsScreenEdgeSelector.selectedSegmentIndex) {
|
||||
case 0: return UIRectEdgeLeft;
|
||||
case 1: return UIRectEdgeRight;
|
||||
case 2: return UIRectEdgeLeft|UIRectEdgeRight;
|
||||
default: return UIRectEdgeLeft;
|
||||
}
|
||||
}
|
||||
|
||||
- (uint32_t) getSelectorIndexFromScreenEdge: (uint32_t)edge {
|
||||
switch (edge) {
|
||||
case UIRectEdgeLeft: return 0;
|
||||
case UIRectEdgeRight: return 1;
|
||||
case UIRectEdgeLeft|UIRectEdgeRight: return 2;
|
||||
default: return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (void) widget:(UISlider*)widget setEnabled:(bool)enabled{
|
||||
[widget setEnabled:enabled];
|
||||
if(enabled){
|
||||
widget.alpha = 1.0;
|
||||
[widget setValue:widget.value + 0.0001]; // this is for low iOS version (like iOS14), only setting this minor value change is able to make widget visibility clear
|
||||
}
|
||||
else widget.alpha = 0.5; // this is for updating widget visibility on low iOS version like mini5 ios14
|
||||
}
|
||||
|
||||
- (void) touchModeChanged {
|
||||
// Disable on-screen controls in absolute touch mode
|
||||
[self.onscreenControlSelector setEnabled:[self.touchModeSelector selectedSegmentIndex] == 0];
|
||||
// Disable On-Screen Controls & Widgets in non-relative touch mode
|
||||
bool oscSelectorEnabled = [self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == ABSOLUTE_TOUCH;
|
||||
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
|
||||
bool isNativeTouch = [self.touchModeSelector selectedSegmentIndex] == PURE_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH;
|
||||
|
||||
[self.onscreenControlSelector setEnabled:oscSelectorEnabled];
|
||||
[self.largerStickLR1Selector setEnabled:oscSelectorEnabled]; // this selector stay aligned with oscSelector
|
||||
|
||||
[self widget:self.pointerVelocityModeDividerSlider setEnabled:isNativeTouch]; // pointer velocity scaling works only in native touch mode.
|
||||
[self widget:self.touchPointerVelocityFactorSlider setEnabled:isNativeTouch]; // pointer velocity scaling works only in native touch mode.
|
||||
[self widget:self.mousePointerVelocityFactorSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH]; // mouse velocity scaling works only in relative touch mode.
|
||||
[self widget:self.oscTapExlusionAreaSizeSlider setEnabled:customOscEnabled];
|
||||
|
||||
// number of touches required to toggle keyboard will be fixed to 3 when OSC is enabled.
|
||||
// [self widget:self.keyboardToggleFingerNumSlider setEnabled: !customOscEnabled]; // cancel OSC limitation for regular native touch,
|
||||
// when CustomOSC is enabled:
|
||||
if(customOscEnabled) {
|
||||
// [self.keyboardToggleFingerNumSlider setValue:3.0];
|
||||
// [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"To Toggle Local Keyboard: Tap %d Fingers", (uint16_t)self.keyboardToggleFingerNumSlider.value]];
|
||||
[self.onscreenControllerLabel setText:[LocalizationHelper localizedStringForKey: @"Tap %d Fingers to Change OSC Layout in Stream View", self->oscLayoutFingers]];
|
||||
[self keyboardToggleFingerNumSliderMoved]; // go exclude 5 fingers
|
||||
//if (self.layoutOnScreenControlsVC.isBeingPresented == NO)
|
||||
}
|
||||
else{
|
||||
[self.onscreenControllerLabel setText:[LocalizationHelper localizedStringForKey: @"On-Screen Controls & Widgets"]];
|
||||
}
|
||||
[self updateTouchModeLabel];
|
||||
}
|
||||
|
||||
- (void) updateBitrate {
|
||||
@@ -342,13 +845,14 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self updateResolutionDisplayViewText];
|
||||
_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
|
||||
}
|
||||
[self updateResolutionTable];
|
||||
}
|
||||
|
||||
- (void) promptCustomResolutionDialog {
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Enter Custom Resolution" message:nil preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey: @"Enter Custom Resolution"] message:nil preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = @"Video Width";
|
||||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"Video Width"];
|
||||
textField.clearButtonMode = UITextFieldViewModeAlways;
|
||||
textField.borderStyle = UITextBorderStyleRoundedRect;
|
||||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
||||
@@ -362,7 +866,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
}];
|
||||
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = @"Video Height";
|
||||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"Video Height"];
|
||||
textField.clearButtonMode = UITextFieldViewModeAlways;
|
||||
textField.borderStyle = UITextBorderStyleRoundedRect;
|
||||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
||||
@@ -375,7 +879,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
}
|
||||
}];
|
||||
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
NSArray * textfields = alertController.textFields;
|
||||
UITextField *widthField = textfields[0];
|
||||
UITextField *heightField = textfields[1];
|
||||
@@ -410,12 +914,12 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self updateResolutionDisplayViewText];
|
||||
self->_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
|
||||
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Custom Resolution Selected" message: @"Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.\n\nResolutions that are not supported by your client or host PC may cause streaming errors." preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Custom Resolution Selected"] message: [LocalizationHelper localizedStringForKey:@"Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.\n\nResolutions that are not supported by your client or host PC may cause streaming errors."] preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
}]];
|
||||
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
|
||||
// Restore the previous selection
|
||||
[self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
|
||||
}]];
|
||||
@@ -442,7 +946,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[subview removeFromSuperview];
|
||||
}
|
||||
UILabel *label1 = [[UILabel alloc] init];
|
||||
label1.text = @"Set PC/Game resolution: ";
|
||||
label1.text = [LocalizationHelper localizedStringForKey:@"Set PC/Game resolution:"];
|
||||
label1.font = [UIFont systemFontOfSize:fontSize];
|
||||
[label1 sizeToFit];
|
||||
label1.frame = CGRectMake(padding, (viewFrameHeight - label1.frame.size.height) / 2, label1.frame.size.width, label1.frame.size.height);
|
||||
@@ -456,6 +960,30 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self.resolutionDisplayView addSubview:label2];
|
||||
}
|
||||
|
||||
- (void) oscTapExlusionAreaSizeSliderMoved{
|
||||
[self.oscTapExlusionAreaSizeLabel setText:[LocalizationHelper localizedStringForKey:@"Tap Exclusion Area Size for Custom OSC: %d%%", (uint16_t)self.oscTapExlusionAreaSizeSlider.value]];
|
||||
}
|
||||
|
||||
- (void) keyboardToggleFingerNumSliderMoved{
|
||||
// bool oscEnabled = [self isOnScreenControllerOrButtonEnabled];
|
||||
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
|
||||
|
||||
CGFloat sliderValue = self.keyboardToggleFingerNumSlider.value;
|
||||
if(customOscEnabled){
|
||||
// exclude self->oscLayoutFingers when custom osc is enabled
|
||||
if(sliderValue > self->oscLayoutFingers - 1 && sliderValue < self->oscLayoutFingers) [self.keyboardToggleFingerNumSlider setValue: self->oscLayoutFingers - 1];
|
||||
if(sliderValue >= self->oscLayoutFingers && sliderValue < self->oscLayoutFingers + 1) [self.keyboardToggleFingerNumSlider setValue: self->oscLayoutFingers + 1];
|
||||
}
|
||||
|
||||
sliderValue = self.keyboardToggleFingerNumSlider.value;
|
||||
if(sliderValue > 10.5f) [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"Local Keyboard Toggle Disabled"]];
|
||||
else [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"To Toggle Local Keyboard: Tap %d Fingers", (uint16_t)sliderValue]]; // Initiate label display, exclude 5 fingers.
|
||||
}
|
||||
|
||||
- (void) slideToMenuDistanceSliderMoved{
|
||||
[self.slideToSettingsDistanceUILabel setText:[LocalizationHelper localizedStringForKey:@"Slide Distance for in-Stream Menu: %.2f * screen-width", self.slideToMenuDistanceSlider.value]];
|
||||
}
|
||||
|
||||
- (void) bitrateSliderMoved {
|
||||
assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
|
||||
_bitrate = bitrateTable[(int)self.bitrateSlider.value];
|
||||
@@ -464,7 +992,8 @@ BOOL isCustomResolution(CGSize res) {
|
||||
|
||||
- (void) updateBitrateText {
|
||||
// Display bitrate in Mbps
|
||||
[self.bitrateLabel setText:[NSString stringWithFormat:bitrateFormat, _bitrate / 1000.]];
|
||||
if(_bitrate / 1000. > 50.0) [self.bitrateLabel setText:[LocalizationHelper localizedStringForKey:@"Bitrate: %.1f Mbps - Use High Bitrates with Caution!", _bitrate / 1000.]];
|
||||
else [self.bitrateLabel setText:[LocalizationHelper localizedStringForKey:@"Bitrate: %.1f Mbps", _bitrate / 1000.]];
|
||||
}
|
||||
|
||||
- (NSInteger) getChosenFrameRate {
|
||||
@@ -528,32 +1057,79 @@ BOOL isCustomResolution(CGSize res) {
|
||||
NSInteger height = [self getChosenStreamHeight];
|
||||
NSInteger width = [self getChosenStreamWidth];
|
||||
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
|
||||
NSInteger motionMode = [self.motionModeSelector selectedSegmentIndex];
|
||||
NSInteger keyboardToggleFingers = (uint16_t)self.keyboardToggleFingerNumSlider.value;
|
||||
NSInteger oscLayoutToolFingers = (uint16_t)self->oscLayoutFingers;
|
||||
// NSLog(@"saveSettings keyboardToggleFingers %d", (uint16_t)keyboardToggleFingers);
|
||||
CGFloat slideToSettingsDistance = self.slideToMenuDistanceSlider.value;
|
||||
uint32_t slideToSettingsScreenEdge = [self getScreenEdgeFromSelector];
|
||||
CGFloat pointerVelocityModeDivider = (CGFloat)(uint8_t)self.pointerVelocityModeDividerSlider.value/100;
|
||||
CGFloat touchPointerVelocityFactor = (CGFloat)(uint16_t)[self map_velocFactorDisplay_fromSliderValue:self.touchPointerVelocityFactorSlider.value]/100;
|
||||
CGFloat mousePointerVelocityFactor = (CGFloat)(uint16_t)[self map_velocFactorDisplay_fromSliderValue:self.mousePointerVelocityFactorSlider.value]/100;
|
||||
|
||||
// CGFloat mousePointerVelocityFactor =(CGFloat)(uint16_t)self.mousePointerVelocityFactorSlider.value/100;
|
||||
CGFloat oscTapExlusionAreaSize =(CGFloat)(uint16_t)self.oscTapExlusionAreaSizeSlider.value/100;
|
||||
|
||||
BOOL reverseMouseWheelDirection = [self.reverseMouseWheelDirectionSelector selectedSegmentIndex] == 1;
|
||||
BOOL largerStickLR1 = [self.largerStickLR1Selector selectedSegmentIndex] == 1;
|
||||
BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1;
|
||||
BOOL showKeyboardToolbar = [self.showKeyboardToolbarSelector selectedSegmentIndex] == 1;
|
||||
BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
|
||||
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
|
||||
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
|
||||
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
|
||||
uint32_t preferredCodec = [self getChosenCodecPreference];
|
||||
BOOL enableYUV444 = [self.yuv444Selector selectedSegmentIndex] == 1;
|
||||
BOOL enablePIP = [self.pipSelector selectedSegmentIndex] == 1;
|
||||
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
|
||||
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
|
||||
BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
|
||||
BOOL statsOverlay = [self.statsOverlaySelector selectedSegmentIndex] == 1;
|
||||
// BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
|
||||
NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex];
|
||||
NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex];
|
||||
BOOL statsOverlayEnabled = statsOverlayLevel != 0;
|
||||
BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
|
||||
BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1;
|
||||
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
|
||||
NSInteger externalDisplayMode = [self.externalDisplayModeSelector selectedSegmentIndex];
|
||||
NSInteger localMousePointerMode = [self.localMousePointerModeSelector selectedSegmentIndex];
|
||||
NSLog(@"saving motionMode: %ld", motionMode);
|
||||
[dataMan saveSettingsWithBitrate:_bitrate
|
||||
framerate:framerate
|
||||
height:height
|
||||
width:width
|
||||
audioConfig:2 // Stereo
|
||||
onscreenControls:onscreenControls
|
||||
motionMode:motionMode
|
||||
keyboardToggleFingers:keyboardToggleFingers
|
||||
oscLayoutToolFingers:oscLayoutToolFingers
|
||||
slideToSettingsScreenEdge:slideToSettingsScreenEdge
|
||||
slideToSettingsDistance:slideToSettingsDistance
|
||||
pointerVelocityModeDivider:pointerVelocityModeDivider
|
||||
touchPointerVelocityFactor:touchPointerVelocityFactor
|
||||
mousePointerVelocityFactor:mousePointerVelocityFactor
|
||||
oscTapExlusionAreaSize:oscTapExlusionAreaSize
|
||||
reverseMouseWheelDirection:reverseMouseWheelDirection
|
||||
largerStickLR1:largerStickLR1
|
||||
liftStreamViewForKeyboard:liftStreamViewForKeyboard
|
||||
showKeyboardToolbar:showKeyboardToolbar
|
||||
optimizeGames:optimizeGames
|
||||
multiController:multiController
|
||||
swapABXYButtons:swapABXYButtons
|
||||
audioOnPC:audioOnPC
|
||||
preferredCodec:preferredCodec
|
||||
enableYUV444:enableYUV444
|
||||
enablePIP:enablePIP
|
||||
useFramePacing:useFramePacing
|
||||
enableHdr:enableHdr
|
||||
btMouseSupport:btMouseSupport
|
||||
absoluteTouchMode:absoluteTouchMode
|
||||
statsOverlay:statsOverlay];
|
||||
// absoluteTouchMode:absoluteTouchMode
|
||||
touchMode:touchMode
|
||||
statsOverlayLevel:statsOverlayLevel
|
||||
statsOverlayEnabled:statsOverlayEnabled
|
||||
unlockDisplayOrientation:unlockDisplayOrientation
|
||||
resolutionSelected:resolutionSelected
|
||||
externalDisplayMode:externalDisplayMode
|
||||
localMousePointerMode:localMousePointerMode];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
@@ -567,5 +1143,4 @@ BOOL isCustomResolution(CGSize res) {
|
||||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// StreamFrameViewController.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/18/14.
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Connection.h"
|
||||
#import "StreamConfiguration.h"
|
||||
#import "StreamView.h"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@import GameController;
|
||||
|
||||
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate>
|
||||
#else
|
||||
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate>
|
||||
#endif
|
||||
@property (nonatomic) StreamConfiguration* streamConfig;
|
||||
|
||||
-(void)updatePreferredDisplayMode:(BOOL)streamActive;
|
||||
|
||||
@end
|
||||
@@ -1,714 +0,0 @@
|
||||
//
|
||||
// StreamFrameViewController.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/18/14.
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamFrameViewController.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamManager.h"
|
||||
#import "ControllerSupport.h"
|
||||
#import "DataManager.h"
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <Limelight.h>
|
||||
|
||||
#if TARGET_OS_TV
|
||||
#import <AVFoundation/AVDisplayCriteria.h>
|
||||
#import <AVKit/AVDisplayManager.h>
|
||||
#import <AVKit/UIWindow.h>
|
||||
#endif
|
||||
|
||||
@interface AVDisplayCriteria()
|
||||
@property(readonly) int videoDynamicRange;
|
||||
@property(readonly, nonatomic) float refreshRate;
|
||||
- (id)initWithRefreshRate:(float)arg1 videoDynamicRange:(int)arg2;
|
||||
@end
|
||||
|
||||
@implementation StreamFrameViewController {
|
||||
ControllerSupport *_controllerSupport;
|
||||
StreamManager *_streamMan;
|
||||
TemporarySettings *_settings;
|
||||
NSTimer *_inactivityTimer;
|
||||
NSTimer *_statsUpdateTimer;
|
||||
UITapGestureRecognizer *_menuTapGestureRecognizer;
|
||||
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
|
||||
UITapGestureRecognizer *_playPauseTapGestureRecognizer;
|
||||
UITextView *_overlayView;
|
||||
UILabel *_stageLabel;
|
||||
UILabel *_tipLabel;
|
||||
UIActivityIndicatorView *_spinner;
|
||||
StreamView *_streamView;
|
||||
UIScrollView *_scrollView;
|
||||
BOOL _userIsInteracting;
|
||||
CGSize _keyboardSize;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
UIScreenEdgePanGestureRecognizer *_exitSwipeRecognizer;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
[[self revealViewController] setPrimaryViewController:self];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void)controllerPauseButtonPressed:(id)sender { }
|
||||
- (void)controllerPauseButtonDoublePressed:(id)sender {
|
||||
Log(LOG_I, @"Menu double-pressed -- backing out of stream");
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
- (void)controllerPlayPauseButtonPressed:(id)sender {
|
||||
Log(LOG_I, @"Play/Pause button pressed -- backing out of stream");
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
[self.navigationController setNavigationBarHidden:YES animated:YES];
|
||||
|
||||
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
||||
|
||||
_settings = [[[DataManager alloc] init] getSettings];
|
||||
|
||||
_stageLabel = [[UILabel alloc] init];
|
||||
[_stageLabel setUserInteractionEnabled:NO];
|
||||
[_stageLabel setText:[NSString stringWithFormat:@"Starting %@...", self.streamConfig.appName]];
|
||||
[_stageLabel sizeToFit];
|
||||
_stageLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_stageLabel.textColor = [UIColor whiteColor];
|
||||
_stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
|
||||
|
||||
_spinner = [[UIActivityIndicatorView alloc] init];
|
||||
[_spinner setUserInteractionEnabled:NO];
|
||||
#if TARGET_OS_TV
|
||||
[_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
||||
#else
|
||||
[_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhite];
|
||||
#endif
|
||||
[_spinner sizeToFit];
|
||||
[_spinner startAnimating];
|
||||
_spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - _stageLabel.frame.size.height - _spinner.frame.size.height);
|
||||
|
||||
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig delegate:self];
|
||||
_inactivityTimer = nil;
|
||||
|
||||
_streamView = [[StreamView alloc] initWithFrame:self.view.frame];
|
||||
[_streamView setupStreamView:_controllerSupport interactionDelegate:self config:self.streamConfig];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer) {
|
||||
_menuTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
|
||||
_menuTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
||||
|
||||
_playPauseTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPlayPauseButtonPressed:)];
|
||||
_playPauseTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypePlayPause)];
|
||||
|
||||
_menuDoubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonDoublePressed:)];
|
||||
_menuDoubleTapGestureRecognizer.numberOfTapsRequired = 2;
|
||||
[_menuTapGestureRecognizer requireGestureRecognizerToFail:_menuDoubleTapGestureRecognizer];
|
||||
_menuDoubleTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
||||
}
|
||||
|
||||
[self.view addGestureRecognizer:_menuTapGestureRecognizer];
|
||||
[self.view addGestureRecognizer:_menuDoubleTapGestureRecognizer];
|
||||
[self.view addGestureRecognizer:_playPauseTapGestureRecognizer];
|
||||
|
||||
#else
|
||||
_exitSwipeRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
|
||||
_exitSwipeRecognizer.edges = UIRectEdgeLeft;
|
||||
_exitSwipeRecognizer.delaysTouchesBegan = NO;
|
||||
_exitSwipeRecognizer.delaysTouchesEnded = NO;
|
||||
|
||||
[self.view addGestureRecognizer:_exitSwipeRecognizer];
|
||||
#endif
|
||||
|
||||
_tipLabel = [[UILabel alloc] init];
|
||||
[_tipLabel setUserInteractionEnabled:NO];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
[_tipLabel setText:@"Tip: Tap the Play/Pause button on the Apple TV Remote to disconnect from your PC"];
|
||||
#else
|
||||
[_tipLabel setText:@"Tip: Swipe from the left edge to disconnect from your PC"];
|
||||
#endif
|
||||
|
||||
[_tipLabel sizeToFit];
|
||||
_tipLabel.textColor = [UIColor whiteColor];
|
||||
_tipLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_tipLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height * 0.9);
|
||||
|
||||
_streamMan = [[StreamManager alloc] initWithConfig:self.streamConfig
|
||||
renderView:_streamView
|
||||
connectionCallbacks:self];
|
||||
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
||||
[opQueue addOperation:_streamMan];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationWillResignActive:)
|
||||
name:UIApplicationWillResignActiveNotification
|
||||
object:nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(applicationDidBecomeActive:)
|
||||
name: UIApplicationDidBecomeActiveNotification
|
||||
object: nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(applicationDidEnterBackground:)
|
||||
name: UIApplicationDidEnterBackgroundNotification
|
||||
object: nil];
|
||||
|
||||
#if 0
|
||||
// FIXME: This doesn't work reliably on iPad for some reason. Showing and hiding the keyboard
|
||||
// several times in a row will not correctly restore the state of the UIScrollView.
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(keyboardWillShow:)
|
||||
name: UIKeyboardWillShowNotification
|
||||
object: nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(keyboardWillHide:)
|
||||
name: UIKeyboardWillHideNotification
|
||||
object: nil];
|
||||
#endif
|
||||
|
||||
// Only enable scroll and zoom in absolute touch mode
|
||||
if (_settings.absoluteTouchMode) {
|
||||
_scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
|
||||
#if !TARGET_OS_TV
|
||||
[_scrollView.panGestureRecognizer setMinimumNumberOfTouches:2];
|
||||
#endif
|
||||
[_scrollView setShowsHorizontalScrollIndicator:NO];
|
||||
[_scrollView setShowsVerticalScrollIndicator:NO];
|
||||
[_scrollView setDelegate:self];
|
||||
[_scrollView setMaximumZoomScale:10.0f];
|
||||
|
||||
// Add StreamView inside a UIScrollView for absolute mode
|
||||
[_scrollView addSubview:_streamView];
|
||||
[self.view addSubview:_scrollView];
|
||||
}
|
||||
else {
|
||||
// Add StreamView directly in relative mode
|
||||
[self.view addSubview:_streamView];
|
||||
}
|
||||
|
||||
[self.view addSubview:_stageLabel];
|
||||
[self.view addSubview:_spinner];
|
||||
[self.view addSubview:_tipLabel];
|
||||
}
|
||||
|
||||
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
|
||||
return _streamView;
|
||||
}
|
||||
|
||||
- (void)willMoveToParentViewController:(UIViewController *)parent {
|
||||
// Only cleanup when we're being destroyed
|
||||
if (parent == nil) {
|
||||
[_controllerSupport cleanup];
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
[_streamMan stopStream];
|
||||
if (_inactivityTimer != nil) {
|
||||
[_inactivityTimer invalidate];
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
- (void)keyboardWillShow:(NSNotification *)notification {
|
||||
_keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
CGRect frame = self->_scrollView.frame;
|
||||
frame.size.height -= self->_keyboardSize.height;
|
||||
self->_scrollView.frame = frame;
|
||||
}];
|
||||
}
|
||||
|
||||
-(void)keyboardWillHide:(NSNotification *)notification {
|
||||
// NOTE: UIKeyboardFrameEndUserInfoKey returns a different keyboard size
|
||||
// than UIKeyboardFrameBeginUserInfoKey, so it's unsuitable for use here
|
||||
// to undo the changes made by keyboardWillShow.
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
CGRect frame = self->_scrollView.frame;
|
||||
frame.size.height += self->_keyboardSize.height;
|
||||
self->_scrollView.frame = frame;
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)updateStatsOverlay {
|
||||
NSString* overlayText = [self->_streamMan getStatsOverlayText];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateOverlayText:overlayText];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateOverlayText:(NSString*)text {
|
||||
if (_overlayView == nil) {
|
||||
_overlayView = [[UITextView alloc] init];
|
||||
#if !TARGET_OS_TV
|
||||
[_overlayView setEditable:NO];
|
||||
#endif
|
||||
[_overlayView setUserInteractionEnabled:NO];
|
||||
[_overlayView setSelectable:NO];
|
||||
[_overlayView setScrollEnabled:NO];
|
||||
|
||||
// HACK: If not using stats overlay, center the text
|
||||
if (_statsUpdateTimer == nil) {
|
||||
[_overlayView setTextAlignment:NSTextAlignmentCenter];
|
||||
}
|
||||
|
||||
[_overlayView setTextColor:[UIColor lightGrayColor]];
|
||||
[_overlayView setBackgroundColor:[UIColor blackColor]];
|
||||
#if TARGET_OS_TV
|
||||
[_overlayView setFont:[UIFont systemFontOfSize:24]];
|
||||
#else
|
||||
[_overlayView setFont:[UIFont systemFontOfSize:12]];
|
||||
#endif
|
||||
[_overlayView setAlpha:0.5];
|
||||
[self.view addSubview:_overlayView];
|
||||
}
|
||||
|
||||
if (text != nil) {
|
||||
// We set our bounds to the maximum width in order to work around a bug where
|
||||
// sizeToFit interacts badly with the UITextView's line breaks, causing the
|
||||
// width to get smaller and smaller each time as more line breaks are inserted.
|
||||
[_overlayView setBounds:CGRectMake(self.view.frame.origin.x,
|
||||
_overlayView.frame.origin.y,
|
||||
self.view.frame.size.width,
|
||||
_overlayView.frame.size.height)];
|
||||
[_overlayView setText:text];
|
||||
[_overlayView sizeToFit];
|
||||
[_overlayView setCenter:CGPointMake(self.view.frame.size.width / 2, _overlayView.frame.size.height / 2)];
|
||||
[_overlayView setHidden:NO];
|
||||
}
|
||||
else {
|
||||
[_overlayView setHidden:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) returnToMainFrame {
|
||||
// Reset display mode back to default
|
||||
[self updatePreferredDisplayMode:NO];
|
||||
|
||||
[_statsUpdateTimer invalidate];
|
||||
_statsUpdateTimer = nil;
|
||||
|
||||
[self.navigationController popToRootViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
// This will fire if the user opens control center or gets a low battery message
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
if (_inactivityTimer != nil) {
|
||||
[_inactivityTimer invalidate];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Terminate the stream if the app is inactive for 60 seconds
|
||||
Log(LOG_I, @"Starting inactivity termination timer");
|
||||
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:60
|
||||
target:self
|
||||
selector:@selector(inactiveTimerExpired:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)inactiveTimerExpired:(NSTimer*)timer {
|
||||
Log(LOG_I, @"Terminating stream after inactivity");
|
||||
|
||||
[self returnToMainFrame];
|
||||
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||
// Stop the background timer, since we're foregrounded again
|
||||
if (_inactivityTimer != nil) {
|
||||
Log(LOG_I, @"Stopping inactivity timer after becoming active again");
|
||||
[_inactivityTimer invalidate];
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
}
|
||||
|
||||
// This fires when the home button is pressed
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
Log(LOG_I, @"Terminating stream immediately for backgrounding");
|
||||
|
||||
if (_inactivityTimer != nil) {
|
||||
[_inactivityTimer invalidate];
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
|
||||
- (void)edgeSwiped {
|
||||
Log(LOG_I, @"User swiped to end stream");
|
||||
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
|
||||
- (void) connectionStarted {
|
||||
Log(LOG_I, @"Connection started");
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Leave the spinner spinning until it's obscured by
|
||||
// the first frame of video.
|
||||
self->_stageLabel.hidden = YES;
|
||||
self->_tipLabel.hidden = YES;
|
||||
|
||||
[self->_streamView showOnScreenControls];
|
||||
|
||||
[self->_controllerSupport connectionEstablished];
|
||||
|
||||
if (self->_settings.statsOverlay) {
|
||||
self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f
|
||||
target:self
|
||||
selector:@selector(updateStatsOverlay)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)connectionTerminated:(int)errorCode {
|
||||
Log(LOG_I, @"Connection terminated: %d", errorCode);
|
||||
|
||||
unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode);
|
||||
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
|
||||
NSString* title;
|
||||
NSString* message;
|
||||
|
||||
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
|
||||
title = @"Connection Error";
|
||||
message = @"Your device's network connection is blocking Moonlight. Streaming may not work while connected to this network.";
|
||||
}
|
||||
else {
|
||||
switch (errorCode) {
|
||||
case ML_ERROR_GRACEFUL_TERMINATION:
|
||||
[self returnToMainFrame];
|
||||
return;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_TRAFFIC:
|
||||
title = @"Connection Error";
|
||||
message = @"No video received from host.";
|
||||
if (portFlags != 0) {
|
||||
char failingPorts[256];
|
||||
LiStringifyPortFlags(portFlags, "\n", failingPorts, sizeof(failingPorts));
|
||||
message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
|
||||
}
|
||||
break;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_FRAME:
|
||||
title = @"Connection Error";
|
||||
message = @"Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.";
|
||||
break;
|
||||
|
||||
case ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
|
||||
case ML_ERROR_PROTECTED_CONTENT:
|
||||
title = @"Connection Error";
|
||||
message = @"Something went wrong on your host PC when starting the stream.\n\nMake sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.\n\nIf the issue persists, try reinstalling your GPU drivers and GeForce Experience.";
|
||||
break;
|
||||
|
||||
case ML_ERROR_FRAME_CONVERSION:
|
||||
title = @"Connection Error";
|
||||
message = @"The host PC reported a fatal video encoding error.\n\nTry disabling HDR mode, changing the streaming resolution, or changing your host PC's display resolution.";
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
NSString* errorString;
|
||||
if (abs(errorCode) > 1000) {
|
||||
// We'll assume large errors are hex values
|
||||
errorString = [NSString stringWithFormat:@"%08X", (uint32_t)errorCode];
|
||||
}
|
||||
else {
|
||||
// Smaller values will just be printed as decimal (probably errno.h values)
|
||||
errorString = [NSString stringWithFormat:@"%d", errorCode];
|
||||
}
|
||||
|
||||
title = @"Connection Terminated";
|
||||
message = [NSString stringWithFormat: @"The connection was terminated\n\nError code: %@", errorString];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:title
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:conTermAlert];
|
||||
[conTermAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[self returnToMainFrame];
|
||||
}]];
|
||||
[self presentViewController:conTermAlert animated:YES completion:nil];
|
||||
});
|
||||
|
||||
[_streamMan stopStream];
|
||||
}
|
||||
|
||||
- (void) stageStarting:(const char*)stageName {
|
||||
Log(LOG_I, @"Starting %s", stageName);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSString* lowerCase = [NSString stringWithFormat:@"%s in progress...", stageName];
|
||||
NSString* titleCase = [[[lowerCase substringToIndex:1] uppercaseString] stringByAppendingString:[lowerCase substringFromIndex:1]];
|
||||
[self->_stageLabel setText:titleCase];
|
||||
[self->_stageLabel sizeToFit];
|
||||
self->_stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self->_stageLabel.center.y);
|
||||
});
|
||||
}
|
||||
|
||||
- (void) stageComplete:(const char*)stageName {
|
||||
}
|
||||
|
||||
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags {
|
||||
Log(LOG_I, @"Stage %s failed: %d", stageName, errorCode);
|
||||
|
||||
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portTestFlags);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
|
||||
NSString* message = [NSString stringWithFormat:@"%s failed with error %d", stageName, errorCode];
|
||||
if (portTestFlags != 0) {
|
||||
char failingPorts[256];
|
||||
LiStringifyPortFlags(portTestFlags, "\n", failingPorts, sizeof(failingPorts));
|
||||
message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
|
||||
}
|
||||
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
|
||||
message = [message stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
|
||||
}
|
||||
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:alert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[self returnToMainFrame];
|
||||
}]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
});
|
||||
|
||||
[_streamMan stopStream];
|
||||
}
|
||||
|
||||
- (void) launchFailed:(NSString*)message {
|
||||
Log(LOG_I, @"Launch failed: %@", message);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Error"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:alert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[self returnToMainFrame];
|
||||
}]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor {
|
||||
Log(LOG_I, @"Rumble on gamepad %d: %04x %04x", controllerNumber, lowFreqMotor, highFreqMotor);
|
||||
|
||||
[_controllerSupport rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
|
||||
}
|
||||
|
||||
- (void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger {
|
||||
Log(LOG_I, @"Trigger rumble on gamepad %d: %04x %04x", controllerNumber, leftTrigger, rightTrigger);
|
||||
|
||||
[_controllerSupport rumbleTriggers:controllerNumber leftTrigger:leftTrigger rightTrigger:rightTrigger];
|
||||
}
|
||||
|
||||
- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz {
|
||||
Log(LOG_I, @"Set motion state on gamepad %d: %02x %u Hz", controllerNumber, motionType, reportRateHz);
|
||||
|
||||
[_controllerSupport setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
|
||||
}
|
||||
|
||||
- (void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
|
||||
Log(LOG_I, @"Set controller LED on gamepad %d: l%02x%02x%02x", controllerNumber, r, g, b);
|
||||
|
||||
[_controllerSupport setControllerLed:controllerNumber r:r g:g b:b];
|
||||
}
|
||||
|
||||
- (void)connectionStatusUpdate:(int)status {
|
||||
Log(LOG_W, @"Connection status update: %d", status);
|
||||
|
||||
// The stats overlay takes precedence over these warnings
|
||||
if (_statsUpdateTimer != nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
switch (status) {
|
||||
case CONN_STATUS_OKAY:
|
||||
[self updateOverlayText:nil];
|
||||
break;
|
||||
|
||||
case CONN_STATUS_POOR:
|
||||
if (self->_streamConfig.bitRate > 5000) {
|
||||
[self updateOverlayText:@"Slow connection to PC\nReduce your bitrate"];
|
||||
}
|
||||
else {
|
||||
[self updateOverlayText:@"Poor connection to PC"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void) updatePreferredDisplayMode:(BOOL)streamActive {
|
||||
#if TARGET_OS_TV
|
||||
if (@available(tvOS 11.2, *)) {
|
||||
UIWindow* window = [[[UIApplication sharedApplication] delegate] window];
|
||||
AVDisplayManager* displayManager = [window avDisplayManager];
|
||||
|
||||
// This logic comes from Kodi and MrMC
|
||||
if (streamActive) {
|
||||
int dynamicRange;
|
||||
|
||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
||||
dynamicRange = 2; // HDR10
|
||||
}
|
||||
else {
|
||||
dynamicRange = 0; // SDR
|
||||
}
|
||||
|
||||
AVDisplayCriteria* displayCriteria = [[AVDisplayCriteria alloc] initWithRefreshRate:[_settings.framerate floatValue]
|
||||
videoDynamicRange:dynamicRange];
|
||||
displayManager.preferredDisplayCriteria = displayCriteria;
|
||||
}
|
||||
else {
|
||||
// Switch back to the default display mode
|
||||
displayManager.preferredDisplayCriteria = nil;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) setHdrMode:(bool)enabled {
|
||||
Log(LOG_I, @"HDR is now: %s", enabled ? "active" : "inactive");
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updatePreferredDisplayMode:YES];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) videoContentShown {
|
||||
[_spinner stopAnimating];
|
||||
[self.view setBackgroundColor:[UIColor blackColor]];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning
|
||||
{
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
- (void)gamepadPresenceChanged {
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)mousePresenceChanged {
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 14.0, *)) {
|
||||
[self setNeedsUpdateOfPrefersPointerLocked];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) streamExitRequested {
|
||||
Log(LOG_I, @"Gamepad combo requested stream exit");
|
||||
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
|
||||
- (void)userInteractionBegan {
|
||||
// Disable hiding home bar when user is interacting.
|
||||
// iOS will force it to be shown anyway, but it will
|
||||
// also discard our edges deferring system gestures unless
|
||||
// we willingly give up home bar hiding preference.
|
||||
_userIsInteracting = YES;
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)userInteractionEnded {
|
||||
// Enable home bar hiding again if conditions allow
|
||||
_userIsInteracting = NO;
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Require a confirmation when streaming to activate a system gesture
|
||||
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
|
||||
return UIRectEdgeAll;
|
||||
}
|
||||
|
||||
- (BOOL)prefersHomeIndicatorAutoHidden {
|
||||
if ([_controllerSupport getConnectedGamepadCount] > 0 &&
|
||||
[_streamView getCurrentOscState] == OnScreenControlsLevelOff &&
|
||||
_userIsInteracting == NO) {
|
||||
// Autohide the home bar when a gamepad is connected
|
||||
// and the on-screen controls are disabled. We can't
|
||||
// do this all the time because any touch on the display
|
||||
// will cause the home indicator to reappear, and our
|
||||
// preferredScreenEdgesDeferringSystemGestures will also
|
||||
// be suppressed (leading to possible errant exits of the
|
||||
// stream).
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)prefersPointerLocked {
|
||||
// Pointer lock breaks the UIKit mouse APIs, which is a problem because
|
||||
// GCMouse is horribly broken on iOS 14.0 for certain mice. Only lock
|
||||
// the cursor if there is a GCMouse present.
|
||||
return [GCMouse mice].count > 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Moonlight</string>
|
||||
<string>VoidLink</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDESourceControlProjectFavoriteDictionaryKey</key>
|
||||
<false/>
|
||||
<key>IDESourceControlProjectIdentifier</key>
|
||||
<string>054A62F3-1197-45FB-A607-EA7922396E24</string>
|
||||
<key>IDESourceControlProjectName</key>
|
||||
<string>Limelight</string>
|
||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
||||
<dict>
|
||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
||||
<string>github.com:limelight-stream/limelight-common-c.git</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectPath</key>
|
||||
<string>Limelight.xcodeproj</string>
|
||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
||||
<dict>
|
||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
||||
<string>../..</string>
|
||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
||||
<string>../../limelight-common-c</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectURL</key>
|
||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
||||
<key>IDESourceControlProjectVersion</key>
|
||||
<integer>111</integer>
|
||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
||||
<key>IDESourceControlProjectWCConfigurations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>EB99D631D5149240E91F8D168F0F298D7A5BAA89</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>limelight-common-c</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>limelight-ios</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDESourceControlProjectFavoriteDictionaryKey</key>
|
||||
<false/>
|
||||
<key>IDESourceControlProjectIdentifier</key>
|
||||
<string>AD7C6582-F39A-441A-A7FD-9EBF143920BA</string>
|
||||
<key>IDESourceControlProjectName</key>
|
||||
<string>Moonlight</string>
|
||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
||||
<dict>
|
||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
||||
<string>github.com:limelight-stream/limelight-common-c.git</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectPath</key>
|
||||
<string>Moonlight.xcodeproj</string>
|
||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
||||
<dict>
|
||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
||||
<string>../..</string>
|
||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
||||
<string>../../limelight-common-c</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectURL</key>
|
||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
||||
<key>IDESourceControlProjectVersion</key>
|
||||
<integer>111</integer>
|
||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
||||
<key>IDESourceControlProjectWCConfigurations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>EB99D631D5149240E91F8D168F0F298D7A5BAA89</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>limelight-common-c</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>moonlight-ios</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDESourceControlProjectFavoriteDictionaryKey</key>
|
||||
<false/>
|
||||
<key>IDESourceControlProjectIdentifier</key>
|
||||
<string>6524503E-ED62-4E8E-8A67-B6DFB5A532FD</string>
|
||||
<key>IDESourceControlProjectName</key>
|
||||
<string>project</string>
|
||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
||||
<dict>
|
||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
||||
<string>github.com:limelight-stream/limelight-common-c.git</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectPath</key>
|
||||
<string>Moonlight.xcodeproj/project.xcworkspace</string>
|
||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
||||
<dict>
|
||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
||||
<string>../..</string>
|
||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
||||
<string>../..limelight-common-c/</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectURL</key>
|
||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
||||
<key>IDESourceControlProjectVersion</key>
|
||||
<integer>111</integer>
|
||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
||||
<key>IDESourceControlProjectWCConfigurations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>EB99D631D5149240E91F8D168F0F298D7A5BAA89</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>limelight-common-c</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>limelight-ios</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,24 +1,42 @@
|
||||
# Moonlight iOS/tvOS
|
||||
# VoidLink 已上架App Store.
|
||||
# VoidLink is available on App Store
|
||||
### [https://apps.apple.com/app/voidlink/id6747717070](https://apps.apple.com/cn/app/voidlink/id6747717070)
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-ios/branch/master)
|
||||
<br>
|
||||
|
||||
[Moonlight for iOS/tvOS](https://moonlight-stream.org) is an open source client for [Sunshine](https://github.com/LizardByte/Sunshine) and NVIDIA GameStream. Moonlight for iOS/tvOS allows you to stream your full collection of games and apps from your powerful desktop computer to your iOS device or Apple TV.
|
||||
# 代码 Coding
|
||||
- 代码提交在`Integration`分支。
|
||||
- For latest coding commits, go to branch `Integration`.
|
||||
|
||||
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [Android client](https://github.com/moonlight-stream/moonlight-android).
|
||||
<br>
|
||||
|
||||
Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed project information, setup guide, or troubleshooting steps.
|
||||
# 关于VoidLink. About VoidLink
|
||||
- 本项目最初基于开源项目 [moonlight-iOS] fork 而来。在此基础上,True砖家(True Zhuanjia)@ Bilibili 及其他社区开发者对项目进行了大量重构、重新设计与功能扩展,包括全新的用户界面和显著增强的功能特性。<br>我们对 moonlight-iOS 开发者的开创性工作表示衷心感谢。 <br><br>
|
||||
- VoidLink was originally forked from the open-source project [moonlight-iOS], but has since been extensively reworked, redesigned, and expanded by True砖家 (True Zhuanjia) @ Bilibili and other community developers. These contributions include a completely new user interface and significant enhancements to the application's functionality.<br>We gratefully acknowledge the foundational work of the moonlight-iOS developers.
|
||||
|
||||
[](https://apps.apple.com/us/app/moonlight-game-streaming/id1000551566)
|
||||
<br>
|
||||
|
||||
## Building
|
||||
* Install Xcode from the [App Store page](https://apps.apple.com/us/app/xcode/id497799835)
|
||||
* Run `git clone --recursive https://github.com/moonlight-stream/moonlight-ios.git`
|
||||
* If you've already clone the repo without `--recursive`, run `git submodule update --init --recursive`
|
||||
* Open Moonlight.xcodeproj in Xcode
|
||||
* To run on a real device, you will need to locally modify the signing options:
|
||||
* Click on "Moonlight" at the top of the left sidebar
|
||||
* Click on the "Signing & Capabilities" tab
|
||||
* Under "Targets", select "Moonlight" (for iOS/iPadOS) or "Moonlight TV" (for tvOS)
|
||||
* In the "Team" dropdown, select your name. If your name doesn't appear, you may need to sign into Xcode with your Apple account.
|
||||
* Change the "Bundle Identifier" to something different. You can add your name or some random letters to make it unique.
|
||||
* Now you can select your Apple device in the top bar as a target and click the Play button to run.
|
||||
# App Store 分发一次性收费声明.
|
||||
# Statement on One-Time App Store Distribution Fee.
|
||||
- VoidLink 的 App Store 安装费用用于覆盖通过 Apple 框架进行安全可靠应用分发的成本。同时也支持项目维护者进行持续的开发、维护,以及公众用户访问并下载官方签名版本。感谢您对项目的支持,帮助我们持续改进和优化。<br><br>
|
||||
- The App Store fee for VoidLink covers the cost of secure and trusted distribution using Apple's infrastructure. It supports ongoing development, maintenance, and access to the official signed build. Thank you for supporting the project and helping sustain ongoing improvements.
|
||||
|
||||
<br>
|
||||
|
||||
# 开发者B站号. Developer on Bilibili
|
||||
|
||||
如果你在用Bilibili, 请关注`True砖家`,了解该fork的最新消息。 <br>
|
||||
If you are on Bilibili, subscribe `True砖家` to get the latest news of this fork: <br>
|
||||
https://b23.tv/A0F9v7n
|
||||
|
||||
<br>
|
||||
|
||||
# 贡献者 Contributors
|
||||
[@TrueZhuangJia](https://github.com/TrueZhuangJia) <br>
|
||||
[@All contributors from moonlight-iOS](https://github.com/moonlight-stream/moonlight-ios/graphs/contributors) <br>
|
||||
[@stefanilijev97](https://github.com/stefanilijev97/stefanilijev97) <br>
|
||||
[@Acaki](https://github.com/Acaki) <br>
|
||||
[@seastwood](https://github.com/seastwood) <br>
|
||||
[@Danos0100](https://github.com/Danos0100) <br>
|
||||
[@xzzpig](https://github.com/xzzpig) <br>
|
||||
[@King0fSpace](https://github.com/King0fSpace) <br>
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:Moonlight.xcodeproj">
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"originHash" : "f17bf6f1131c7255a75ffd6a989b69573cb49c332e612dffb429905d8d7afe72",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "openssl-package",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/OpenSSL-Package.git",
|
||||
"state" : {
|
||||
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
|
||||
"version" : "3.3.2000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// AboutView.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 5/18/25.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
|
||||
public struct AboutView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
public var aboutVC:UIViewController
|
||||
|
||||
public var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
// App 图标
|
||||
Image(uiImage: UIImage(named: "AppIconMedium") ?? UIImage())
|
||||
// .resizable()
|
||||
.resizable(capInsets:EdgeInsets(top: 3.5, leading: 3.5, bottom: 3.5, trailing: 3.5))
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 28))
|
||||
|
||||
// App 名称
|
||||
Text(Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "App Name")
|
||||
.font(.title)
|
||||
.bold()
|
||||
|
||||
// 版本号
|
||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
|
||||
Text("Version \(version)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
// 说明文字
|
||||
Text(SwiftLocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
|
||||
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
|
||||
if #available(iOS 14.0, *) {
|
||||
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption2)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
.padding()
|
||||
} else {
|
||||
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
// 链接按钮
|
||||
if #available(iOS 14.0, *) {
|
||||
Link(SwiftLocalizationHelper.localizedString(forKey: "Join us"), destination: URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink"))!)
|
||||
.padding(.top, 10)
|
||||
Spacer()
|
||||
// OK 按钮
|
||||
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
|
||||
aboutVC.dismiss(animated:true)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.frame(height: 46)
|
||||
.cornerRadius(12)
|
||||
} else {
|
||||
HStack(spacing: 20) {
|
||||
Button(SwiftLocalizationHelper.localizedString(forKey: "Join us")) {
|
||||
// 打开链接
|
||||
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink")) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
.frame(height: 33)
|
||||
.frame(minWidth: 100)
|
||||
/*
|
||||
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
.frame(height: 33)
|
||||
.frame(minWidth: 100)*/
|
||||
Button(SwiftLocalizationHelper.localizedString(forKey: "OK")) {
|
||||
aboutVC.dismiss(animated:true)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
.frame(height: 33)
|
||||
}
|
||||
.padding(.top, 10)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// AboutViewController.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by Weimin on 2025/7/14.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
@objc class AboutViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let hostingVC = UIHostingController(rootView: AboutView( aboutVC: self))
|
||||
|
||||
addChild(hostingVC)
|
||||
hostingVC.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(hostingVC.view)
|
||||
hostingVC.didMove(toParent: self)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
hostingVC.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
hostingVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
hostingVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
hostingVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
|
||||
// 🎯 设置弹窗大小
|
||||
preferredContentSize = CGSize(width: 530, height: 450)
|
||||
|
||||
// ⚠️ 一定要设置以下样式才会生效 preferredContentSize
|
||||
modalPresentationStyle = .formSheet
|
||||
}
|
||||
}
|
||||
|
||||
func isIPhone() -> Bool {
|
||||
return UIDevice.current.userInterfaceIdiom == .phone
|
||||
}
|
||||
@@ -21,5 +21,6 @@
|
||||
- (void)saveContext;
|
||||
- (NSURL *)applicationDocumentsDirectory;
|
||||
- (NSURL*) getStoreURL;
|
||||
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0));
|
||||
|
||||
@end
|
||||
@@ -5,8 +5,13 @@
|
||||
// Created by Diego Waxemberg on 1/17/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.7.12
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
@@ -22,17 +27,64 @@ static NSString* DB_NAME = @"Moonlight_tvOS.bin";
|
||||
static NSString* DB_NAME = @"Limelight_iOS.sqlite";
|
||||
#endif
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
#pragma mark - UISceneSession lifecycle
|
||||
|
||||
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0)){
|
||||
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions API_AVAILABLE(ios(13.0)){
|
||||
}
|
||||
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
|
||||
if (shortcut != nil) {
|
||||
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
|
||||
|
||||
/*
|
||||
// orietation limitatioin test
|
||||
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
|
||||
UIViewController *topController = window.rootViewController;
|
||||
while (topController.presentedViewController) {
|
||||
topController = topController.presentedViewController;
|
||||
}
|
||||
#endif
|
||||
return [topController supportedInterfaceOrientations];
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
|
||||
NSLog(@"orientation limit");
|
||||
return UIInterfaceOrientationMaskLandscape;
|
||||
}
|
||||
*/
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
// Override point for command tool customization after application launch (works only when user default is nil)
|
||||
[CommandManager presetDefaultCommands];
|
||||
|
||||
// For iOS 12 and below, we need to manually create the window
|
||||
if (@available(iOS 13.0, *)) {
|
||||
// Scene delegate will handle window creation
|
||||
} else {
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
|
||||
NSString *storyboardName;
|
||||
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
||||
storyboardName = @"iPad";
|
||||
} else {
|
||||
storyboardName = @"iPhone";
|
||||
}
|
||||
|
||||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
|
||||
UIViewController *initialViewController = [storyboard instantiateInitialViewController];
|
||||
|
||||
self.window.rootViewController = initialViewController;
|
||||
[self.window makeKeyAndVisible];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler {
|
||||
_pcUuidToLoad = (NSString*)[shortcutItem.userInfo objectForKey:@"UUID"];
|
||||
_shortcutCompletionHandler = completionHandler;
|
||||
@@ -96,6 +96,7 @@ static NSData* p12 = nil;
|
||||
|
||||
bio = BIO_new(BIO_s_mem());
|
||||
i2d_X509_bio(bio, x509);
|
||||
X509_free(x509);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
@@ -222,6 +223,7 @@ static NSData* p12 = nil;
|
||||
+ (NSData *)getSignatureFromCert:(NSData *)cert {
|
||||
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
|
||||
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||
BIO_free(bio);
|
||||
|
||||
if (!x509) {
|
||||
Log(LOG_E, @"Unable to parse certificate in memory!");
|
||||
@@ -4,10 +4,12 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <OpenSSL/provider.h>
|
||||
#include <OpenSSL/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <OpenSSL/rand.h>
|
||||
|
||||
static const int NUM_BITS = 2048;
|
||||
static const int SERIAL = 0;
|
||||
@@ -61,22 +63,30 @@ void mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
|
||||
}
|
||||
|
||||
struct CertKeyPair generateCertKeyPair(void) {
|
||||
BIO *bio_err;
|
||||
X509 *x509 = NULL;
|
||||
EVP_PKEY *pkey = NULL;
|
||||
PKCS12 *p12 = NULL;
|
||||
|
||||
bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
|
||||
|
||||
mkcert(&x509, &pkey, NUM_BITS, SERIAL, NUM_YEARS);
|
||||
|
||||
p12 = PKCS12_create("limelight", "GameStream", pkey, x509, NULL, 0, 0, 0, 0, 0);
|
||||
|
||||
char* pass = "limelight";
|
||||
p12 = PKCS12_create(pass,
|
||||
"GameStream",
|
||||
pkey,
|
||||
x509,
|
||||
NULL,
|
||||
NID_pbe_WithSHA1And3_Key_TripleDES_CBC,
|
||||
-1, // disable certificate encryption
|
||||
2048,
|
||||
-1, // disable the automatic MAC
|
||||
0);
|
||||
// MAC it ourselves with SHA1 since iOS refuses to load anything else.
|
||||
PKCS12_set_mac(p12, pass, -1, NULL, 0, 1, EVP_sha1());
|
||||
|
||||
if (p12 == NULL) {
|
||||
printf("Error generating a valid PKCS12 certificate.\n");
|
||||
}
|
||||
|
||||
BIO_free(bio_err);
|
||||
|
||||
return (CertKeyPair){x509, pkey, p12};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef Limelight_mkcert_h
|
||||
#define Limelight_mkcert_h
|
||||
|
||||
#include <sys/_types/_time_t.h>
|
||||
#include <openssl/x509v3.h>
|
||||
#include <openssl/pkcs12.h>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// DataManager.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/28/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.1
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "TemporaryHost.h"
|
||||
#import "TemporaryApp.h"
|
||||
#import "TemporarySettings.h"
|
||||
|
||||
@interface DataManager : NSObject
|
||||
|
||||
typedef NS_ENUM(NSUInteger, UINavigationBarHeight) {
|
||||
UINavigationBarHeightIPad = 50,
|
||||
UINavigationBarHeightIPhone = 44
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, TouchMode) {
|
||||
RelativeTouch,
|
||||
NativeTouch,
|
||||
AbsoluteTouch,
|
||||
NativeTouchOnly
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, TouchEventPriorityEnum) {
|
||||
AsyncNativeTouchOff,
|
||||
EqualPriority,
|
||||
TouchDownPriority,// deprecated by GUI
|
||||
TouchMovePriority// deprecated by GUI
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, GyroMode) {
|
||||
GyroModeOff,
|
||||
GyroModeAuto,
|
||||
AlwaysDevice,
|
||||
AlwaysController
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
||||
FramePacingModeLegacy,
|
||||
FramePacingModeQueue
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
||||
AllSettings,
|
||||
FavoriteSettings,
|
||||
RemoveSettingItem,
|
||||
};
|
||||
|
||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
||||
framerate:(NSInteger)framerate
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
audioConfig:(NSInteger)audioConfig
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
gyroMode:(NSInteger)gyroMode
|
||||
emulatedControllerType:(NSInteger)emulatedControllerType
|
||||
keyboardToggleFingers:(NSInteger)keyboardToggleFingers
|
||||
oscLayoutToolFingers:(NSInteger)oscLayoutToolFingers
|
||||
slideToSettingsScreenEdge:(NSInteger)slideToSettingsScreenEdge
|
||||
slideToSettingsDistance:(CGFloat)slideToSettingsDistance
|
||||
pointerVelocityModeDivider:(CGFloat)pointerVelocityModeDivider
|
||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
||||
gyroSensitivity:(CGFloat)gyroSensitivity
|
||||
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
swapABXYButtons:(BOOL)swapABXYButtons
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
preferredCodec:(uint32_t)preferredCodec
|
||||
enableYUV444:(BOOL)enableYUV444
|
||||
enablePIP:(BOOL)enablePIP
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
touchMode:(NSInteger)touchMode
|
||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||
resolutionSelected:(NSInteger)resolutionSelected
|
||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
||||
localMousePointerMode:(NSInteger)localMousePointerMode
|
||||
frameQueueSize:(NSInteger)frameQueueSize
|
||||
enableGraphs:(BOOL)enableGraphs
|
||||
graphOpacity:(NSInteger)graphOpacity
|
||||
renderingBackend:(NSInteger)renderingBackend
|
||||
framePacingMode:(NSInteger)framePacingMode
|
||||
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
|
||||
|
||||
- (NSArray*) getHosts;
|
||||
- (void) updateHost:(TemporaryHost*)host;
|
||||
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
|
||||
- (void) removeHost:(TemporaryHost*)host;
|
||||
- (void) removeApp:(TemporaryApp*)app;
|
||||
- (Settings*) retrieveSettings;
|
||||
- (void) saveData;
|
||||
- (TemporarySettings*) getSettings;
|
||||
|
||||
- (void) updateUniqueId:(NSString*)uniqueId;
|
||||
- (NSString*) getUniqueId;
|
||||
|
||||
@end
|
||||
@@ -5,6 +5,9 @@
|
||||
// Created by Diego Waxemberg on 10/28/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.1
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DataManager.h"
|
||||
#import "TemporaryApp.h"
|
||||
@@ -58,17 +61,45 @@
|
||||
width:(NSInteger)width
|
||||
audioConfig:(NSInteger)audioConfig
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
gyroMode:(NSInteger)gyroMode
|
||||
emulatedControllerType:(NSInteger)emulatedControllerType
|
||||
keyboardToggleFingers:(NSInteger)keyboardToggleFingers
|
||||
oscLayoutToolFingers:(NSInteger)oscLayoutToolFingers
|
||||
slideToSettingsScreenEdge:(NSInteger)slideToSettingsScreenEdge
|
||||
slideToSettingsDistance:(CGFloat)slideToSettingsDistance
|
||||
pointerVelocityModeDivider:(CGFloat)pointerVelocityModeDivider
|
||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
||||
gyroSensitivity:(CGFloat)gyroSensitivity
|
||||
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
swapABXYButtons:(BOOL)swapABXYButtons
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
preferredCodec:(uint32_t)preferredCodec
|
||||
useFramePacing:(BOOL)useFramePacing
|
||||
enableYUV444:(BOOL)enableYUV444
|
||||
enablePIP:(BOOL)enablePIP
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
statsOverlay:(BOOL)statsOverlay {
|
||||
|
||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
touchMode:(NSInteger)touchMode
|
||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||
resolutionSelected:(NSInteger)resolutionSelected
|
||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
||||
localMousePointerMode:(NSInteger)localMousePointerMode
|
||||
frameQueueSize:(NSInteger)frameQueueSize
|
||||
enableGraphs:(BOOL)enableGraphs
|
||||
graphOpacity:(NSInteger)graphOpacity
|
||||
renderingBackend:(NSInteger)renderingBackend
|
||||
framePacingMode:(NSInteger)framePacingMode
|
||||
backgroundSessionTimer:(NSInteger)backgroundSessionTimer{
|
||||
|
||||
[_managedObjectContext performBlockAndWait:^{
|
||||
Settings* settingsToSave = [self retrieveSettings];
|
||||
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
||||
@@ -77,17 +108,45 @@
|
||||
settingsToSave.width = [NSNumber numberWithInteger:width];
|
||||
settingsToSave.audioConfig = [NSNumber numberWithInteger:audioConfig];
|
||||
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
|
||||
settingsToSave.gyroMode = [NSNumber numberWithInteger:gyroMode];
|
||||
settingsToSave.emulatedControllerType = [NSNumber numberWithInteger:emulatedControllerType];
|
||||
settingsToSave.keyboardToggleFingers = [NSNumber numberWithInteger:(uint16_t)keyboardToggleFingers];
|
||||
settingsToSave.oscLayoutToolFingers = [NSNumber numberWithInteger:(uint16_t)oscLayoutToolFingers];
|
||||
settingsToSave.slideToSettingsScreenEdge = [NSNumber numberWithInteger:(uint32_t)slideToSettingsScreenEdge];
|
||||
settingsToSave.slideToSettingsDistance = [NSNumber numberWithFloat:slideToSettingsDistance];
|
||||
settingsToSave.pointerVelocityModeDivider = [NSNumber numberWithFloat:pointerVelocityModeDivider];
|
||||
settingsToSave.touchPointerVelocityFactor = [NSNumber numberWithFloat:touchPointerVelocityFactor];
|
||||
settingsToSave.mousePointerVelocityFactor = [NSNumber numberWithFloat:mousePointerVelocityFactor];
|
||||
settingsToSave.gyroSensitivity = [NSNumber numberWithFloat:gyroSensitivity];
|
||||
settingsToSave.touchMoveEventInterval = [NSNumber numberWithInteger:touchMoveEventInterval];
|
||||
settingsToSave.reverseMouseWheelDirection = reverseMouseWheelDirection;
|
||||
settingsToSave.asyncNativeTouchPriority = [NSNumber numberWithInteger:asyncNativeTouchPriority];
|
||||
settingsToSave.liftStreamViewForKeyboard = liftStreamViewForKeyboard;
|
||||
settingsToSave.showKeyboardToolbar = showKeyboardToolbar;
|
||||
settingsToSave.optimizeGames = optimizeGames;
|
||||
settingsToSave.multiController = multiController;
|
||||
settingsToSave.swapABXYButtons = swapABXYButtons;
|
||||
settingsToSave.playAudioOnPC = audioOnPC;
|
||||
settingsToSave.preferredCodec = preferredCodec;
|
||||
settingsToSave.useFramePacing = useFramePacing;
|
||||
settingsToSave.enableYUV444 = enableYUV444;
|
||||
settingsToSave.enablePIP = enablePIP;
|
||||
settingsToSave.enableHdr = enableHdr;
|
||||
settingsToSave.btMouseSupport = btMouseSupport;
|
||||
settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
||||
settingsToSave.statsOverlay = statsOverlay;
|
||||
|
||||
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
||||
settingsToSave.touchMode = [NSNumber numberWithInteger:(uint16_t)touchMode];
|
||||
settingsToSave.statsOverlayEnabled = statsOverlayEnabled;
|
||||
settingsToSave.statsOverlayLevel = [NSNumber numberWithInteger:(uint16_t)statsOverlayLevel];
|
||||
settingsToSave.unlockDisplayOrientation = unlockDisplayOrientation;
|
||||
settingsToSave.resolutionSelected = [NSNumber numberWithInteger:resolutionSelected];
|
||||
settingsToSave.externalDisplayMode = [NSNumber numberWithInteger:externalDisplayMode];
|
||||
settingsToSave.localMousePointerMode = [NSNumber numberWithInteger:localMousePointerMode];
|
||||
settingsToSave.backroundSessionTimer = [NSNumber numberWithInteger:backgroundSessionTimer];
|
||||
|
||||
settingsToSave.frameQueueSize = [NSNumber numberWithInteger:frameQueueSize];
|
||||
settingsToSave.enableGraphs = enableGraphs;
|
||||
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
|
||||
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
|
||||
settingsToSave.framePacingMode = [NSNumber numberWithInteger:framePacingMode];
|
||||
[self saveData];
|
||||
}];
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
// Created by Cameron Gutman on 12/1/15.
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.8.3
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DataManager.h"
|
||||
#import "TemporaryHost.h"
|
||||
@@ -28,7 +31,7 @@
|
||||
self.externalAddress = host.externalAddress;
|
||||
self.localAddress = host.localAddress;
|
||||
self.ipv6Address = host.ipv6Address;
|
||||
self.mac = host.mac;
|
||||
if(!(host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"])) self.mac = host.mac; // try to fix invalid mac happens in some cases
|
||||
self.name = host.name;
|
||||
self.uuid = host.uuid;
|
||||
self.serverCodecModeSupport = host.serverCodecModeSupport;
|
||||
@@ -70,9 +73,9 @@
|
||||
if (self.ipv6Address != nil) {
|
||||
parentHost.ipv6Address = self.ipv6Address;
|
||||
}
|
||||
if (self.mac != nil) {
|
||||
parentHost.mac = self.mac;
|
||||
}
|
||||
// try to fix invalid mac happens in some cases
|
||||
// 添加主机crash问题重点关注
|
||||
if (!(self.mac == nil || [self.mac isEqualToString:@"00:00:00:00:00:00"])) parentHost.mac = self.mac;
|
||||
if (self.serverCert != nil) {
|
||||
parentHost.serverCert = self.serverCert;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// TemporarySettings.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 12/1/15.
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.1
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Settings+CoreDataClass.h"
|
||||
#define OSC_TOOL_FINGERS 4
|
||||
|
||||
|
||||
@interface TemporarySettings : NSObject
|
||||
|
||||
@property (nonatomic, retain) Settings * parent;
|
||||
|
||||
@property (nonatomic, retain) NSNumber * settingsMenuMode;
|
||||
@property (nonatomic, retain) NSNumber * settingsMenuWidth;
|
||||
@property (nonatomic, retain) NSNumber * bitrate;
|
||||
@property (nonatomic, retain) NSNumber * framerate;
|
||||
@property (nonatomic, retain) NSNumber * height;
|
||||
@property (nonatomic, retain) NSNumber * width;
|
||||
@property (nonatomic, retain) NSNumber * audioConfig;
|
||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
||||
@property (nonatomic, retain) NSNumber * gyroMode;
|
||||
@property (nonatomic, retain) NSNumber * keyboardToggleFingers;
|
||||
@property (nonatomic, retain) NSNumber * oscLayoutToolFingers;
|
||||
@property (nonatomic, retain) NSNumber * slideToSettingsScreenEdge;
|
||||
@property (nonatomic, retain) NSNumber * slideToSettingsDistance;
|
||||
@property (nonatomic, retain) NSNumber * touchMoveEventInterval;
|
||||
@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor;
|
||||
@property (nonatomic, retain) NSNumber * gyroSensitivity;
|
||||
@property (nonatomic, retain) NSNumber * emulatedControllerType;
|
||||
@property (nonatomic, retain) NSNumber * mousePointerVelocityFactor;
|
||||
@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider;
|
||||
@property (nonatomic, retain) NSString * uniqueId;
|
||||
@property (nonatomic, retain) NSNumber * resolutionSelected;
|
||||
@property (nonatomic, retain) NSNumber * externalDisplayMode;
|
||||
@property (nonatomic, retain) NSNumber * localMousePointerMode;
|
||||
@property (nonatomic, retain) NSNumber * backgroundSessionTimer;
|
||||
@property (nonatomic) enum {
|
||||
CODEC_PREF_AUTO,
|
||||
CODEC_PREF_H264,
|
||||
CODEC_PREF_HEVC,
|
||||
CODEC_PREF_AV1,
|
||||
} preferredCodec;
|
||||
@property (nonatomic) BOOL enableYUV444;
|
||||
@property (nonatomic) BOOL enablePIP;
|
||||
@property (nonatomic) BOOL reverseMouseWheelDirection;
|
||||
@property (nonatomic, retain) NSNumber * asyncNativeTouchPriority;
|
||||
@property (nonatomic) BOOL multiController;
|
||||
@property (nonatomic) BOOL swapABXYButtons;
|
||||
@property (nonatomic) BOOL playAudioOnPC;
|
||||
@property (nonatomic) BOOL optimizeGames;
|
||||
@property (nonatomic) BOOL enableHdr;
|
||||
@property (nonatomic) BOOL btMouseSupport;
|
||||
// @property (nonatomic) BOOL absoluteTouchMode;
|
||||
@property (nonatomic, retain) NSNumber * touchMode;
|
||||
@property (nonatomic, retain) NSNumber * statsOverlayLevel;
|
||||
@property (nonatomic) BOOL statsOverlayEnabled;
|
||||
@property (nonatomic) BOOL liftStreamViewForKeyboard;
|
||||
@property (nonatomic) BOOL showKeyboardToolbar;
|
||||
@property (nonatomic) BOOL unlockDisplayOrientation;
|
||||
@property (nonatomic) BOOL enableGraphs;
|
||||
@property (nonatomic, retain) NSNumber * frameQueueSize;
|
||||
@property (nonatomic, retain) NSNumber * graphOpacity;
|
||||
@property (nonatomic, retain) NSNumber * renderingBackend;
|
||||
@property (nonatomic, retain) NSNumber * framePacingMode;
|
||||
|
||||
- (id) initFromSettings:(Settings*)settings;
|
||||
|
||||
@end
|
||||
@@ -5,6 +5,9 @@
|
||||
// Created by Cameron Gutman on 12/1/15.
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.1
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TemporarySettings.h"
|
||||
#import "OnScreenControls.h"
|
||||
@@ -32,12 +35,14 @@
|
||||
|
||||
self.bitrate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"]];
|
||||
assert([self.bitrate intValue] != 0);
|
||||
self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
|
||||
assert([self.framerate intValue] != 0);
|
||||
self.framerate = [NSNumber numberWithDouble:[[NSUserDefaults standardUserDefaults] doubleForKey:@"framerate"]];
|
||||
assert([self.framerate doubleValue] != 0.0);
|
||||
self.audioConfig = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"audioConfig"]];
|
||||
assert([self.audioConfig intValue] != 0);
|
||||
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
|
||||
self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0;
|
||||
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
|
||||
self.enablePIP = [[NSUserDefaults standardUserDefaults] boolForKey:@"enablePIP"];
|
||||
self.frameQueueSize = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"frameQueueSize"]];
|
||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
||||
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
|
||||
@@ -45,7 +50,19 @@
|
||||
self.swapABXYButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"swapABXYButtons"];
|
||||
self.btMouseSupport = [[NSUserDefaults standardUserDefaults] boolForKey:@"btMouseSupport"];
|
||||
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
||||
|
||||
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
|
||||
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
|
||||
// Set rendering backend based on iOS version and user preference
|
||||
NSInteger savedBackend = [[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"];
|
||||
if (@available(iOS 17.0, *)) {
|
||||
// iOS 17+ can use Performance mode (Metal renderer)
|
||||
self.renderingBackend = [NSNumber numberWithInteger:savedBackend];
|
||||
} else {
|
||||
// iOS < 17 must use Balanced mode (AVSB renderer)
|
||||
self.renderingBackend = [NSNumber numberWithInteger:RENDER_AVSB];
|
||||
}
|
||||
self.framePacingMode = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framePacingMode"]];
|
||||
|
||||
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
||||
switch (_screenSize) {
|
||||
case 0:
|
||||
@@ -69,22 +86,52 @@
|
||||
}
|
||||
self.onscreenControls = [NSNumber numberWithInteger:OnScreenControlsLevelOff];
|
||||
#else
|
||||
self.settingsMenuMode = settings.settingsMenuMode;
|
||||
self.settingsMenuWidth = settings.settingsMenuWidth;
|
||||
self.bitrate = settings.bitrate;
|
||||
self.framerate = settings.framerate;
|
||||
self.height = settings.height;
|
||||
self.width = settings.width;
|
||||
self.audioConfig = settings.audioConfig;
|
||||
self.preferredCodec = settings.preferredCodec;
|
||||
self.useFramePacing = settings.useFramePacing;
|
||||
self.enableYUV444 = settings.enableYUV444;
|
||||
self.enablePIP = settings.enablePIP;
|
||||
self.frameQueueSize = settings.frameQueueSize;
|
||||
self.playAudioOnPC = settings.playAudioOnPC;
|
||||
self.enableHdr = settings.enableHdr;
|
||||
self.optimizeGames = settings.optimizeGames;
|
||||
self.multiController = settings.multiController;
|
||||
self.swapABXYButtons = settings.swapABXYButtons;
|
||||
self.onscreenControls = settings.onscreenControls;
|
||||
self.gyroMode = settings.gyroMode;
|
||||
self.emulatedControllerType = settings.emulatedControllerType;
|
||||
self.reverseMouseWheelDirection = settings.reverseMouseWheelDirection;
|
||||
self.asyncNativeTouchPriority = settings.asyncNativeTouchPriority;
|
||||
self.btMouseSupport = settings.btMouseSupport;
|
||||
self.absoluteTouchMode = settings.absoluteTouchMode;
|
||||
self.statsOverlay = settings.statsOverlay;
|
||||
// self.absoluteTouchMode = settings.absoluteTouchMode;
|
||||
self.touchMode = settings.touchMode;
|
||||
self.statsOverlayLevel = settings.statsOverlayLevel;
|
||||
self.statsOverlayEnabled = settings.statsOverlayEnabled;
|
||||
self.keyboardToggleFingers = settings.keyboardToggleFingers;
|
||||
self.oscLayoutToolFingers = settings.oscLayoutToolFingers;
|
||||
self.slideToSettingsScreenEdge = settings.slideToSettingsScreenEdge;
|
||||
self.slideToSettingsDistance = settings.slideToSettingsDistance;
|
||||
self.liftStreamViewForKeyboard = settings.liftStreamViewForKeyboard;
|
||||
self.showKeyboardToolbar = settings.showKeyboardToolbar;
|
||||
self.touchMoveEventInterval = settings.touchMoveEventInterval;
|
||||
self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor;
|
||||
self.mousePointerVelocityFactor = settings.mousePointerVelocityFactor;
|
||||
self.gyroSensitivity = settings.gyroSensitivity;
|
||||
self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider;
|
||||
self.unlockDisplayOrientation = settings.unlockDisplayOrientation;
|
||||
self.resolutionSelected = settings.resolutionSelected;
|
||||
self.externalDisplayMode = settings.externalDisplayMode;
|
||||
self.localMousePointerMode = settings.localMousePointerMode;
|
||||
self.enableGraphs = settings.enableGraphs;
|
||||
self.graphOpacity = settings.graphOpacity;
|
||||
self.renderingBackend = settings.renderingBackend;
|
||||
self.framePacingMode = settings.framePacingMode;
|
||||
self.backgroundSessionTimer = settings.backroundSessionTimer;
|
||||
#endif
|
||||
self.uniqueId = settings.uniqueId;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// FixedTintImageView.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025.6.1.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
// MenuSectionView.m
|
||||
|
||||
@interface FixedTintImageView : UIImageView
|
||||
@end
|
||||
|
||||
@interface FixedTintButton : UIButton
|
||||
@end
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// FixedTintImageView.m
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/6/1.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FixedTintImageView.h"
|
||||
|
||||
|
||||
@interface FixedTintButton()
|
||||
@property (nonatomic, strong) UIColor *fixedTintColor;
|
||||
@end
|
||||
|
||||
@implementation FixedTintButton
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
[super tintColorDidChange];
|
||||
// 强制 tint 保持固定颜色
|
||||
if (self.fixedTintColor) {
|
||||
[self setImage:[self.imageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal];
|
||||
self.tintColor = self.fixedTintColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTintColor:(UIColor *)tintColor {
|
||||
_fixedTintColor = tintColor;
|
||||
[super setTintColor:tintColor];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@interface FixedTintImageView ()
|
||||
@property (nonatomic, strong) UIColor *fixedTintColor;
|
||||
@end
|
||||
|
||||
@implementation FixedTintImageView
|
||||
|
||||
- (instancetype)initWithImage:(UIImage *)image {
|
||||
self = [super initWithImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
|
||||
if (self) {
|
||||
// _fixedTintColor = [UIColor systemBlueColor]; // 默认颜色
|
||||
if(_fixedTintColor != nil) [super setTintColor:_fixedTintColor];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setTintColor:(UIColor *)tintColor {
|
||||
if(tintColor != nil){
|
||||
_fixedTintColor = tintColor;
|
||||
[super setTintColor:tintColor];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tintColorDidChange {
|
||||
// 保持你固定的颜色,忽略系统的暗淡变更
|
||||
if(_fixedTintColor != nil) [super setTintColor:_fixedTintColor];
|
||||
}
|
||||
|
||||
@end
|
||||