Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e340b6d6a | ||
|
|
ec12492ea7 | ||
|
|
13ccbb3866 | ||
|
|
20d6e221a4 | ||
|
|
2bfddf255a | ||
|
|
0e8903d233 | ||
|
|
78f385d732 | ||
|
|
bd306ba07f | ||
|
|
ececc03d46 | ||
|
|
6202a53153 | ||
|
|
84b6b22f75 | ||
|
|
f8388658d8 | ||
|
|
3c36964b5c | ||
|
|
089515443d | ||
|
|
e63e5f2120 | ||
|
|
5a5238afee | ||
|
|
e00e2f8000 | ||
|
|
8795a9f173 | ||
|
|
5bd938abc5 | ||
|
|
514dbb98e7 | ||
|
|
106b1548d4 | ||
|
|
cacf2b1951 | ||
|
|
f1a4a519c3 | ||
|
|
4f0be34947 | ||
|
|
9da1121cdb | ||
|
|
0e92af13eb | ||
|
|
cdd613cc63 | ||
|
|
58535c9997 | ||
|
|
48647d4d89 | ||
|
|
16c54ac966 | ||
|
|
4214afd2b7 | ||
|
|
d28aa43319 | ||
|
|
4983aecb4c | ||
|
|
5ba566ceee | ||
|
|
b0fd0222df | ||
|
|
7b7bb3837c | ||
|
|
6879bf6379 | ||
|
|
fc2b1689d0 | ||
|
|
9c9ef4a13f | ||
|
|
a92010f5b1 | ||
|
|
813005694a | ||
|
|
11dd542cb3 | ||
|
|
949a0e0bfb | ||
|
|
ed8a246bad | ||
|
|
cfb8946ac3 | ||
|
|
660a015f8b | ||
|
|
54ac7f6522 | ||
|
|
a01af9ee21 | ||
|
|
71b7634069 | ||
|
|
32718071e0 | ||
|
|
30d602af29 | ||
|
|
f0043a763b | ||
|
|
516648cf34 | ||
|
|
5bd9c6bd98 | ||
|
|
3d82009d4a | ||
|
|
51c13faa0c | ||
|
|
e0aff4b532 | ||
|
|
b0c16922a2 | ||
|
|
ef5d839f5e | ||
|
|
4a3ca7774e | ||
|
|
8f93cce257 | ||
|
|
9a634da3ed | ||
|
|
6460036661 | ||
|
|
c968a1df2a | ||
|
|
54c106e01e | ||
|
|
41a5855211 | ||
|
|
2652c2a99b | ||
|
|
e987965765 | ||
|
|
a7d7a7975f | ||
|
|
c79929f37e | ||
|
|
d286c8b073 | ||
|
|
2cef75fb32 | ||
|
|
ca17c1cc66 | ||
|
|
98e8305c94 | ||
|
|
45ccfa0c7f | ||
|
|
7d7b909c66 | ||
|
|
d6c1059e99 | ||
|
|
9c43e39ffb | ||
|
|
adb5091dc2 | ||
|
|
7d3c2d93ac | ||
|
|
f4b161ed06 | ||
|
|
d57334265b | ||
|
|
f7efa48ae8 | ||
|
|
deb335efc3 | ||
|
|
f039f5af29 | ||
|
|
286b5e94e6 | ||
|
|
1a719e11e6 | ||
|
|
becfc596c1 | ||
|
|
1b76d0e0a6 | ||
|
|
53209ce89e | ||
|
|
384724ff27 | ||
|
|
e2eccd7968 | ||
|
|
f762ecc3b2 | ||
|
|
fa0b864933 | ||
|
|
e0f3921a75 | ||
|
|
f698ad2fb6 | ||
|
|
a0179cc6cc | ||
|
|
511e7c0e4b | ||
|
|
e734973be1 | ||
|
|
9979408b1a | ||
|
|
a5388bb60d | ||
|
|
4c32ec59d6 | ||
|
|
af1f0e75c7 | ||
|
|
3aa7a90a2f | ||
|
|
ec956218fd | ||
|
|
02fcdcecea | ||
|
|
3575e84165 | ||
|
|
90116e7ff6 | ||
|
|
ffe48a5382 | ||
|
|
c4ddc0c4fc | ||
|
|
23299ad73d | ||
|
|
accd07f808 | ||
|
|
ed39f85508 | ||
|
|
e8349b2a6f | ||
|
|
c36051fffc | ||
|
|
5f4fe6a040 | ||
|
|
843570dbca | ||
|
|
bb44b2067e | ||
|
|
2c2088e4b1 | ||
|
|
a7088c70e5 | ||
|
|
091fd828ab | ||
|
|
d168ffbd5a | ||
|
|
0af4c70e16 | ||
|
|
e402c17fa6 | ||
|
|
ab9b2509ff | ||
|
|
535d6009ad | ||
|
|
09981df37e | ||
|
|
f7eae4cee9 | ||
|
|
ab14e18b4a | ||
|
|
1630f7987a | ||
|
|
5a9bcab368 | ||
|
|
f7b118b671 | ||
|
|
b49f4a6891 | ||
|
|
3fd0c1b397 | ||
|
|
9282c3f404 | ||
|
|
ef3fca754f | ||
|
|
c08bb232cd | ||
|
|
2d1a1573e1 | ||
|
|
92b65b4a17 | ||
|
|
f72e0f6292 | ||
|
|
57a8c22a3f | ||
|
|
4195f97d56 | ||
|
|
c734030e87 | ||
|
|
96d997d41e | ||
|
|
023cc0b583 | ||
|
|
57dac128e2 | ||
|
|
77e0048c57 | ||
|
|
fe6954cb9c | ||
|
|
237f03a501 | ||
|
|
94f61ebf0c | ||
|
|
c3a0004c68 | ||
|
|
d7aa5cc9fc | ||
|
|
26c1d068a8 | ||
|
|
35ceebfe0d | ||
|
|
5daf158908 | ||
|
|
8f12a4f598 | ||
|
|
233a23c6c8 | ||
|
|
cf48d4d7c3 | ||
|
|
5bf8eeb873 | ||
|
|
8f5d58a25c | ||
|
|
363b91b244 | ||
|
|
efc34ae1fc | ||
|
|
9e858520c8 | ||
|
|
20aba200b3 | ||
|
|
cb235d9255 | ||
|
|
b0224d502a | ||
|
|
ce67aefc3b | ||
|
|
e8d864ff66 | ||
|
|
9e2ef6cc0d | ||
|
|
9d2df484d1 | ||
|
|
d28573dcd4 | ||
|
|
c3cb31d443 | ||
|
|
b258c58d59 | ||
|
|
d8e83d1704 | ||
|
|
e78a351c28 | ||
|
|
5ee443337c | ||
|
|
4dc96ba189 | ||
|
|
ecacb1afa7 | ||
|
|
7e21292a62 | ||
|
|
9104a7b7b9 | ||
|
|
ecc7a79a8c | ||
|
|
a1a70fcca2 | ||
|
|
d769c98e22 | ||
|
|
ce59a9eb05 | ||
|
|
b2984e0af5 | ||
|
|
bed2232aff | ||
|
|
e87f0f1aa0 | ||
|
|
9807a77d9b | ||
|
|
3e1423ff46 | ||
|
|
a4f805db26 | ||
|
|
0195aeb765 | ||
|
|
fcfe83572f | ||
|
|
d61942caec | ||
|
|
515360166c | ||
|
|
5c5507b02f | ||
|
|
7e0d2af5b7 | ||
|
|
780a0e7ab8 | ||
|
|
9359333b15 | ||
|
|
819aa436e8 | ||
|
|
5beee0ffe4 | ||
|
|
30aa9a6b8d | ||
|
|
7e088acd49 | ||
|
|
f702f726b7 | ||
|
|
46834a8a8b | ||
|
|
ca69fd62d9 | ||
|
|
8fd43d47dd | ||
|
|
732ec9a9cd | ||
|
|
a0d1982a14 | ||
|
|
bb63183a51 | ||
|
|
707beddcae | ||
|
|
c4c896e43e | ||
|
|
e2acf5ac4b | ||
|
|
fcecae3a87 | ||
|
|
55a2bb7576 | ||
|
|
6b085982f1 | ||
|
|
94f1c625be | ||
|
|
b0842ea4dc | ||
|
|
49431b5eba | ||
|
|
68ee5d879b | ||
|
|
984a1acd48 | ||
|
|
00b06b7b70 | ||
|
|
84adf3ac95 | ||
|
|
9cd72271ca | ||
|
|
e71ce58d9b | ||
|
|
eb03b055ea | ||
|
|
5af39e8fb6 | ||
|
|
b12cb96416 | ||
|
|
7f4108c4e3 | ||
|
|
be48a48704 | ||
|
|
88bc5c7551 | ||
|
|
7cd857a38b | ||
|
|
60e92c458e | ||
|
|
605f276a71 | ||
|
|
210ca4190a | ||
|
|
bd2d9c05bc | ||
|
|
ae526ecddf | ||
|
|
37d7949304 | ||
|
|
6092839004 | ||
|
|
31dc13b87d | ||
|
|
dbe91f5b07 | ||
|
|
d092b93a29 | ||
|
|
eff35450f6 | ||
|
|
6204feefc0 | ||
|
|
924374bccd | ||
|
|
eac8d15240 | ||
|
|
a55d840bce | ||
|
|
22aa0a5249 | ||
|
|
c3358efa9d | ||
|
|
e59f3ba1ab | ||
|
|
42f29074fa | ||
|
|
6299401286 | ||
|
|
deba65857d | ||
|
|
691aa9c899 | ||
|
|
bd9dccd834 | ||
|
|
0dd419a5a3 | ||
|
|
2410222c19 | ||
|
|
1269c46f56 | ||
|
|
2c787a8d77 | ||
|
|
92fca9bca1 | ||
|
|
f8badd51c5 | ||
|
|
ed8bf5af9e | ||
|
|
b2521de84e | ||
|
|
4dd1aad123 | ||
|
|
165b5e6638 | ||
|
|
d8ac06ddca | ||
|
|
e742466409 | ||
|
|
201bf5b447 | ||
|
|
7bed913a1e | ||
|
|
e067fd8fa2 | ||
|
|
ba7d5feb81 | ||
|
|
4a9e7ad2a1 | ||
|
|
4b56a44a47 | ||
|
|
bc98b43de2 | ||
|
|
16b3df5279 | ||
|
|
d202f2f6e3 | ||
|
|
142474eac8 | ||
|
|
acc89dba39 | ||
|
|
8cd366d2cd | ||
|
|
2d67c1b22b | ||
|
|
77b0ef55c9 | ||
|
|
d76f404a4c | ||
|
|
d0b45a019f | ||
|
|
0728e999eb | ||
|
|
f325e9a206 | ||
|
|
49b03e65b2 | ||
|
|
2bc18e2f02 | ||
|
|
dadb292676 | ||
|
|
2c9902ce77 | ||
|
|
6d36219e88 | ||
|
|
470862dbe0 | ||
|
|
f6017824ee | ||
|
|
adeb77cf65 | ||
|
|
9bc080815a | ||
|
|
d03eebb5ba | ||
|
|
ca453d8059 | ||
|
|
cb8126d201 | ||
|
|
1626f30bb6 | ||
|
|
47b5ec2678 | ||
|
|
ccd032834d | ||
|
|
c7ffd51cec | ||
|
|
38f8c50d7f | ||
|
|
fd653b87af | ||
|
|
9bf02e477b | ||
|
|
4440c668f1 | ||
|
|
63d5417a2c | ||
|
|
28d9a0faf2 | ||
|
|
04845df09c | ||
|
|
85df511a23 | ||
|
|
4ecb8bfede | ||
|
|
6008865a41 | ||
|
|
02926b2c6b | ||
|
|
49c5fd308e | ||
|
|
7eeff3bc35 | ||
|
|
6e99889681 | ||
|
|
8cbd09fe9a | ||
|
|
edc703c534 | ||
|
|
f85d545c17 | ||
|
|
0830173e59 | ||
|
|
0e21e3a9ed | ||
|
|
6356427454 | ||
|
|
4dc6220f28 | ||
|
|
03fcfaff28 | ||
|
|
d5c96f77cb | ||
|
|
bb6f2f0c0d | ||
|
|
8d54ed9316 | ||
|
|
44123c74d9 | ||
|
|
fdf2548fe3 | ||
|
|
bee72bd854 | ||
|
|
8ec8f32935 | ||
|
|
1d33feec8b | ||
|
|
80af6a9636 | ||
|
|
94eb4dd8a0 | ||
|
|
d3ce55cbd6 | ||
|
|
0b942caa98 | ||
|
|
afc6289a56 | ||
|
|
79684598ab | ||
|
|
b901f2f506 | ||
|
|
14294009f0 | ||
|
|
adbebd23c8 | ||
|
|
3973e13706 | ||
|
|
e56e68dd6f | ||
|
|
c3fcff8d29 | ||
|
|
0152f48886 | ||
|
|
bbd2e37067 | ||
|
|
8377b5062a | ||
|
|
086f1af974 | ||
|
|
652e989927 | ||
|
|
5480e15595 | ||
|
|
375670beb4 | ||
|
|
fa164f0e02 | ||
|
|
a9ede7a904 | ||
|
|
fa32d157ca | ||
|
|
10839f88d8 | ||
|
|
8fa909dea8 | ||
|
|
94250624a8 | ||
|
|
6f264441ab | ||
|
|
03cf463182 | ||
|
|
3e29501b23 | ||
|
|
600b882929 | ||
|
|
be69af925a | ||
|
|
d14ee640c5 | ||
|
|
ac6c688054 | ||
|
|
8f8e86ab4f | ||
|
|
ab1d6bfa90 | ||
|
|
e038601b37 | ||
|
|
008beb9444 | ||
|
|
835d9c9ef6 | ||
|
|
e29e5bd650 | ||
|
|
fecdbec31c | ||
|
|
66fdb607d8 | ||
|
|
6d4f192cbe | ||
|
|
4a59af7c20 | ||
|
|
156b363049 | ||
|
|
c68540c169 | ||
|
|
f89372e265 | ||
|
|
c134dffec2 | ||
|
|
c2d4527d7e | ||
|
|
2071b89d18 | ||
|
|
46902afdc4 | ||
|
|
d23e640977 | ||
|
|
9c18ab4142 | ||
|
|
22e270c4df | ||
|
|
16bd288e70 | ||
|
|
39643153df | ||
|
|
2cf9fdfd40 | ||
|
|
f5226562ae | ||
|
|
32ad5d2f8a | ||
|
|
245f47f660 | ||
|
|
17e3fd07f9 | ||
|
|
cdbe480863 | ||
|
|
189ad76f38 | ||
|
|
d8528fdc81 | ||
|
|
c257a8cab0 | ||
|
|
2912a3a5cb | ||
|
|
75f1d4d392 | ||
|
|
a6159cc785 | ||
|
|
38affb4981 | ||
|
|
e64090d88f | ||
|
|
ef60aeeead | ||
|
|
25112b2c27 | ||
|
|
c0b1326dbd | ||
|
|
f77e533dd6 | ||
|
|
4b001f9366 | ||
|
|
41eadce89a | ||
|
|
46194a5aa0 | ||
|
|
3630880454 | ||
|
|
a59ffacd3a | ||
|
|
03817bcfd7 | ||
|
|
2c4751bef9 | ||
|
|
7caa778ff8 | ||
|
|
3977d342ba | ||
|
|
4e83421104 | ||
|
|
adafacbf81 | ||
|
|
053893005b | ||
|
|
838af88d64 | ||
|
|
573ed07b4f | ||
|
|
aad8673ff0 | ||
|
|
b6a58ea8ab | ||
|
|
8c7bf18b65 | ||
|
|
3823c95582 | ||
|
|
ad0f3dca30 | ||
|
|
5248bbe9c3 | ||
|
|
1262a7b41f | ||
|
|
85611bcab5 | ||
|
|
46b5d9522d | ||
|
|
6dc9ee569b | ||
|
|
9916f64065 | ||
|
|
0ad6b13fae | ||
|
|
5bbdb76d21 | ||
|
|
cbf42b1e52 | ||
|
|
72867d855d | ||
|
|
7d5b984157 | ||
|
|
a3d5771b14 | ||
|
|
1338b4fb85 | ||
|
|
379d965769 | ||
|
|
d7bc73250d | ||
|
|
349daef47f | ||
|
|
138658c635 | ||
|
|
e058839faf | ||
|
|
d4d5146f66 | ||
|
|
85f4838067 | ||
|
|
f23108a3ba | ||
|
|
1623fc34cf | ||
|
|
d244c9bd1f | ||
|
|
e53e997cd4 | ||
|
|
5c646ef3d2 | ||
|
|
f01d36d4a0 | ||
|
|
37fc7e2cb4 | ||
|
|
ea68353328 | ||
|
|
da4bfb851c | ||
|
|
6a1f85975f | ||
|
|
700552bd75 | ||
|
|
5750b99af3 | ||
|
|
cc35e6b3a4 | ||
|
|
3b4800eb78 | ||
|
|
a2855ef522 | ||
|
|
935406b75b | ||
|
|
e933c5de60 | ||
|
|
a23a5491ae | ||
|
|
1ad8f51b7a | ||
|
|
4d1619346d | ||
|
|
df8c273cb9 | ||
|
|
6eca97b4aa | ||
|
|
39f8398b10 | ||
|
|
e32a356cb5 | ||
|
|
e0c556da10 | ||
|
|
a3f9e07cf7 | ||
|
|
ff40e55cd1 | ||
|
|
e548bda73a | ||
|
|
cf11b06f3e | ||
|
|
a799c39aff | ||
|
|
ff230b3433 | ||
|
|
a9f4db52b5 | ||
|
|
071e2a764a | ||
|
|
e050c914bb | ||
|
|
cd9ef74f3f | ||
|
|
644687a50c | ||
|
|
e7132e3371 | ||
|
|
7223ab3be5 | ||
|
|
99952281cb | ||
|
|
9c518dfeb6 | ||
|
|
28e2f13e87 | ||
|
|
4a9e109de5 | ||
|
|
f5b56ca67f | ||
|
|
b7ad4d3562 | ||
|
|
94a8caa5df | ||
|
|
b650ac0ca3 | ||
|
|
9dda594519 | ||
|
|
0ea6248773 | ||
|
|
fee003b824 | ||
|
|
cd415cc639 | ||
|
|
10ceac2ce3 | ||
|
|
86a0ff46ee | ||
|
|
3887ad0c4c | ||
|
|
58f534fcc8 | ||
|
|
f8e14006c1 | ||
|
|
a959a346b0 | ||
|
|
65459b3f4e | ||
|
|
2216899204 | ||
|
|
5fb4887c67 | ||
|
|
6ff2add9dd | ||
|
|
c963c1254d | ||
|
|
8afce40e81 | ||
|
|
e44504bc48 | ||
|
|
11b79dda5b | ||
|
|
8cee8b679c | ||
|
|
1cb9360d83 | ||
|
|
297f5ad7e6 | ||
|
|
3f2a011d7b | ||
|
|
dd0c36bb20 | ||
|
|
aff1bdfe89 | ||
|
|
00533f5d3b | ||
|
|
eff682bc05 | ||
|
|
16a210efa2 | ||
|
|
99a2a4ca1a | ||
|
|
67af68fcf1 | ||
|
|
b96e2d1bdf | ||
|
|
2bb0ce086f | ||
|
|
9bc518dc68 | ||
|
|
f60433ff3f | ||
|
|
b4493707ca | ||
|
|
0fe10d8654 | ||
|
|
87d9590f3b | ||
|
|
eeff9b74da | ||
|
|
ceebd19c0e | ||
|
|
00e0566dd4 | ||
|
|
acd4a04076 | ||
|
|
ac0aa29578 | ||
|
|
9a053d9ed8 | ||
|
|
9124a1fee5 | ||
|
|
858e7c7440 | ||
|
|
e5669e2a30 | ||
|
|
431b32b680 | ||
|
|
7dce11b4c4 | ||
|
|
9fca3a5984 | ||
|
|
733d5c38f3 | ||
|
|
3834b5a604 | ||
|
|
07e36029d0 | ||
|
|
689221f10e | ||
|
|
60dacdf05f | ||
|
|
1614bb0609 | ||
|
|
a386867dd0 | ||
|
|
e177d6eaea | ||
|
|
464492529e | ||
|
|
b950882846 | ||
|
|
63808c9e9c | ||
|
|
76d138d64d | ||
|
|
1232644268 | ||
|
|
2e776baec9 | ||
|
|
8fb4829aef | ||
|
|
fcbc844587 | ||
|
|
aecb1163e3 | ||
|
|
0e51a49898 | ||
|
|
adce543c45 | ||
|
|
83a32f96e6 | ||
|
|
6c72f0331b | ||
|
|
92f1fbf33b | ||
|
|
424fe2c2f0 | ||
|
|
f5d629c62c | ||
|
|
19fa975b6d | ||
|
|
a4951309d3 | ||
|
|
3ec53c29d4 | ||
|
|
3fcaafc42b | ||
|
|
5318acc427 | ||
|
|
daa4fc80eb | ||
|
|
c094d74d8e | ||
|
|
5418936816 | ||
|
|
c3b7679268 | ||
|
|
d12b83d1ef | ||
|
|
32b78d1a4c | ||
|
|
71f66fe8c0 | ||
|
|
d054bbe35f | ||
|
|
bd1c4e3cc9 | ||
|
|
928b765700 | ||
|
|
45acc28384 | ||
|
|
4e65abf6b1 | ||
|
|
d818f8a7ce | ||
|
|
c0850ce9d2 | ||
|
|
c3941df7e9 | ||
|
|
b1b46b3ec0 | ||
|
|
dff6679a90 | ||
|
|
4ab21aed0e | ||
|
|
d67a40f155 | ||
|
|
b506917382 | ||
|
|
944f7422b3 | ||
|
|
4843c046d7 | ||
|
|
61df4e7e61 | ||
|
|
48558316d1 | ||
|
|
5903b358ee | ||
|
|
6881098ca2 | ||
|
|
4d783649f5 | ||
|
|
2b40224b2b | ||
|
|
e4d6be3bb3 | ||
|
|
6c0b1550fd | ||
|
|
967fbe8072 | ||
|
|
c7dd7a2c21 | ||
|
|
49ed0e43e9 | ||
|
|
6bae985e2c | ||
|
|
39d7d9e213 | ||
|
|
eb3501e090 | ||
|
|
83566ad4e1 | ||
|
|
19e78d5eb6 | ||
|
|
0a2fdbf0c0 | ||
|
|
91f95f13ae | ||
|
|
ebf46042ba | ||
|
|
8547cee1b9 | ||
|
|
56729469bf | ||
|
|
e619d31622 | ||
|
|
370cc76dbe | ||
|
|
1ac2f9e923 | ||
|
|
8caf360671 | ||
|
|
59b1dcfd13 | ||
|
|
34a115706e | ||
|
|
b6c029df38 | ||
|
|
31ab333f5b | ||
|
|
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 | ||
|
|
ff8cb20cae | ||
|
|
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 |
No files matched your search
@@ -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,91 @@
|
|||||||
|
name: Build VoidLink IPA
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- Integration
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build .ipa and publish source
|
||||||
|
runs-on: macos
|
||||||
|
env:
|
||||||
|
# Per-app: change APP_NAME for each app repo. Used everywhere so the
|
||||||
|
# perlis dir name and file names always match (no hardcoded drift).
|
||||||
|
APP_NAME: VoidLink
|
||||||
|
# Point xcodebuild at full Xcode without sudo (sudo hangs in CI).
|
||||||
|
# DEVELOPER_DIR overrides the active developer directory for all steps.
|
||||||
|
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: git submodule update --init --recursive
|
||||||
|
|
||||||
|
- name: Select Xcode
|
||||||
|
run: xcodebuild -version
|
||||||
|
|
||||||
|
- name: Build unsigned .ipa
|
||||||
|
run: |
|
||||||
|
# Build the app (no signing — SideStore signs on-device).
|
||||||
|
# VoidLink's scheme is auto-generated from the VoidLink target.
|
||||||
|
# Release config → bundle id com.voidlink.tf.iOS (Debug uses
|
||||||
|
# com.voidlink.tf.debug10.iOS), so the .ipa matches the metadata.
|
||||||
|
xcodebuild \
|
||||||
|
-scheme VoidLink \
|
||||||
|
-sdk iphoneos \
|
||||||
|
-configuration Release \
|
||||||
|
archive \
|
||||||
|
-archivePath $PWD/build/VoidLink.xcarchive \
|
||||||
|
CODE_SIGN_IDENTITY="" \
|
||||||
|
CODE_SIGNING_REQUIRED="NO" \
|
||||||
|
CODE_SIGN_ENTITLEMENTS="" \
|
||||||
|
CODE_SIGNING_ALLOWED="NO"
|
||||||
|
|
||||||
|
# Package the .app bundle into an .ipa (Payload/App.app structure)
|
||||||
|
mkdir -p build/IPA/Payload
|
||||||
|
cp -R build/VoidLink.xcarchive/Products/Applications/VoidLink.app build/IPA/Payload/VoidLink.app
|
||||||
|
cd build/IPA
|
||||||
|
zip -r ../../$APP_NAME.ipa Payload
|
||||||
|
cd ../..
|
||||||
|
ls -la $APP_NAME.ipa
|
||||||
|
|
||||||
|
- name: Generate app metadata
|
||||||
|
run: |
|
||||||
|
# Read the version from the built app so it always matches the .ipa
|
||||||
|
# (no hardcoded drift). CFBundleShortVersionString = MARKETING_VERSION.
|
||||||
|
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" \
|
||||||
|
build/VoidLink.xcarchive/Products/Applications/VoidLink.app/Info.plist)
|
||||||
|
SIZE=$(stat -f%z $APP_NAME.ipa)
|
||||||
|
DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
# Per-app metadata file. The perlis aggregator folds this into the
|
||||||
|
# root source.json at https://apps.bueso.eu/source.json.
|
||||||
|
cat > $APP_NAME.json <<EOF
|
||||||
|
{
|
||||||
|
"name": "VoidLink",
|
||||||
|
"bundleIdentifier": "com.voidlink.tf.iOS",
|
||||||
|
"developerName": "Luis",
|
||||||
|
"localizedDescription": "VoidLink (Moonlight fork) - stream from your PC. Built via Gitea Actions.",
|
||||||
|
"iconURL": "https://apps.bueso.eu/$APP_NAME/icon.png",
|
||||||
|
"downloadURL": "https://apps.bueso.eu/$APP_NAME/$APP_NAME.ipa",
|
||||||
|
"version": "$VERSION",
|
||||||
|
"versionDate": "$DATE",
|
||||||
|
"size": $SIZE,
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
cat $APP_NAME.json
|
||||||
|
|
||||||
|
- name: Upload to source host (perlis)
|
||||||
|
run: |
|
||||||
|
# Upload .ipa + per-app metadata + icon to perlis via SSH.
|
||||||
|
# PERLIS_SSH_HOST is the Tailscale MagicDNS name (perlis.bueso.loc),
|
||||||
|
# reachable from the iMac runner over the Tailscale mesh.
|
||||||
|
ssh -o StrictHostKeyChecking=no \
|
||||||
|
"${{ secrets.PERLIS_SSH_USER }}@${{ secrets.PERLIS_SSH_HOST }}" \
|
||||||
|
"mkdir -p /mnt/appdata/ios-apps/$APP_NAME"
|
||||||
|
scp -o StrictHostKeyChecking=no \
|
||||||
|
$APP_NAME.ipa $APP_NAME.json icon.png \
|
||||||
|
"${{ secrets.PERLIS_SSH_USER }}@${{ secrets.PERLIS_SSH_HOST }}:/mnt/appdata/ios-apps/$APP_NAME/"
|
||||||
@@ -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,10 @@
|
|||||||
**/xcuserdata/
|
**/xcuserdata/
|
||||||
**/Moonlight.xcscmblueprint
|
**/Moonlight.xcscmblueprint
|
||||||
**/xcschemes/*.xcscheme
|
**/xcschemes/*.xcscheme
|
||||||
|
.DS_Store
|
||||||
Build
|
Build
|
||||||
DerivedData
|
DerivedData
|
||||||
|
build-home/
|
||||||
|
*.xccheckout
|
||||||
|
.github
|
||||||
|
README.md
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
[submodule "moonlight-common/moonlight-common-c"]
|
[submodule "moonlight-common/moonlight-common-c"]
|
||||||
path = 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"]
|
[submodule "X1Kit"]
|
||||||
path = X1Kit
|
path = X1Kit
|
||||||
url = https://github.com/cgutman/X1Kit.git
|
url = https://github.com/cgutman/X1Kit.git
|
||||||
|
[submodule "third_party/imgui"]
|
||||||
|
path = third_party/imgui
|
||||||
|
url = https://github.com/ocornut/imgui.git
|
||||||
|
[submodule "third_party/moonlight-audio-haptics"]
|
||||||
|
path = third_party/moonlight-audio-haptics
|
||||||
|
url = https://github.com/TrueZhuangJia/moonlight-audio-haptics.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,9 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="landscape" appearance="light"/>
|
<device id="retina6_1" orientation="landscape" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@@ -12,8 +13,13 @@
|
|||||||
<view contentMode="scaleToFill" id="VWy-YH-qYj">
|
<view contentMode="scaleToFill" id="VWy-YH-qYj">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
|
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<color key="backgroundColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<point key="canvasLocation" x="33" y="187"/>
|
<point key="canvasLocation" x="33" y="187"/>
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
@@ -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,245 +0,0 @@
|
|||||||
//
|
|
||||||
// DataManager.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 10/28/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "DataManager.h"
|
|
||||||
#import "TemporaryApp.h"
|
|
||||||
#import "TemporarySettings.h"
|
|
||||||
|
|
||||||
@implementation DataManager {
|
|
||||||
NSManagedObjectContext *_managedObjectContext;
|
|
||||||
AppDelegate *_appDelegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id) init {
|
|
||||||
self = [super init];
|
|
||||||
|
|
||||||
// HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
|
|
||||||
// Main Thread Checker happy.
|
|
||||||
if ([NSThread isMainThread]) {
|
|
||||||
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
||||||
self->_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
|
||||||
[_managedObjectContext setParentContext:[_appDelegate managedObjectContext]];
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateUniqueId:(NSString*)uniqueId {
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
[self retrieveSettings].uniqueId = uniqueId;
|
|
||||||
[self saveData];
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString*) getUniqueId {
|
|
||||||
__block NSString *uid;
|
|
||||||
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
uid = [self retrieveSettings].uniqueId;
|
|
||||||
}];
|
|
||||||
|
|
||||||
return uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (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 {
|
|
||||||
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
Settings* settingsToSave = [self retrieveSettings];
|
|
||||||
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
|
||||||
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
|
||||||
settingsToSave.height = [NSNumber numberWithInteger:height];
|
|
||||||
settingsToSave.width = [NSNumber numberWithInteger:width];
|
|
||||||
settingsToSave.audioConfig = [NSNumber numberWithInteger:audioConfig];
|
|
||||||
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
|
|
||||||
settingsToSave.optimizeGames = optimizeGames;
|
|
||||||
settingsToSave.multiController = multiController;
|
|
||||||
settingsToSave.swapABXYButtons = swapABXYButtons;
|
|
||||||
settingsToSave.playAudioOnPC = audioOnPC;
|
|
||||||
settingsToSave.preferredCodec = preferredCodec;
|
|
||||||
settingsToSave.useFramePacing = useFramePacing;
|
|
||||||
settingsToSave.enableHdr = enableHdr;
|
|
||||||
settingsToSave.btMouseSupport = btMouseSupport;
|
|
||||||
settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
|
||||||
settingsToSave.statsOverlay = statsOverlay;
|
|
||||||
|
|
||||||
[self saveData];
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateHost:(TemporaryHost *)host {
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
// Add a new persistent managed object if one doesn't exist
|
|
||||||
Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
|
||||||
if (parent == nil) {
|
|
||||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:self->_managedObjectContext];
|
|
||||||
parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push changes from the temp host to the persistent one
|
|
||||||
[host propagateChangesToParent:parent];
|
|
||||||
|
|
||||||
[self saveData];
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateAppsForExistingHost:(TemporaryHost *)host {
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
|
||||||
if (parent == nil) {
|
|
||||||
// The host must exist to be updated
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableSet *applist = [[NSMutableSet alloc] init];
|
|
||||||
NSArray *appRecords = [self fetchRecords:@"App"];
|
|
||||||
for (TemporaryApp* app in host.appList) {
|
|
||||||
// Add a new persistent managed object if one doesn't exist
|
|
||||||
App* parentApp = [self getAppForTemporaryApp:app withAppRecords:appRecords];
|
|
||||||
if (parentApp == nil) {
|
|
||||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:self->_managedObjectContext];
|
|
||||||
parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
|
|
||||||
}
|
|
||||||
|
|
||||||
[app propagateChangesToParent:parentApp withHost:parent];
|
|
||||||
|
|
||||||
[applist addObject:parentApp];
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.appList = applist;
|
|
||||||
|
|
||||||
[self saveData];
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (TemporarySettings*) getSettings {
|
|
||||||
__block TemporarySettings *tempSettings;
|
|
||||||
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
tempSettings = [[TemporarySettings alloc] initFromSettings:[self retrieveSettings]];
|
|
||||||
}];
|
|
||||||
|
|
||||||
return tempSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (Settings*) retrieveSettings {
|
|
||||||
NSArray* fetchedRecords = [self fetchRecords:@"Settings"];
|
|
||||||
if (fetchedRecords.count == 0) {
|
|
||||||
// create a new settings object with the default values
|
|
||||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Settings" inManagedObjectContext:_managedObjectContext];
|
|
||||||
Settings* settings = [[Settings alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
} else {
|
|
||||||
// we should only ever have 1 settings object stored
|
|
||||||
return [fetchedRecords objectAtIndex:0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) removeApp:(TemporaryApp*)app {
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
App* managedApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
|
|
||||||
if (managedApp != nil) {
|
|
||||||
[self->_managedObjectContext deleteObject:managedApp];
|
|
||||||
[self saveData];
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) removeHost:(TemporaryHost*)host {
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
Host* managedHost = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
|
||||||
if (managedHost != nil) {
|
|
||||||
[self->_managedObjectContext deleteObject:managedHost];
|
|
||||||
[self saveData];
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) saveData {
|
|
||||||
NSError* error;
|
|
||||||
if ([_managedObjectContext hasChanges] && ![_managedObjectContext save:&error]) {
|
|
||||||
Log(LOG_E, @"Unable to save hosts to database: %@", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
[_appDelegate saveContext];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSArray*) getHosts {
|
|
||||||
__block NSMutableArray *tempHosts = [[NSMutableArray alloc] init];
|
|
||||||
|
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
|
||||||
NSArray *hosts = [self fetchRecords:@"Host"];
|
|
||||||
|
|
||||||
for (Host* host in hosts) {
|
|
||||||
[tempHosts addObject:[[TemporaryHost alloc] initFromHost:host]];
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
return tempHosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only call from within performBlockAndWait!!!
|
|
||||||
- (Host*) getHostForTemporaryHost:(TemporaryHost*)tempHost withHostRecords:(NSArray*)hosts {
|
|
||||||
for (Host* host in hosts) {
|
|
||||||
if ([tempHost.uuid isEqualToString:host.uuid]) {
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only call from within performBlockAndWait!!!
|
|
||||||
- (App*) getAppForTemporaryApp:(TemporaryApp*)tempApp withAppRecords:(NSArray*)apps {
|
|
||||||
for (App* app in apps) {
|
|
||||||
if ([app.id isEqualToString:tempApp.id] &&
|
|
||||||
[app.host.uuid isEqualToString:tempApp.host.uuid]) {
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSArray*) fetchRecords:(NSString*)entityName {
|
|
||||||
NSArray* fetchedRecords;
|
|
||||||
|
|
||||||
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
|
|
||||||
NSEntityDescription* entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:_managedObjectContext];
|
|
||||||
[fetchRequest setEntity:entity];
|
|
||||||
|
|
||||||
NSError* error;
|
|
||||||
fetchedRecords = [_managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
|
||||||
//TODO: handle errors
|
|
||||||
|
|
||||||
return fetchedRecords;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
//
|
|
||||||
// TemporarySettings.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 12/1/15.
|
|
||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "TemporarySettings.h"
|
|
||||||
#import "OnScreenControls.h"
|
|
||||||
|
|
||||||
@implementation TemporarySettings
|
|
||||||
|
|
||||||
- (id) initFromSettings:(Settings*)settings {
|
|
||||||
self = [self init];
|
|
||||||
|
|
||||||
self.parent = settings;
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
// Apply default values from our Root.plist
|
|
||||||
NSString* settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
|
|
||||||
NSDictionary* settingsData = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
|
|
||||||
NSArray* preferences = [settingsData objectForKey:@"PreferenceSpecifiers"];
|
|
||||||
NSMutableDictionary* defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
|
|
||||||
for (NSDictionary* prefSpecification in preferences) {
|
|
||||||
NSString* key = [prefSpecification objectForKey:@"Key"];
|
|
||||||
if (key != nil) {
|
|
||||||
[defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister];
|
|
||||||
|
|
||||||
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.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.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
|
||||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
|
||||||
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
|
|
||||||
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"];
|
|
||||||
self.swapABXYButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"swapABXYButtons"];
|
|
||||||
self.btMouseSupport = [[NSUserDefaults standardUserDefaults] boolForKey:@"btMouseSupport"];
|
|
||||||
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
|
||||||
|
|
||||||
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
|
||||||
switch (_screenSize) {
|
|
||||||
case 0:
|
|
||||||
self.height = [NSNumber numberWithInteger:720];
|
|
||||||
self.width = [NSNumber numberWithInteger:1280];
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
self.height = [NSNumber numberWithInteger:1080];
|
|
||||||
self.width = [NSNumber numberWithInteger:1920];
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
self.height = [NSNumber numberWithInteger:2160];
|
|
||||||
self.width = [NSNumber numberWithInteger:3840];
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
self.height = [NSNumber numberWithInteger:1440];
|
|
||||||
self.width = [NSNumber numberWithInteger:2560];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
self.onscreenControls = [NSNumber numberWithInteger:OnScreenControlsLevelOff];
|
|
||||||
#else
|
|
||||||
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.playAudioOnPC = settings.playAudioOnPC;
|
|
||||||
self.enableHdr = settings.enableHdr;
|
|
||||||
self.optimizeGames = settings.optimizeGames;
|
|
||||||
self.multiController = settings.multiController;
|
|
||||||
self.swapABXYButtons = settings.swapABXYButtons;
|
|
||||||
self.onscreenControls = settings.onscreenControls;
|
|
||||||
self.btMouseSupport = settings.btMouseSupport;
|
|
||||||
self.absoluteTouchMode = settings.absoluteTouchMode;
|
|
||||||
self.statsOverlay = settings.statsOverlay;
|
|
||||||
#endif
|
|
||||||
self.uniqueId = settings.uniqueId;
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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,120 +0,0 @@
|
|||||||
//
|
|
||||||
// AbsoluteTouchHandler.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 11/1/20.
|
|
||||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "AbsoluteTouchHandler.h"
|
|
||||||
|
|
||||||
#include <Limelight.h>
|
|
||||||
|
|
||||||
// How long the fingers must be stationary to start a right click
|
|
||||||
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
|
||||||
|
|
||||||
// How far the finger can move before it cancels a right click
|
|
||||||
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
|
||||||
|
|
||||||
// How long the double tap deadzone stays in effect between touch up and touch down
|
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
|
|
||||||
|
|
||||||
// How far the finger can move before it can override the double tap deadzone
|
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
|
||||||
|
|
||||||
@implementation AbsoluteTouchHandler {
|
|
||||||
StreamView* view;
|
|
||||||
|
|
||||||
NSTimer* longPressTimer;
|
|
||||||
UITouch* lastTouchDown;
|
|
||||||
CGPoint lastTouchDownLocation;
|
|
||||||
UITouch* lastTouchUp;
|
|
||||||
CGPoint lastTouchUpLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view {
|
|
||||||
self = [self init];
|
|
||||||
self->view = view;
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)onLongPressStart:(NSTimer*)timer {
|
|
||||||
// Raise the left click and start a right click
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
// Ignore touch down events with more than one finger
|
|
||||||
if ([[event allTouches] count] > 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UITouch* touch = [touches anyObject];
|
|
||||||
CGPoint touchLocation = [touch locationInView:view];
|
|
||||||
|
|
||||||
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
|
||||||
if (touch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
|
||||||
sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchUpLocation.x / view.bounds.size.width), 2) +
|
|
||||||
pow((touchLocation.y / view.bounds.size.height) - (lastTouchUpLocation.y / view.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
|
||||||
[view updateCursorLocation:touchLocation isMouse:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Press the left button down
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
|
||||||
|
|
||||||
// Start the long press timer
|
|
||||||
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
|
|
||||||
target:self
|
|
||||||
selector:@selector(onLongPressStart:)
|
|
||||||
userInfo:nil
|
|
||||||
repeats:NO];
|
|
||||||
|
|
||||||
lastTouchDown = touch;
|
|
||||||
lastTouchDownLocation = touchLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
// Ignore touch move events with more than one finger
|
|
||||||
if ([[event allTouches] count] > 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UITouch* touch = [touches anyObject];
|
|
||||||
CGPoint touchLocation = [touch locationInView:view];
|
|
||||||
|
|
||||||
if (sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchDownLocation.x / view.bounds.size.width), 2) +
|
|
||||||
pow((touchLocation.y / view.bounds.size.height) - (lastTouchDownLocation.y / view.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
|
||||||
// Moved too far since touch down. Cancel the long press timer.
|
|
||||||
[longPressTimer invalidate];
|
|
||||||
longPressTimer = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
[view updateCursorLocation:[[touches anyObject] locationInView:view] isMouse:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
// Only fire this logic if all touches have ended
|
|
||||||
if ([[event allTouches] count] == [touches count]) {
|
|
||||||
// Cancel the long press timer
|
|
||||||
[longPressTimer invalidate];
|
|
||||||
longPressTimer = nil;
|
|
||||||
|
|
||||||
// Left button up on finger up
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
|
||||||
|
|
||||||
// Raise right button too in case we triggered a long press gesture
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
|
||||||
|
|
||||||
// Remember this last touch for touch-down deadzoning
|
|
||||||
lastTouchUp = [touches anyObject];
|
|
||||||
lastTouchUpLocation = [lastTouchUp locationInView:view];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
// Treat this as a normal touchesEnded event
|
|
||||||
[self touchesEnded:touches withEvent:event];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
//
|
|
||||||
// ControllerSupport.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 10/20/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "StreamConfiguration.h"
|
|
||||||
#import "Controller.h"
|
|
||||||
|
|
||||||
@class OnScreenControls;
|
|
||||||
|
|
||||||
@protocol ControllerSupportDelegate <NSObject>
|
|
||||||
|
|
||||||
- (void) gamepadPresenceChanged;
|
|
||||||
- (void) mousePresenceChanged;
|
|
||||||
- (void) streamExitRequested;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface ControllerSupport : NSObject
|
|
||||||
|
|
||||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
|
||||||
-(void) connectionEstablished;
|
|
||||||
|
|
||||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
|
|
||||||
-(void) cleanup;
|
|
||||||
-(Controller*) getOscController;
|
|
||||||
|
|
||||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y;
|
|
||||||
-(void) updateRightStick:(Controller*)controller x:(short)x y:(short)y;
|
|
||||||
|
|
||||||
-(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left;
|
|
||||||
-(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right;
|
|
||||||
-(void) updateTriggers:(Controller*)controller left:(unsigned char)left right:(unsigned char)right;
|
|
||||||
|
|
||||||
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags;
|
|
||||||
-(void) setButtonFlag:(Controller*)controller flags:(int)flags;
|
|
||||||
-(void) clearButtonFlag:(Controller*)controller flags:(int)flags;
|
|
||||||
|
|
||||||
-(void) updateFinished:(Controller*)controller;
|
|
||||||
|
|
||||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
|
|
||||||
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
|
|
||||||
-(void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz;
|
|
||||||
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
|
|
||||||
|
|
||||||
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
|
|
||||||
|
|
||||||
-(NSUInteger) getConnectedGamepadCount;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
//
|
|
||||||
// HapticContext.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 9/17/20.
|
|
||||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "HapticContext.h"
|
|
||||||
|
|
||||||
@import CoreHaptics;
|
|
||||||
@import GameController;
|
|
||||||
|
|
||||||
@implementation HapticContext {
|
|
||||||
GCControllerPlayerIndex _playerIndex;
|
|
||||||
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0));
|
|
||||||
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
|
|
||||||
BOOL _playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void)cleanup API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
|
||||||
if (_hapticPlayer != nil) {
|
|
||||||
[_hapticPlayer cancelAndReturnError:nil];
|
|
||||||
_hapticPlayer = nil;
|
|
||||||
}
|
|
||||||
if (_hapticEngine != nil) {
|
|
||||||
[_hapticEngine stopWithCompletionHandler:nil];
|
|
||||||
_hapticEngine = nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void)setMotorAmplitude:(unsigned short)amplitude API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
|
||||||
NSError* error;
|
|
||||||
|
|
||||||
// Check if the haptic engine died
|
|
||||||
if (_hapticEngine == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the effect entirely if the amplitude is 0
|
|
||||||
if (amplitude == 0) {
|
|
||||||
if (_playing) {
|
|
||||||
[_hapticPlayer stopAtTime:0 error:&error];
|
|
||||||
_playing = NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_hapticPlayer == nil) {
|
|
||||||
// We must initialize the intensity to 1.0f because the dynamic parameters are multiplied by this value before being applied
|
|
||||||
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
|
|
||||||
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite];
|
|
||||||
CHHapticPattern* hapticPattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:hapticEvent] parameters:[[NSArray alloc] init] error:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic pattern creation failed: %@", _playerIndex, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CHHapticDynamicParameter* intensityParameter = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:amplitude / 65535.0f relativeTime:0];
|
|
||||||
[_hapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_playing) {
|
|
||||||
[_hapticPlayer startAtTime:0 error:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
_hapticPlayer = nil;
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_playing = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
|
||||||
if (gamepad.haptics == nil) {
|
|
||||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
|
||||||
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
_playerIndex = gamepad.playerIndex;
|
|
||||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
|
||||||
|
|
||||||
NSError* error;
|
|
||||||
[_hapticEngine startAndReturnError:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
__weak typeof(self) weakSelf = self;
|
|
||||||
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
|
|
||||||
HapticContext* me = weakSelf;
|
|
||||||
if (me == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
|
|
||||||
me->_hapticPlayer = nil;
|
|
||||||
me->_hapticEngine = nil;
|
|
||||||
me->_playing = NO;
|
|
||||||
};
|
|
||||||
_hapticEngine.resetHandler = ^{
|
|
||||||
HapticContext* me = weakSelf;
|
|
||||||
if (me == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
|
|
||||||
me->_hapticPlayer = nil;
|
|
||||||
me->_playing = NO;
|
|
||||||
[me->_hapticEngine startAndReturnError:nil];
|
|
||||||
};
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad {
|
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad {
|
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad {
|
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad {
|
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -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,958 +0,0 @@
|
|||||||
//
|
|
||||||
// StreamView.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 10/19/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "StreamView.h"
|
|
||||||
#include <Limelight.h>
|
|
||||||
#import "DataManager.h"
|
|
||||||
#import "ControllerSupport.h"
|
|
||||||
#import "KeyboardSupport.h"
|
|
||||||
#import "RelativeTouchHandler.h"
|
|
||||||
#import "AbsoluteTouchHandler.h"
|
|
||||||
#import "KeyboardInputField.h"
|
|
||||||
|
|
||||||
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|
||||||
|
|
||||||
@implementation StreamView {
|
|
||||||
OnScreenControls* onScreenControls;
|
|
||||||
|
|
||||||
KeyboardInputField* keyInputField;
|
|
||||||
BOOL isInputingText;
|
|
||||||
NSMutableSet* keysDown;
|
|
||||||
|
|
||||||
float streamAspectRatio;
|
|
||||||
|
|
||||||
// iOS 13.4 mouse support
|
|
||||||
NSInteger lastMouseButtonMask;
|
|
||||||
float lastMouseX;
|
|
||||||
float lastMouseY;
|
|
||||||
CGPoint lastScrollTranslation;
|
|
||||||
|
|
||||||
// Citrix X1 mouse support
|
|
||||||
X1Mouse* x1mouse;
|
|
||||||
double accumulatedMouseDeltaX;
|
|
||||||
double accumulatedMouseDeltaY;
|
|
||||||
|
|
||||||
UIResponder* touchHandler;
|
|
||||||
|
|
||||||
id<UserInteractionDelegate> interactionDelegate;
|
|
||||||
NSTimer* interactionTimer;
|
|
||||||
BOOL hasUserInteracted;
|
|
||||||
|
|
||||||
NSDictionary<NSString *, NSNumber *> *dictCodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
|
||||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
|
||||||
config:(StreamConfiguration*)streamConfig {
|
|
||||||
self->interactionDelegate = interactionDelegate;
|
|
||||||
self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
|
|
||||||
|
|
||||||
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
|
|
||||||
|
|
||||||
keysDown = [[NSMutableSet alloc] init];
|
|
||||||
keyInputField = [[KeyboardInputField alloc] initWithFrame:CGRectZero];
|
|
||||||
[keyInputField setKeyboardType:UIKeyboardTypeDefault];
|
|
||||||
[keyInputField setAutocorrectionType:UITextAutocorrectionTypeNo];
|
|
||||||
[keyInputField setAutocapitalizationType:UITextAutocapitalizationTypeNone];
|
|
||||||
[keyInputField setSpellCheckingType:UITextSpellCheckingTypeNo];
|
|
||||||
[self addSubview:keyInputField];
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
// tvOS requires RelativeTouchHandler to manage Apple Remote input
|
|
||||||
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
|
|
||||||
#else
|
|
||||||
// iOS uses RelativeTouchHandler or AbsoluteTouchHandler depending on user preference
|
|
||||||
if (settings.absoluteTouchMode) {
|
|
||||||
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
|
|
||||||
}
|
|
||||||
|
|
||||||
onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport streamConfig:streamConfig];
|
|
||||||
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
|
|
||||||
if (settings.absoluteTouchMode) {
|
|
||||||
Log(LOG_I, @"On-screen controls disabled in absolute touch mode");
|
|
||||||
[onScreenControls setLevel:OnScreenControlsLevelOff];
|
|
||||||
}
|
|
||||||
else if (level == OnScreenControlsLevelAuto) {
|
|
||||||
[controllerSupport initAutoOnScreenControlMode:onScreenControls];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
|
|
||||||
[onScreenControls setLevel:level];
|
|
||||||
}
|
|
||||||
|
|
||||||
// It would be nice to just use GCMouse on iOS 14+ and the older API on iOS 13
|
|
||||||
// but unfortunately that isn't possible today. GCMouse doesn't recognize many
|
|
||||||
// mice correctly, but UIKit does. We will register for both and ignore UIKit
|
|
||||||
// events if a GCMouse is connected.
|
|
||||||
if (@available(iOS 13.4, *)) {
|
|
||||||
[self addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]];
|
|
||||||
|
|
||||||
UIPanGestureRecognizer *discreteMouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMovedDiscrete:)];
|
|
||||||
discreteMouseWheelRecognizer.maximumNumberOfTouches = 0;
|
|
||||||
discreteMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
|
|
||||||
discreteMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
|
|
||||||
[self addGestureRecognizer:discreteMouseWheelRecognizer];
|
|
||||||
|
|
||||||
UIPanGestureRecognizer *continuousMouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMovedContinuous:)];
|
|
||||||
continuousMouseWheelRecognizer.maximumNumberOfTouches = 0;
|
|
||||||
continuousMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
|
|
||||||
continuousMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
|
|
||||||
[self addGestureRecognizer:continuousMouseWheelRecognizer];
|
|
||||||
}
|
|
||||||
|
|
||||||
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
|
|
||||||
if (@available(iOS 16.1, *)) {
|
|
||||||
UIHoverGestureRecognizer *stylusHoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(sendStylusHoverEvent:)];
|
|
||||||
stylusHoverRecognizer.allowedTouchTypes = @[@(UITouchTypePencil)];
|
|
||||||
[self addGestureRecognizer:stylusHoverRecognizer];
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
x1mouse = [[X1Mouse alloc] init];
|
|
||||||
x1mouse.delegate = self;
|
|
||||||
|
|
||||||
if (settings.btMouseSupport) {
|
|
||||||
[x1mouse start];
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is critical to ensure keyboard events are delivered to this
|
|
||||||
// StreamView and not our parent UIView, especially on tvOS.
|
|
||||||
[self becomeFirstResponder];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)startInteractionTimer {
|
|
||||||
// Restart user interaction tracking
|
|
||||||
hasUserInteracted = NO;
|
|
||||||
|
|
||||||
BOOL timerAlreadyRunning = interactionTimer != nil;
|
|
||||||
|
|
||||||
// Start/restart the timer
|
|
||||||
[interactionTimer invalidate];
|
|
||||||
interactionTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
|
|
||||||
target:self
|
|
||||||
selector:@selector(interactionTimerExpired:)
|
|
||||||
userInfo:nil
|
|
||||||
repeats:NO];
|
|
||||||
|
|
||||||
// Notify the delegate if this was a new user interaction
|
|
||||||
if (!timerAlreadyRunning) {
|
|
||||||
[interactionDelegate userInteractionBegan];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)interactionTimerExpired:(NSTimer *)timer {
|
|
||||||
if (!hasUserInteracted) {
|
|
||||||
// User has finished touching the screen
|
|
||||||
interactionTimer = nil;
|
|
||||||
[interactionDelegate userInteractionEnded];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// User is still touching the screen. Restart the timer.
|
|
||||||
[self startInteractionTimer];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) showOnScreenControls {
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
[onScreenControls show];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
- (OnScreenControlsLevel) getCurrentOscState {
|
|
||||||
if (onScreenControls == nil) {
|
|
||||||
return OnScreenControlsLevelOff;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return [onScreenControls getLevel];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (CGSize) getVideoAreaSize {
|
|
||||||
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
|
|
||||||
return CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
|
|
||||||
} else {
|
|
||||||
return CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point {
|
|
||||||
// These are now relative to the StreamView, however we need to scale them
|
|
||||||
// further to make them relative to the actual video portion.
|
|
||||||
float x = point.x - self.bounds.origin.x;
|
|
||||||
float y = point.y - self.bounds.origin.y;
|
|
||||||
|
|
||||||
// For some reason, we don't seem to always get to the bounds of the window
|
|
||||||
// so we'll subtract 1 pixel if we're to the left/below of the origin and
|
|
||||||
// and add 1 pixel if we're to the right/above. It should be imperceptible
|
|
||||||
// to the user but it will allow activation of gestures that require contact
|
|
||||||
// with the edge of the screen (like Aero Snap).
|
|
||||||
if (x < self.bounds.size.width / 2) {
|
|
||||||
x--;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
x++;
|
|
||||||
}
|
|
||||||
if (y < self.bounds.size.height / 2) {
|
|
||||||
y--;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
y++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
|
|
||||||
CGSize videoSize = [self getVideoAreaSize];
|
|
||||||
CGPoint videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
|
|
||||||
self.bounds.size.height / 2 - videoSize.height / 2);
|
|
||||||
|
|
||||||
// Confine the cursor to the video region. We don't just discard events outside
|
|
||||||
// the region because we won't always get one exactly when the mouse leaves the region.
|
|
||||||
return CGPointMake(MIN(MAX(x, videoOrigin.x), videoOrigin.x + videoSize.width) - videoOrigin.x,
|
|
||||||
MIN(MAX(y, videoOrigin.y), videoOrigin.y + videoSize.height) - videoOrigin.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
|
|
||||||
- (uint16_t)getRotationFromAzimuthAngle:(float)azimuthAngle {
|
|
||||||
// iOS reports azimuth of 0 when the stylus is pointing west, but Moonlight expects
|
|
||||||
// rotation of 0 to mean the stylus is pointing north. Rotate the azimuth angle
|
|
||||||
// clockwise by 90 degrees to convert from iOS to Moonlight rotation conventions.
|
|
||||||
int32_t rotationAngle = (azimuthAngle - M_PI_2) * (180.f / M_PI);
|
|
||||||
if (rotationAngle < 0) {
|
|
||||||
rotationAngle += 360;
|
|
||||||
}
|
|
||||||
return (uint16_t)rotationAngle;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (uint8_t)getTiltFromAltitudeAngle:(float)altitudeAngle {
|
|
||||||
// iOS reports an altitude of 0 when the stylus is parallel to the touch surface,
|
|
||||||
// while Moonlight expects a tilt of 0 when the stylus is perpendicular to the surface.
|
|
||||||
// Subtract the tilt angle from 90 to convert from iOS to Moonlight tilt conventions.
|
|
||||||
uint8_t altitudeDegs = abs((int16_t)(altitudeAngle * (180.f / M_PI)));
|
|
||||||
return 90 - MIN(90, altitudeDegs);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)sendStylusEvent:(UITouch*)event {
|
|
||||||
uint8_t type;
|
|
||||||
|
|
||||||
// Don't touch stylus events if the host doesn't support them. We want to pass
|
|
||||||
// them as normal touches for legacy hosts that don't understand pen events.
|
|
||||||
if (!(LiGetHostFeatureFlags() & LI_FF_PEN_TOUCH_EVENTS)) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.phase) {
|
|
||||||
case UITouchPhaseBegan:
|
|
||||||
type = LI_TOUCH_EVENT_DOWN;
|
|
||||||
break;
|
|
||||||
case UITouchPhaseMoved:
|
|
||||||
type = LI_TOUCH_EVENT_MOVE;
|
|
||||||
break;
|
|
||||||
case UITouchPhaseEnded:
|
|
||||||
type = LI_TOUCH_EVENT_UP;
|
|
||||||
break;
|
|
||||||
case UITouchPhaseCancelled:
|
|
||||||
type = LI_TOUCH_EVENT_CANCEL;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]];
|
|
||||||
CGSize videoSize = [self getVideoAreaSize];
|
|
||||||
|
|
||||||
return LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
|
|
||||||
(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),
|
|
||||||
0.0f, 0.0f,
|
|
||||||
[self getRotationFromAzimuthAngle:[event azimuthAngleInView:self]],
|
|
||||||
[self getTiltFromAltitudeAngle:event.altitudeAngle]) != LI_ERR_UNSUPPORTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)sendStylusHoverEvent:(UIHoverGestureRecognizer*)gesture API_AVAILABLE(ios(13.0)) {
|
|
||||||
uint8_t type;
|
|
||||||
|
|
||||||
switch (gesture.state) {
|
|
||||||
case UIGestureRecognizerStateBegan:
|
|
||||||
case UIGestureRecognizerStateChanged:
|
|
||||||
type = LI_TOUCH_EVENT_HOVER;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UIGestureRecognizerStateEnded:
|
|
||||||
type = LI_TOUCH_EVENT_HOVER_LEAVE;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGPoint location = [self adjustCoordinatesForVideoArea:[gesture locationInView:self]];
|
|
||||||
CGSize videoSize = [self getVideoAreaSize];
|
|
||||||
|
|
||||||
float distance = 0.0f;
|
|
||||||
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
|
|
||||||
if (@available(iOS 16.1, *)) {
|
|
||||||
distance = gesture.zOffset;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
uint16_t rotationAngle = LI_ROT_UNKNOWN;
|
|
||||||
uint8_t tiltAngle = LI_TILT_UNKNOWN;
|
|
||||||
#if defined(__IPHONE_16_4) || defined(__TVOS_16_4)
|
|
||||||
if (@available(iOS 16.4, *)) {
|
|
||||||
rotationAngle = [self getRotationFromAzimuthAngle:[gesture azimuthAngleInView:self]];
|
|
||||||
tiltAngle = [self getTiltFromAltitudeAngle:gesture.altitudeAngle];
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
|
|
||||||
distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
|
|
||||||
forTouches:touches
|
|
||||||
withEvent:event]) {
|
|
||||||
// If it's a mouse event, we're done
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log(LOG_D, @"Touch down");
|
|
||||||
|
|
||||||
// Notify of user interaction and start expiration timer
|
|
||||||
[self startInteractionTimer];
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
if (@available(iOS 13.4, *)) {
|
|
||||||
for (UITouch* touch in touches) {
|
|
||||||
if (touch.type == UITouchTypePencil) {
|
|
||||||
if ([self sendStylusEvent:touch]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (![onScreenControls handleTouchDownEvent:touches]) {
|
|
||||||
// We still inform the touch handler even if we're going trigger the
|
|
||||||
// keyboard activation gesture. This is important to ensure the touch
|
|
||||||
// handler has a consistent view of touch events to correctly suppress
|
|
||||||
// activation of one or two finger gestures when a three finger gesture
|
|
||||||
// is triggered.
|
|
||||||
[touchHandler touchesBegan:touches withEvent:event];
|
|
||||||
|
|
||||||
if ([[event allTouches] count] == 3) {
|
|
||||||
if (isInputingText) {
|
|
||||||
Log(LOG_D, @"Closing the keyboard");
|
|
||||||
[keyInputField resignFirstResponder];
|
|
||||||
isInputingText = false;
|
|
||||||
} else {
|
|
||||||
Log(LOG_D, @"Opening the keyboard");
|
|
||||||
// Prepare the textbox used to capture keyboard events.
|
|
||||||
keyInputField.delegate = self;
|
|
||||||
keyInputField.text = @"0";
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
// Prepare the toolbar above the keyboard for more options
|
|
||||||
UIToolbar *customToolbarView = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 44)];
|
|
||||||
|
|
||||||
UIBarButtonItem *doneBarButton = [self createButtonWithImageNamed:@"DoneIcon.png" backgroundColor:[UIColor clearColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x00 isToggleable:NO];
|
|
||||||
UIBarButtonItem *windowsBarButton = [self createButtonWithImageNamed:@"WindowsIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x5B isToggleable:YES];
|
|
||||||
UIBarButtonItem *tabBarButton = [self createButtonWithImageNamed:@"TabIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x09 isToggleable:NO];
|
|
||||||
UIBarButtonItem *shiftBarButton = [self createButtonWithImageNamed:@"ShiftIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA0 isToggleable:YES];
|
|
||||||
UIBarButtonItem *escapeBarButton = [self createButtonWithImageNamed:@"EscapeIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x1B isToggleable:NO];
|
|
||||||
UIBarButtonItem *controlBarButton = [self createButtonWithImageNamed:@"ControlIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA2 isToggleable:YES];
|
|
||||||
UIBarButtonItem *altBarButton = [self createButtonWithImageNamed:@"AltIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA4 isToggleable:YES];
|
|
||||||
UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO];
|
|
||||||
UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
|
|
||||||
|
|
||||||
[customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, nil]];
|
|
||||||
keyInputField.inputAccessoryView = customToolbarView;
|
|
||||||
#endif
|
|
||||||
[keyInputField becomeFirstResponder];
|
|
||||||
[keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
|
|
||||||
|
|
||||||
// Undo causes issues for our state management, so turn it off
|
|
||||||
[keyInputField.undoManager disableUndoRegistration];
|
|
||||||
|
|
||||||
isInputingText = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIBarButtonItem *)createButtonWithImageNamed:(NSString *)imageName backgroundColor:(UIColor *)backgroundColor target:(id)target action:(SEL)action keyCode:(NSInteger)keyCode isToggleable:(BOOL)isToggleable {
|
|
||||||
UIImage *image = [UIImage imageNamed:imageName];
|
|
||||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
||||||
[button setImage:image forState:UIControlStateNormal];
|
|
||||||
button.frame = CGRectMake(0, 0, 30, 30);
|
|
||||||
button.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
|
||||||
button.imageView.backgroundColor = backgroundColor;
|
|
||||||
button.imageView.layer.cornerRadius = 10.0;
|
|
||||||
button.imageEdgeInsets = UIEdgeInsetsMake(6, 6, 6, 6);
|
|
||||||
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
|
|
||||||
objc_setAssociatedObject(button, "keyCode", @(keyCode), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
||||||
objc_setAssociatedObject(button, "isToggleable", @(isToggleable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
||||||
objc_setAssociatedObject(button, "isOn", @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
||||||
UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:button];
|
|
||||||
return barButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)toolbarButtonClicked:(UIButton *)sender {
|
|
||||||
BOOL isToggleable = [objc_getAssociatedObject(sender, "isToggleable") boolValue];
|
|
||||||
BOOL isOn = [objc_getAssociatedObject(sender, "isOn") boolValue];
|
|
||||||
if (isToggleable){
|
|
||||||
isOn = !isOn;
|
|
||||||
// Update the button's appearance based on its new state
|
|
||||||
if (isOn) {
|
|
||||||
sender.imageView.backgroundColor = [UIColor lightGrayColor];
|
|
||||||
} else {
|
|
||||||
sender.imageView.backgroundColor = [UIColor blackColor];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update the new on/off state of the button
|
|
||||||
objc_setAssociatedObject(sender, "isOn", @(isOn), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
||||||
// Get the keyCode parameter and convert to short for key press event
|
|
||||||
short keyCode = [objc_getAssociatedObject(sender, "keyCode") shortValue];
|
|
||||||
// Close keyboard if done button clicked
|
|
||||||
if (!keyCode) {
|
|
||||||
[keyInputField resignFirstResponder];
|
|
||||||
isInputingText = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Send key press event using keyCode parameter, toggle if necessary
|
|
||||||
if (isToggleable){
|
|
||||||
if (isOn){
|
|
||||||
LiSendKeyboardEvent(keyCode, KEY_ACTION_DOWN, 0);
|
|
||||||
[keysDown addObject:@(keyCode)];
|
|
||||||
} else {
|
|
||||||
LiSendKeyboardEvent(keyCode, KEY_ACTION_UP, 0);
|
|
||||||
[keysDown removeObject:@(keyCode)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LiSendKeyboardEvent(keyCode, KEY_ACTION_DOWN, 0);
|
|
||||||
usleep(50 * 1000);
|
|
||||||
LiSendKeyboardEvent(keyCode, KEY_ACTION_UP, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
if (@available(iOS 13.4, *)) {
|
|
||||||
UITouch* touch = [touches anyObject];
|
|
||||||
if (touch.type == UITouchTypeIndirectPointer) {
|
|
||||||
if (@available(iOS 14.0, *)) {
|
|
||||||
if ([GCMouse current] != nil) {
|
|
||||||
// We'll handle this with GCMouse. Do nothing here.
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UIEventButtonMask normalizedButtonMask;
|
|
||||||
|
|
||||||
// iOS 14 includes the released button in the buttonMask for the release
|
|
||||||
// event, while iOS 13 does not. Normalize that behavior here.
|
|
||||||
if (@available(iOS 14.0, *)) {
|
|
||||||
if (buttonAction == BUTTON_ACTION_RELEASE) {
|
|
||||||
normalizedButtonMask = lastMouseButtonMask & ~event.buttonMask;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
normalizedButtonMask = event.buttonMask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
normalizedButtonMask = event.buttonMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIEventButtonMask changedButtons = lastMouseButtonMask ^ normalizedButtonMask;
|
|
||||||
|
|
||||||
for (int i = BUTTON_LEFT; i <= BUTTON_X2; i++) {
|
|
||||||
UIEventButtonMask buttonFlag;
|
|
||||||
|
|
||||||
switch (i) {
|
|
||||||
// Right and Middle are reversed from what iOS uses
|
|
||||||
case BUTTON_RIGHT:
|
|
||||||
buttonFlag = UIEventButtonMaskForButtonNumber(2);
|
|
||||||
break;
|
|
||||||
case BUTTON_MIDDLE:
|
|
||||||
buttonFlag = UIEventButtonMaskForButtonNumber(3);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
buttonFlag = UIEventButtonMaskForButtonNumber(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedButtons & buttonFlag) {
|
|
||||||
LiSendMouseButtonEvent(buttonAction, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMouseButtonMask = normalizedButtonMask;
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
if (@available(iOS 13.4, *)) {
|
|
||||||
for (UITouch* touch in touches) {
|
|
||||||
if (touch.type == UITouchTypePencil) {
|
|
||||||
if ([self sendStylusEvent:touch]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UITouch *touch = [touches anyObject];
|
|
||||||
if (touch.type == UITouchTypeIndirectPointer) {
|
|
||||||
if (@available(iOS 14.0, *)) {
|
|
||||||
if ([GCMouse current] != nil) {
|
|
||||||
// We'll handle this with GCMouse. Do nothing here.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We must handle this event to properly support
|
|
||||||
// drags while the middle, X1, or X2 mouse buttons are
|
|
||||||
// held down. For some reason, left and right buttons
|
|
||||||
// don't require this, but we do it anyway for them too.
|
|
||||||
// Cursor movement without a button held down is handled
|
|
||||||
// in pointerInteraction:regionForRequest:defaultRegion.
|
|
||||||
[self updateCursorLocation:[touch locationInView:self] isMouse:YES];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
hasUserInteracted = YES;
|
|
||||||
|
|
||||||
if (![onScreenControls handleTouchMovedEvent:touches]) {
|
|
||||||
[touchHandler touchesMoved:touches withEvent:event];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
|
|
||||||
BOOL handled = NO;
|
|
||||||
|
|
||||||
if (@available(iOS 13.4, tvOS 13.4, *)) {
|
|
||||||
for (UIPress* press in presses) {
|
|
||||||
// For now, we'll treated it as handled if we handle at least one of the
|
|
||||||
// UIPress events inside the set.
|
|
||||||
if ([KeyboardSupport sendKeyEventForPress:press down:YES]) {
|
|
||||||
// This will prevent the legacy UITextField from receiving the event
|
|
||||||
handled = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!handled) {
|
|
||||||
[super pressesBegan:presses withEvent:event];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
|
|
||||||
BOOL handled = NO;
|
|
||||||
|
|
||||||
if (@available(iOS 13.4, tvOS 13.4, *)) {
|
|
||||||
for (UIPress* press in presses) {
|
|
||||||
// For now, we'll treated it as handled if we handle at least one of the
|
|
||||||
// UIPress events inside the set.
|
|
||||||
if ([KeyboardSupport sendKeyEventForPress:press down:NO]) {
|
|
||||||
// This will prevent the legacy UITextField from receiving the event
|
|
||||||
handled = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!handled) {
|
|
||||||
[super pressesEnded:presses withEvent:event];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
|
|
||||||
forTouches:touches
|
|
||||||
withEvent:event]) {
|
|
||||||
// If it's a mouse event, we're done
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log(LOG_D, @"Touch up");
|
|
||||||
|
|
||||||
hasUserInteracted = YES;
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
if (@available(iOS 13.4, *)) {
|
|
||||||
for (UITouch* touch in touches) {
|
|
||||||
if (touch.type == UITouchTypePencil) {
|
|
||||||
if ([self sendStylusEvent:touch]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (![onScreenControls handleTouchUpEvent:touches]) {
|
|
||||||
[touchHandler touchesEnded:touches withEvent:event];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
[touchHandler touchesCancelled:touches withEvent:event];
|
|
||||||
[self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
|
|
||||||
forTouches:touches
|
|
||||||
withEvent:event];
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
if (@available(iOS 13.4, *)) {
|
|
||||||
for (UITouch* touch in touches) {
|
|
||||||
if (touch.type == UITouchTypePencil) {
|
|
||||||
[self sendStylusEvent:touch];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse {
|
|
||||||
CGPoint normalizedLocation = [self adjustCoordinatesForVideoArea:location];
|
|
||||||
CGSize videoSize = [self getVideoAreaSize];
|
|
||||||
|
|
||||||
// Send the mouse position relative to the video region if it has changed
|
|
||||||
// if we're receiving coordinates from a real mouse.
|
|
||||||
//
|
|
||||||
// NB: It is important for functionality (not just optimization) to only
|
|
||||||
// send it if the value has changed. We will receive one of these events
|
|
||||||
// any time the user presses a modifier key, which can result in errant
|
|
||||||
// mouse motion when using a Citrix X1 mouse.
|
|
||||||
if (normalizedLocation.x != lastMouseX || normalizedLocation.y != lastMouseY || !isMouse) {
|
|
||||||
if (lastMouseX != 0 || lastMouseY != 0 || !isMouse) {
|
|
||||||
LiSendMousePositionEvent(normalizedLocation.x, normalizedLocation.y, videoSize.width, videoSize.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMouse) {
|
|
||||||
lastMouseX = normalizedLocation.x;
|
|
||||||
lastMouseY = normalizedLocation.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction
|
|
||||||
regionForRequest:(UIPointerRegionRequest *)request
|
|
||||||
defaultRegion:(UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4)) {
|
|
||||||
if (@available(iOS 14.0, *)) {
|
|
||||||
if ([GCMouse current] != nil) {
|
|
||||||
// We'll handle this with GCMouse. Do nothing here.
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
|
|
||||||
CGSize videoSize;
|
|
||||||
CGPoint videoOrigin;
|
|
||||||
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
|
|
||||||
videoSize = CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
|
|
||||||
} else {
|
|
||||||
videoSize = CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
|
|
||||||
}
|
|
||||||
videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
|
|
||||||
self.bounds.size.height / 2 - videoSize.height / 2);
|
|
||||||
|
|
||||||
// Move the cursor on the host if no buttons are pressed.
|
|
||||||
// Motion with buttons pressed in handled in touchesMoved:
|
|
||||||
if (lastMouseButtonMask == 0) {
|
|
||||||
[self updateCursorLocation:request.location isMouse:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
// The pointer interaction should cover the video region only
|
|
||||||
return [UIPointerRegion regionWithRect:CGRectMake(videoOrigin.x, videoOrigin.y, videoSize.width, videoSize.height) identifier:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) {
|
|
||||||
// Always hide the mouse cursor over our stream view
|
|
||||||
return [UIPointerStyle hiddenPointerStyle];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)mouseWheelMovedContinuous:(UIPanGestureRecognizer *)gesture {
|
|
||||||
switch (gesture.state) {
|
|
||||||
case UIGestureRecognizerStateBegan:
|
|
||||||
case UIGestureRecognizerStateChanged:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UIGestureRecognizerStateEnded:
|
|
||||||
default:
|
|
||||||
// Ignore recognition failure and other states
|
|
||||||
lastScrollTranslation = CGPointMake(0, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CGPoint currentScrollTranslation = [gesture translationInView:self];
|
|
||||||
const short translationMultiplier = 120 * 20; // WHEEL_DELTA * 20
|
|
||||||
|
|
||||||
{
|
|
||||||
short translationDeltaY = ((currentScrollTranslation.y - lastScrollTranslation.y) / self.bounds.size.height) * translationMultiplier;
|
|
||||||
if (translationDeltaY != 0) {
|
|
||||||
LiSendHighResScrollEvent(translationDeltaY);
|
|
||||||
lastScrollTranslation = currentScrollTranslation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
short translationDeltaX = ((currentScrollTranslation.x - lastScrollTranslation.x) / self.bounds.size.width) * translationMultiplier;
|
|
||||||
if (translationDeltaX != 0) {
|
|
||||||
// Direction is reversed from vertical scrolling
|
|
||||||
LiSendHighResHScrollEvent(-translationDeltaX);
|
|
||||||
lastScrollTranslation = currentScrollTranslation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)mouseWheelMovedDiscrete:(UIPanGestureRecognizer *)gesture {
|
|
||||||
switch (gesture.state) {
|
|
||||||
case UIGestureRecognizerStateBegan:
|
|
||||||
case UIGestureRecognizerStateChanged:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UIGestureRecognizerStateEnded:
|
|
||||||
default:
|
|
||||||
// Ignore recognition failure and other states
|
|
||||||
lastScrollTranslation = CGPointMake(0, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using velocityInView is 0 for discrete scroll events
|
|
||||||
// when scrolling very slowly, but translationInView does work.
|
|
||||||
CGPoint currentScrollTranslation = [gesture translationInView:self];
|
|
||||||
|
|
||||||
{
|
|
||||||
short translationDeltaY = currentScrollTranslation.y - lastScrollTranslation.y;
|
|
||||||
if (translationDeltaY != 0) {
|
|
||||||
LiSendScrollEvent(translationDeltaY > 0 ? 1 : -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
short translationDeltaX = currentScrollTranslation.x - lastScrollTranslation.x;
|
|
||||||
if (translationDeltaX != 0) {
|
|
||||||
// Direction is reversed from vertical scrolling
|
|
||||||
LiSendHScrollEvent(translationDeltaX < 0 ? 1 : -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScrollTranslation = currentScrollTranslation;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
// Disable the 3 finger tap gestures that trigger the copy/paste/undo toolbar on iOS 13+
|
|
||||||
return gestureRecognizer.name == nil || ![gestureRecognizer.name hasPrefix:@"kbProductivity."];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
|
||||||
// This method is called when the "Return" key is pressed.
|
|
||||||
LiSendKeyboardEvent(0x0d, KEY_ACTION_DOWN, 0);
|
|
||||||
usleep(50 * 1000);
|
|
||||||
LiSendKeyboardEvent(0x0d, KEY_ACTION_UP, 0);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)textFieldDidEndEditing:(UITextField *)textField {
|
|
||||||
for (NSNumber* keyCode in keysDown) {
|
|
||||||
LiSendKeyboardEvent([keyCode shortValue], KEY_ACTION_UP, 0);
|
|
||||||
}
|
|
||||||
[keysDown removeAllObjects];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)onKeyboardPressed:(UITextField *)textField {
|
|
||||||
NSString* inputText = textField.text;
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
||||||
// If the text became empty, we know the user pressed the backspace key.
|
|
||||||
if ([inputText isEqual:@""]) {
|
|
||||||
LiSendKeyboardEvent(0x08, KEY_ACTION_DOWN, 0);
|
|
||||||
usleep(50 * 1000);
|
|
||||||
LiSendKeyboardEvent(0x08, KEY_ACTION_UP, 0);
|
|
||||||
} else {
|
|
||||||
// Character 0 will be our known sentinel value
|
|
||||||
|
|
||||||
// Check if any characters exist which can't be represented in a basic key event
|
|
||||||
for (int i = 1; i < [inputText length]; i++) {
|
|
||||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
|
|
||||||
if (event.keycode == 0) {
|
|
||||||
// We found an unknown key, so send the entire string as UTF-8
|
|
||||||
const char* utf8String = [inputText UTF8String];
|
|
||||||
|
|
||||||
// Skip the first character which is our sentinel
|
|
||||||
LiSendUtf8TextEvent(utf8String + 1, (int)strlen(utf8String) - 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We didn't find any unknown characters, so send them all as basic key events
|
|
||||||
for (int i = 1; i < [inputText length]; i++) {
|
|
||||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
|
|
||||||
assert(event.keycode != 0);
|
|
||||||
[self sendLowLevelEvent:event];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset text field back to known state
|
|
||||||
textField.text = @"0";
|
|
||||||
|
|
||||||
// Move the insertion point back to the end of the text box
|
|
||||||
UITextRange *textRange = [textField textRangeFromPosition:textField.endOfDocument toPosition:textField.endOfDocument];
|
|
||||||
[textField setSelectedTextRange:textRange];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)specialCharPressed:(UIKeyCommand *)cmd {
|
|
||||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:0x20 withModifierFlags:[cmd modifierFlags]];
|
|
||||||
event.keycode = [[dictCodes valueForKey:[cmd input]] intValue];
|
|
||||||
[self sendLowLevelEvent:event];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)keyPressed:(UIKeyCommand *)cmd {
|
|
||||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[[cmd input] characterAtIndex:0] withModifierFlags:[cmd modifierFlags]];
|
|
||||||
[self sendLowLevelEvent:event];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)sendLowLevelEvent:(struct KeyEvent)event {
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
||||||
// When we want to send a modified key (like uppercase letters) we need to send the
|
|
||||||
// modifier ("shift") seperately from the key itself.
|
|
||||||
if (event.modifier != 0) {
|
|
||||||
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_DOWN, event.modifier);
|
|
||||||
}
|
|
||||||
// Let the host know these are not (necessarily) normalized to US English scancodes
|
|
||||||
LiSendKeyboardEvent2(event.keycode, KEY_ACTION_DOWN, event.modifier, SS_KBE_FLAG_NON_NORMALIZED);
|
|
||||||
usleep(50 * 1000);
|
|
||||||
LiSendKeyboardEvent2(event.keycode, KEY_ACTION_UP, event.modifier, SS_KBE_FLAG_NON_NORMALIZED);
|
|
||||||
if (event.modifier != 0) {
|
|
||||||
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_UP, event.modifier);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)canBecomeFirstResponder {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSArray<UIKeyCommand *> *)keyCommands
|
|
||||||
{
|
|
||||||
NSString *charset = @"qwertyuiopasdfghjklzxcvbnm1234567890\t§[]\\'\"/.,`<>-´ç+`¡'º;ñ= ";
|
|
||||||
|
|
||||||
NSMutableArray<UIKeyCommand *> * commands = [NSMutableArray<UIKeyCommand *> array];
|
|
||||||
dictCodes = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: 0x0d], @"\r", [NSNumber numberWithInt: 0x08], @"\b", [NSNumber numberWithInt: 0x1b], UIKeyInputEscape, [NSNumber numberWithInt: 0x28], UIKeyInputDownArrow, [NSNumber numberWithInt: 0x26], UIKeyInputUpArrow, [NSNumber numberWithInt: 0x25], UIKeyInputLeftArrow, [NSNumber numberWithInt: 0x27], UIKeyInputRightArrow, nil];
|
|
||||||
|
|
||||||
[charset enumerateSubstringsInRange:NSMakeRange(0, charset.length)
|
|
||||||
options:NSStringEnumerationByComposedCharacterSequences
|
|
||||||
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:0 action:@selector(keyPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierShift action:@selector(keyPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierControl action:@selector(keyPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierAlternate action:@selector(keyPressed:)]];
|
|
||||||
}];
|
|
||||||
|
|
||||||
for (NSString *c in [dictCodes keyEnumerator]) {
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
||||||
modifierFlags:0
|
|
||||||
action:@selector(specialCharPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
||||||
modifierFlags:UIKeyModifierShift
|
|
||||||
action:@selector(specialCharPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
||||||
modifierFlags:UIKeyModifierShift | UIKeyModifierAlternate
|
|
||||||
action:@selector(specialCharPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
||||||
modifierFlags:UIKeyModifierShift | UIKeyModifierControl
|
|
||||||
action:@selector(specialCharPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
||||||
modifierFlags:UIKeyModifierControl
|
|
||||||
action:@selector(specialCharPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
||||||
modifierFlags:UIKeyModifierControl | UIKeyModifierAlternate
|
|
||||||
action:@selector(specialCharPressed:)]];
|
|
||||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
||||||
modifierFlags:UIKeyModifierAlternate
|
|
||||||
action:@selector(specialCharPressed:)]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)connectedStateDidChangeWithIdentifier:(NSUUID * _Nonnull)identifier isConnected:(BOOL)isConnected {
|
|
||||||
NSLog(@"Citrix X1 mouse state change: %@ -> %s",
|
|
||||||
identifier, isConnected ? "connected" : "disconnected");
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)mouseDidMoveWithIdentifier:(NSUUID * _Nonnull)identifier deltaX:(int16_t)deltaX deltaY:(int16_t)deltaY {
|
|
||||||
accumulatedMouseDeltaX += deltaX / X1_MOUSE_SPEED_DIVISOR;
|
|
||||||
accumulatedMouseDeltaY += deltaY / X1_MOUSE_SPEED_DIVISOR;
|
|
||||||
|
|
||||||
short shortX = (short)accumulatedMouseDeltaX;
|
|
||||||
short shortY = (short)accumulatedMouseDeltaY;
|
|
||||||
|
|
||||||
if (shortX == 0 && shortY == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LiSendMouseMoveEvent(shortX, shortY);
|
|
||||||
|
|
||||||
accumulatedMouseDeltaX -= shortX;
|
|
||||||
accumulatedMouseDeltaY -= shortY;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (int) buttonFromX1ButtonCode:(enum X1MouseButton)button {
|
|
||||||
switch (button) {
|
|
||||||
case X1MouseButtonLeft:
|
|
||||||
return BUTTON_LEFT;
|
|
||||||
case X1MouseButtonRight:
|
|
||||||
return BUTTON_RIGHT;
|
|
||||||
case X1MouseButtonMiddle:
|
|
||||||
return BUTTON_MIDDLE;
|
|
||||||
default:
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)mouseDownWithIdentifier:(NSUUID * _Nonnull)identifier button:(enum X1MouseButton)button {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, [self buttonFromX1ButtonCode:button]);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)mouseUpWithIdentifier:(NSUUID * _Nonnull)identifier button:(enum X1MouseButton)button {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, [self buttonFromX1ButtonCode:button]);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)wheelDidScrollWithIdentifier:(NSUUID * _Nonnull)identifier deltaZ:(int8_t)deltaZ {
|
|
||||||
LiSendScrollEvent(deltaZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
- (BOOL)isMultipleTouchEnabled {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
#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,34 +0,0 @@
|
|||||||
//
|
|
||||||
// Connection.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 1/19/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "VideoDecoderRenderer.h"
|
|
||||||
#import "StreamConfiguration.h"
|
|
||||||
|
|
||||||
#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
CFTimeInterval startTime;
|
|
||||||
CFTimeInterval endTime;
|
|
||||||
int totalFrames;
|
|
||||||
int receivedFrames;
|
|
||||||
int networkDroppedFrames;
|
|
||||||
int totalHostProcessingLatency;
|
|
||||||
int framesWithHostProcessingLatency;
|
|
||||||
int maxHostProcessingLatency;
|
|
||||||
int minHostProcessingLatency;
|
|
||||||
} video_stats_t;
|
|
||||||
|
|
||||||
@interface Connection : NSOperation <NSStreamDelegate>
|
|
||||||
|
|
||||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
|
|
||||||
-(void) terminate;
|
|
||||||
-(void) main;
|
|
||||||
-(BOOL) getVideoStats:(video_stats_t*)stats;
|
|
||||||
-(NSString*) getActiveCodecName;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,488 +0,0 @@
|
|||||||
//
|
|
||||||
// Connection.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 1/19/14.
|
|
||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "Connection.h"
|
|
||||||
#import "Utils.h"
|
|
||||||
|
|
||||||
#import <VideoToolbox/VideoToolbox.h>
|
|
||||||
|
|
||||||
#define SDL_MAIN_HANDLED
|
|
||||||
#import <SDL.h>
|
|
||||||
|
|
||||||
#include "Limelight.h"
|
|
||||||
#include "opus_multistream.h"
|
|
||||||
|
|
||||||
@implementation Connection {
|
|
||||||
SERVER_INFORMATION _serverInfo;
|
|
||||||
STREAM_CONFIGURATION _streamConfig;
|
|
||||||
CONNECTION_LISTENER_CALLBACKS _clCallbacks;
|
|
||||||
DECODER_RENDERER_CALLBACKS _drCallbacks;
|
|
||||||
AUDIO_RENDERER_CALLBACKS _arCallbacks;
|
|
||||||
char _hostString[256];
|
|
||||||
char _appVersionString[32];
|
|
||||||
char _gfeVersionString[32];
|
|
||||||
char _rtspSessionUrl[128];
|
|
||||||
}
|
|
||||||
|
|
||||||
static NSLock* initLock;
|
|
||||||
static OpusMSDecoder* opusDecoder;
|
|
||||||
static id<ConnectionCallbacks> _callbacks;
|
|
||||||
static int lastFrameNumber;
|
|
||||||
static int activeVideoFormat;
|
|
||||||
static video_stats_t currentVideoStats;
|
|
||||||
static video_stats_t lastVideoStats;
|
|
||||||
static NSLock* videoStatsLock;
|
|
||||||
|
|
||||||
static SDL_AudioDeviceID audioDevice;
|
|
||||||
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
|
|
||||||
static void* audioBuffer;
|
|
||||||
static int audioFrameSize;
|
|
||||||
|
|
||||||
static VideoDecoderRenderer* renderer;
|
|
||||||
|
|
||||||
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
|
|
||||||
{
|
|
||||||
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
|
|
||||||
lastFrameNumber = 0;
|
|
||||||
activeVideoFormat = videoFormat;
|
|
||||||
memset(¤tVideoStats, 0, sizeof(currentVideoStats));
|
|
||||||
memset(&lastVideoStats, 0, sizeof(lastVideoStats));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DrStart(void)
|
|
||||||
{
|
|
||||||
[renderer start];
|
|
||||||
}
|
|
||||||
|
|
||||||
void DrStop(void)
|
|
||||||
{
|
|
||||||
[renderer stop];
|
|
||||||
}
|
|
||||||
|
|
||||||
-(BOOL) getVideoStats:(video_stats_t*)stats
|
|
||||||
{
|
|
||||||
// We return lastVideoStats because it is a complete 1 second window
|
|
||||||
[videoStatsLock lock];
|
|
||||||
if (lastVideoStats.endTime != 0) {
|
|
||||||
memcpy(stats, &lastVideoStats, sizeof(*stats));
|
|
||||||
[videoStatsLock unlock];
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No stats yet
|
|
||||||
[videoStatsLock unlock];
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
-(NSString*) getActiveCodecName
|
|
||||||
{
|
|
||||||
switch (activeVideoFormat)
|
|
||||||
{
|
|
||||||
case VIDEO_FORMAT_H264:
|
|
||||||
return @"H.264";
|
|
||||||
case VIDEO_FORMAT_H265:
|
|
||||||
return @"HEVC";
|
|
||||||
case VIDEO_FORMAT_H265_MAIN10:
|
|
||||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
|
||||||
return @"HEVC Main 10 HDR";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return @"HEVC Main 10 SDR";
|
|
||||||
}
|
|
||||||
case VIDEO_FORMAT_AV1_MAIN8:
|
|
||||||
return @"AV1";
|
|
||||||
case VIDEO_FORMAT_AV1_MAIN10:
|
|
||||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
|
||||||
return @"AV1 10-bit HDR";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return @"AV1 10-bit SDR";
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return @"UNKNOWN";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
|
||||||
{
|
|
||||||
int offset = 0;
|
|
||||||
int ret;
|
|
||||||
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
|
|
||||||
if (data == NULL) {
|
|
||||||
// A frame was lost due to OOM condition
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
CFTimeInterval now = CACurrentMediaTime();
|
|
||||||
if (!lastFrameNumber) {
|
|
||||||
currentVideoStats.startTime = now;
|
|
||||||
lastFrameNumber = decodeUnit->frameNumber;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Flip stats roughly every second
|
|
||||||
if (now - currentVideoStats.startTime >= 1.0f) {
|
|
||||||
currentVideoStats.endTime = now;
|
|
||||||
|
|
||||||
[videoStatsLock lock];
|
|
||||||
lastVideoStats = currentVideoStats;
|
|
||||||
[videoStatsLock unlock];
|
|
||||||
|
|
||||||
memset(¤tVideoStats, 0, sizeof(currentVideoStats));
|
|
||||||
currentVideoStats.startTime = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
|
|
||||||
currentVideoStats.networkDroppedFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
|
|
||||||
currentVideoStats.totalFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
|
|
||||||
lastFrameNumber = decodeUnit->frameNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decodeUnit->frameHostProcessingLatency != 0) {
|
|
||||||
if (currentVideoStats.minHostProcessingLatency == 0 || decodeUnit->frameHostProcessingLatency < currentVideoStats.minHostProcessingLatency) {
|
|
||||||
currentVideoStats.minHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decodeUnit->frameHostProcessingLatency > currentVideoStats.maxHostProcessingLatency) {
|
|
||||||
currentVideoStats.maxHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentVideoStats.framesWithHostProcessingLatency++;
|
|
||||||
currentVideoStats.totalHostProcessingLatency += decodeUnit->frameHostProcessingLatency;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentVideoStats.receivedFrames++;
|
|
||||||
currentVideoStats.totalFrames++;
|
|
||||||
|
|
||||||
PLENTRY entry = decodeUnit->bufferList;
|
|
||||||
while (entry != NULL) {
|
|
||||||
// Submit parameter set NALUs directly since no copy is required by the decoder
|
|
||||||
if (entry->bufferType != BUFFER_TYPE_PICDATA) {
|
|
||||||
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
|
|
||||||
length:entry->length
|
|
||||||
bufferType:entry->bufferType
|
|
||||||
decodeUnit:decodeUnit];
|
|
||||||
if (ret != DR_OK) {
|
|
||||||
free(data);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
memcpy(&data[offset], entry->data, entry->length);
|
|
||||||
offset += entry->length;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = entry->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function will take our picture data buffer
|
|
||||||
return [renderer submitDecodeBuffer:data
|
|
||||||
length:offset
|
|
||||||
bufferType:BUFFER_TYPE_PICDATA
|
|
||||||
decodeUnit:decodeUnit];
|
|
||||||
}
|
|
||||||
|
|
||||||
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
|
|
||||||
{
|
|
||||||
int err;
|
|
||||||
SDL_AudioSpec want, have;
|
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
|
|
||||||
Log(LOG_E, @"Failed to initialize audio subsystem: %s\n", SDL_GetError());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_zero(want);
|
|
||||||
want.freq = opusConfig->sampleRate;
|
|
||||||
want.format = AUDIO_S16;
|
|
||||||
want.channels = opusConfig->channelCount;
|
|
||||||
want.samples = opusConfig->samplesPerFrame;
|
|
||||||
|
|
||||||
audioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
|
||||||
if (audioDevice == 0) {
|
|
||||||
Log(LOG_E, @"Failed to open audio device: %s\n", SDL_GetError());
|
|
||||||
ArCleanup();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
audioConfig = *opusConfig;
|
|
||||||
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
|
||||||
audioBuffer = SDL_malloc(audioFrameSize);
|
|
||||||
if (audioBuffer == NULL) {
|
|
||||||
Log(LOG_E, @"Failed to allocate audio frame buffer");
|
|
||||||
ArCleanup();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
|
||||||
opusConfig->channelCount,
|
|
||||||
opusConfig->streams,
|
|
||||||
opusConfig->coupledStreams,
|
|
||||||
opusConfig->mapping,
|
|
||||||
&err);
|
|
||||||
if (opusDecoder == NULL) {
|
|
||||||
Log(LOG_E, @"Failed to create Opus decoder");
|
|
||||||
ArCleanup();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start playback
|
|
||||||
SDL_PauseAudioDevice(audioDevice, 0);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ArCleanup(void)
|
|
||||||
{
|
|
||||||
if (opusDecoder != NULL) {
|
|
||||||
opus_multistream_decoder_destroy(opusDecoder);
|
|
||||||
opusDecoder = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioDevice != 0) {
|
|
||||||
SDL_CloseAudioDevice(audioDevice);
|
|
||||||
audioDevice = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioBuffer != NULL) {
|
|
||||||
SDL_free(audioBuffer);
|
|
||||||
audioBuffer = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|
||||||
{
|
|
||||||
int decodeLen;
|
|
||||||
|
|
||||||
// Don't queue if there's already more than 30 ms of audio data waiting
|
|
||||||
// in Moonlight's audio queue.
|
|
||||||
if (LiGetPendingAudioDuration() > 30) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
|
|
||||||
(short*)audioBuffer, audioConfig.samplesPerFrame, 0);
|
|
||||||
if (decodeLen > 0) {
|
|
||||||
// Provide backpressure on the queue to ensure too many frames don't build up
|
|
||||||
// in SDL's audio queue.
|
|
||||||
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
|
|
||||||
SDL_Delay(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SDL_QueueAudio(audioDevice,
|
|
||||||
audioBuffer,
|
|
||||||
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
|
|
||||||
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClStageStarting(int stage)
|
|
||||||
{
|
|
||||||
[_callbacks stageStarting:LiGetStageName(stage)];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClStageComplete(int stage)
|
|
||||||
{
|
|
||||||
[_callbacks stageComplete:LiGetStageName(stage)];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClStageFailed(int stage, int errorCode)
|
|
||||||
{
|
|
||||||
[_callbacks stageFailed:LiGetStageName(stage) withError:errorCode portTestFlags:LiGetPortFlagsFromStage(stage)];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClConnectionStarted(void)
|
|
||||||
{
|
|
||||||
[_callbacks connectionStarted];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClConnectionTerminated(int errorCode)
|
|
||||||
{
|
|
||||||
[_callbacks connectionTerminated: errorCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClLogMessage(const char* format, ...)
|
|
||||||
{
|
|
||||||
va_list va;
|
|
||||||
va_start(va, format);
|
|
||||||
vfprintf(stderr, format, va);
|
|
||||||
va_end(va);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
|
||||||
{
|
|
||||||
[_callbacks rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClConnectionStatusUpdate(int status)
|
|
||||||
{
|
|
||||||
[_callbacks connectionStatusUpdate:status];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClSetHdrMode(bool enabled)
|
|
||||||
{
|
|
||||||
[renderer setHdrMode:enabled];
|
|
||||||
[_callbacks setHdrMode:enabled];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClRumbleTriggers(uint16_t controllerNumber, uint16_t leftTriggerMotor, uint16_t rightTriggerMotor)
|
|
||||||
{
|
|
||||||
[_callbacks rumbleTriggers:controllerNumber leftTrigger:leftTriggerMotor rightTrigger:rightTriggerMotor];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz)
|
|
||||||
{
|
|
||||||
[_callbacks setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b)
|
|
||||||
{
|
|
||||||
[_callbacks setControllerLed:controllerNumber r:r g:g b:b];
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void) terminate
|
|
||||||
{
|
|
||||||
// Interrupt any action blocking LiStartConnection(). This is
|
|
||||||
// thread-safe and done outside initLock on purpose, since we
|
|
||||||
// won't be able to acquire it if LiStartConnection is in
|
|
||||||
// progress.
|
|
||||||
LiInterruptConnection();
|
|
||||||
|
|
||||||
// We dispatch this async to get out because this can be invoked
|
|
||||||
// on a thread inside common and we don't want to deadlock. It also avoids
|
|
||||||
// blocking on the caller's thread waiting to acquire initLock.
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
||||||
[initLock lock];
|
|
||||||
LiStopConnection();
|
|
||||||
[initLock unlock];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks
|
|
||||||
{
|
|
||||||
self = [super init];
|
|
||||||
|
|
||||||
// Use a lock to ensure that only one thread is initializing
|
|
||||||
// or deinitializing a connection at a time.
|
|
||||||
if (initLock == nil) {
|
|
||||||
initLock = [[NSLock alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoStatsLock == nil) {
|
|
||||||
videoStatsLock = [[NSLock alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *rawAddress = [Utils addressPortStringToAddress:config.host];
|
|
||||||
strncpy(_hostString,
|
|
||||||
[rawAddress cStringUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
sizeof(_hostString) - 1);
|
|
||||||
strncpy(_appVersionString,
|
|
||||||
[config.appVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
sizeof(_appVersionString) - 1);
|
|
||||||
if (config.gfeVersion != nil) {
|
|
||||||
strncpy(_gfeVersionString,
|
|
||||||
[config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
sizeof(_gfeVersionString) - 1);
|
|
||||||
}
|
|
||||||
if (config.rtspSessionUrl != nil) {
|
|
||||||
strncpy(_rtspSessionUrl,
|
|
||||||
[config.rtspSessionUrl cStringUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
sizeof(_rtspSessionUrl) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
LiInitializeServerInformation(&_serverInfo);
|
|
||||||
_serverInfo.address = _hostString;
|
|
||||||
_serverInfo.serverInfoAppVersion = _appVersionString;
|
|
||||||
if (config.gfeVersion != nil) {
|
|
||||||
_serverInfo.serverInfoGfeVersion = _gfeVersionString;
|
|
||||||
}
|
|
||||||
if (config.rtspSessionUrl != nil) {
|
|
||||||
_serverInfo.rtspSessionUrl = _rtspSessionUrl;
|
|
||||||
}
|
|
||||||
_serverInfo.serverCodecModeSupport = config.serverCodecModeSupport;
|
|
||||||
|
|
||||||
renderer = myRenderer;
|
|
||||||
_callbacks = callbacks;
|
|
||||||
|
|
||||||
LiInitializeStreamConfiguration(&_streamConfig);
|
|
||||||
_streamConfig.width = config.width;
|
|
||||||
_streamConfig.height = config.height;
|
|
||||||
_streamConfig.fps = config.frameRate;
|
|
||||||
_streamConfig.bitrate = config.bitRate;
|
|
||||||
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
|
|
||||||
_streamConfig.audioConfiguration = config.audioConfiguration;
|
|
||||||
|
|
||||||
// Since we require iOS 12 or above, we're guaranteed to be running
|
|
||||||
// on a 64-bit device with ARMv8 crypto instructions, so we don't
|
|
||||||
// need to check for that here.
|
|
||||||
_streamConfig.encryptionFlags = ENCFLG_ALL;
|
|
||||||
|
|
||||||
if ([Utils isActiveNetworkVPN]) {
|
|
||||||
// Force remote streaming mode when a VPN is connected
|
|
||||||
_streamConfig.streamingRemotely = STREAM_CFG_REMOTE;
|
|
||||||
_streamConfig.packetSize = 1024;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Detect remote streaming automatically based on the IP address of the target
|
|
||||||
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
|
||||||
_streamConfig.packetSize = 1392;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(_streamConfig.remoteInputAesKey, [config.riKey bytes], [config.riKey length]);
|
|
||||||
memset(_streamConfig.remoteInputAesIv, 0, 16);
|
|
||||||
int riKeyId = htonl(config.riKeyId);
|
|
||||||
memcpy(_streamConfig.remoteInputAesIv, &riKeyId, sizeof(riKeyId));
|
|
||||||
|
|
||||||
LiInitializeVideoCallbacks(&_drCallbacks);
|
|
||||||
_drCallbacks.setup = DrDecoderSetup;
|
|
||||||
_drCallbacks.start = DrStart;
|
|
||||||
_drCallbacks.stop = DrStop;
|
|
||||||
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
|
|
||||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
|
|
||||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
|
|
||||||
|
|
||||||
LiInitializeAudioCallbacks(&_arCallbacks);
|
|
||||||
_arCallbacks.init = ArInit;
|
|
||||||
_arCallbacks.cleanup = ArCleanup;
|
|
||||||
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
|
|
||||||
_arCallbacks.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
|
||||||
|
|
||||||
LiInitializeConnectionCallbacks(&_clCallbacks);
|
|
||||||
_clCallbacks.stageStarting = ClStageStarting;
|
|
||||||
_clCallbacks.stageComplete = ClStageComplete;
|
|
||||||
_clCallbacks.stageFailed = ClStageFailed;
|
|
||||||
_clCallbacks.connectionStarted = ClConnectionStarted;
|
|
||||||
_clCallbacks.connectionTerminated = ClConnectionTerminated;
|
|
||||||
_clCallbacks.logMessage = ClLogMessage;
|
|
||||||
_clCallbacks.rumble = ClRumble;
|
|
||||||
_clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate;
|
|
||||||
_clCallbacks.setHdrMode = ClSetHdrMode;
|
|
||||||
_clCallbacks.rumbleTriggers = ClRumbleTriggers;
|
|
||||||
_clCallbacks.setMotionEventState = ClSetMotionEventState;
|
|
||||||
_clCallbacks.setControllerLED = ClSetControllerLED;
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void) main
|
|
||||||
{
|
|
||||||
[initLock lock];
|
|
||||||
LiStartConnection(&_serverInfo,
|
|
||||||
&_streamConfig,
|
|
||||||
&_clCallbacks,
|
|
||||||
&_drCallbacks,
|
|
||||||
&_arCallbacks,
|
|
||||||
NULL, 0,
|
|
||||||
NULL, 0);
|
|
||||||
[initLock unlock];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -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,28 +0,0 @@
|
|||||||
//
|
|
||||||
// UIAppView.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 10/22/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import "TemporaryApp.h"
|
|
||||||
|
|
||||||
@protocol AppCallback <NSObject>
|
|
||||||
|
|
||||||
- (void) appClicked:(TemporaryApp*)app view:(UIView*)view;
|
|
||||||
- (void) appLongClicked:(TemporaryApp*)app view:(UIView*)view;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
@interface UIAppView : UIButton <UIContextMenuInteractionDelegate>
|
|
||||||
#else
|
|
||||||
@interface UIAppView : UIButton
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback;
|
|
||||||
- (void) updateAppImage;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
//
|
|
||||||
// UIAppView.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 10/22/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "UIAppView.h"
|
|
||||||
#import "AppAssetManager.h"
|
|
||||||
|
|
||||||
static const float REFRESH_CYCLE = 1.0f;
|
|
||||||
|
|
||||||
@implementation UIAppView {
|
|
||||||
TemporaryApp* _app;
|
|
||||||
UILabel* _appLabel;
|
|
||||||
UIImageView* _appOverlay;
|
|
||||||
UIImageView* _appImage;
|
|
||||||
NSCache* _artCache;
|
|
||||||
id<AppCallback> _callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
static UIImage* noImage;
|
|
||||||
|
|
||||||
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback {
|
|
||||||
self = [super init];
|
|
||||||
_app = app;
|
|
||||||
_callback = callback;
|
|
||||||
_artCache = cache;
|
|
||||||
|
|
||||||
// Cache the NoAppImage ourselves to avoid
|
|
||||||
// having to load it each time
|
|
||||||
if (noImage == nil) {
|
|
||||||
noImage = [UIImage imageNamed:@"NoAppImage"];
|
|
||||||
}
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
self.frame = CGRectMake(0, 0, 200, 265);
|
|
||||||
#else
|
|
||||||
self.frame = CGRectMake(0, 0, 150, 200);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
[self setAlpha:app.hidden ? 0.4 : 1.0];
|
|
||||||
|
|
||||||
_appImage = [[UIImageView alloc] initWithFrame:self.frame];
|
|
||||||
[_appImage setImage:noImage];
|
|
||||||
[self addSubview:_appImage];
|
|
||||||
|
|
||||||
// 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(appLongClicked:)];
|
|
||||||
[self addGestureRecognizer:longPressRecognizer];
|
|
||||||
}
|
|
||||||
|
|
||||||
[self addTarget:self action:@selector(appClicked:) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
|
|
||||||
[self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown];
|
|
||||||
[self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
_appImage.adjustsImageWhenAncestorFocused = YES;
|
|
||||||
#else
|
|
||||||
// Rasterizing the cell layer increases rendering performance by quite a bit
|
|
||||||
// but we want it unrasterized for tvOS where it must be scaled.
|
|
||||||
self.layer.shouldRasterize = YES;
|
|
||||||
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
|
|
||||||
|
|
||||||
if (@available(iOS 13.4.1, *)) {
|
|
||||||
// Allow the button style to change when moused over
|
|
||||||
self.pointerInteractionEnabled = YES;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
[self updateAppImage];
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)didMoveToSuperview {
|
|
||||||
// Start our update loop when we are added to our cell
|
|
||||||
if (self.superview != nil) {
|
|
||||||
[self updateLoop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) appClicked:(UIView *)view {
|
|
||||||
[_callback appClicked:_app view:view];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) appLongClicked:(UILongPressGestureRecognizer*)gesture {
|
|
||||||
if (gesture.state == UIGestureRecognizerStateBegan) {
|
|
||||||
[_callback appLongClicked:_app 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 appLongClicked:_app view:self];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (void) updateAppImage {
|
|
||||||
if (_appOverlay != nil) {
|
|
||||||
[_appOverlay removeFromSuperview];
|
|
||||||
_appOverlay = nil;
|
|
||||||
}
|
|
||||||
if (_appLabel != nil) {
|
|
||||||
[_appLabel removeFromSuperview];
|
|
||||||
_appLabel = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL noAppImage = false;
|
|
||||||
|
|
||||||
// First check the memory cache
|
|
||||||
UIImage* appImage = [_artCache objectForKey:_app];
|
|
||||||
if (appImage == nil) {
|
|
||||||
// Next try to load from the on disk cache
|
|
||||||
appImage = [UIImage imageWithContentsOfFile:[AppAssetManager boxArtPathForApp:_app]];
|
|
||||||
if (appImage != nil) {
|
|
||||||
[_artCache setObject:appImage forKey:_app];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appImage != nil) {
|
|
||||||
// This size of image might be blank image received from GameStream.
|
|
||||||
// TODO: Improve no-app image detection
|
|
||||||
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
|
|
||||||
!(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
|
|
||||||
[_appImage setImage:appImage];
|
|
||||||
} else {
|
|
||||||
noAppImage = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
noAppImage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([_app.id isEqualToString:_app.host.currentGame]) {
|
|
||||||
// Only create the app overlay if needed
|
|
||||||
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
|
|
||||||
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
|
|
||||||
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
|
|
||||||
_appOverlay.layer.shadowOpacity = 1;
|
|
||||||
_appOverlay.layer.shadowRadius = 4.0;
|
|
||||||
_appOverlay.contentMode = UIViewContentModeScaleAspectFit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noAppImage) {
|
|
||||||
_appLabel = [[UILabel alloc] init];
|
|
||||||
[_appLabel setTextColor:[UIColor whiteColor]];
|
|
||||||
[_appLabel setText:_app.name];
|
|
||||||
[_appLabel setFont:[UIFont systemFontOfSize:24]];
|
|
||||||
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
|
|
||||||
[_appLabel setTextAlignment:NSTextAlignmentCenter];
|
|
||||||
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
|
|
||||||
[_appLabel setNumberOfLines:0];
|
|
||||||
}
|
|
||||||
|
|
||||||
[self positionSubviews];
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
[_appImage.overlayContentView addSubview:_appLabel];
|
|
||||||
[_appImage.overlayContentView addSubview:_appOverlay];
|
|
||||||
#else
|
|
||||||
[self addSubview:_appLabel];
|
|
||||||
[self addSubview:_appOverlay];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) buttonSelected:(id)sender {
|
|
||||||
_appImage.layer.opacity = 0.5f;
|
|
||||||
}
|
|
||||||
- (void) buttonDeselected:(id)sender {
|
|
||||||
_appImage.layer.opacity = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) positionSubviews {
|
|
||||||
CGFloat padding = 5.f;
|
|
||||||
CGSize frameSize = _appImage.frame.size;
|
|
||||||
CGPoint center = _appImage.center;
|
|
||||||
|
|
||||||
if (_appLabel != nil) {
|
|
||||||
if (_appOverlay != nil) {
|
|
||||||
_appOverlay.frame = CGRectMake(0, 0, frameSize.width / 3, frameSize.width / 3);
|
|
||||||
_appOverlay.center = CGPointMake(frameSize.width / 2, padding + _appOverlay.frame.size.height / 2);
|
|
||||||
|
|
||||||
[_appLabel setFrame:CGRectMake(padding, _appOverlay.frame.size.height + padding, frameSize.width - 2 * padding, frameSize.height - _appOverlay.frame.size.height - 2 * padding)];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[_appLabel setFrame:CGRectMake(padding, padding, frameSize.width - 2 * padding, frameSize.height - 2 * padding)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (_appOverlay != nil) {
|
|
||||||
_appOverlay.frame = CGRectMake(0, 0, frameSize.width / 2, frameSize.width / 2);
|
|
||||||
_appOverlay.center = center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateLoop {
|
|
||||||
// Stop immediately if the view has been detached
|
|
||||||
if (self.superview == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the app image if neccessary
|
|
||||||
if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
|
|
||||||
(_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
|
|
||||||
[self updateAppImage];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show no shadow for hidden apps. Because we adjust the opacity of the
|
|
||||||
// cells for hidden apps, it makes them look bad when the shadow draws
|
|
||||||
// through the app tile.
|
|
||||||
self.superview.layer.shadowOpacity = _app.hidden ? 0.0f : 0.5f;
|
|
||||||
|
|
||||||
// Update opacity if neccessary
|
|
||||||
[self setAlpha:_app.hidden ? 0.4 : 1.0];
|
|
||||||
|
|
||||||
// Queue the next refresh cycle
|
|
||||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 <UIKit/UIKit.h>
|
||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
|
#import "CustomOSCViewControl/LayoutOnScreenControlsViewController.h"
|
||||||
|
#import "MainFrameViewController.h"
|
||||||
|
#import "CustomEdgeSlideGestureRecognizer.h"
|
||||||
|
|
||||||
@interface SettingsViewController : UIViewController
|
@interface SettingsViewController : UIViewController
|
||||||
@property (strong, nonatomic) IBOutlet UILabel *bitrateLabel;
|
@property (strong, nonatomic) IBOutlet UILabel *bitrateLabel;
|
||||||
@@ -15,18 +18,50 @@
|
|||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UIView *resolutionDisplayView;
|
@property (strong, nonatomic) IBOutlet UIView *resolutionDisplayView;
|
||||||
|
@property (strong, nonatomic) IBOutlet UILabel *touchModeLabel;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UILabel *onscreenControllerLabel;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
|
@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 *optimizeSettingsSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *swapABXYButtonsSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *swapABXYButtonsSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
|
@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 *hdrSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
|
||||||
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
|
@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 push
|
||||||
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
||||||
@@ -37,5 +72,8 @@
|
|||||||
#pragma clang diagnostic pop
|
#pragma clang diagnostic pop
|
||||||
|
|
||||||
- (void) saveSettings;
|
- (void) saveSettings;
|
||||||
|
+ (bool) isLandscapeNow;
|
||||||
|
- (void)updateResolutionTable;
|
||||||
|
- (void) widget:(UISlider*)widget setEnabled:(bool)enabled;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -10,17 +10,23 @@
|
|||||||
#import "TemporarySettings.h"
|
#import "TemporarySettings.h"
|
||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
|
|
||||||
|
#import <UIKit/UIGestureRecognizerSubclass.h>
|
||||||
#import <VideoToolbox/VideoToolbox.h>
|
#import <VideoToolbox/VideoToolbox.h>
|
||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
|
||||||
|
#import "LocalizationHelper.h"
|
||||||
|
|
||||||
@implementation SettingsViewController {
|
@implementation SettingsViewController {
|
||||||
NSInteger _bitrate;
|
NSInteger _bitrate;
|
||||||
NSInteger _lastSelectedResolutionIndex;
|
NSInteger _lastSelectedResolutionIndex;
|
||||||
|
bool justEnteredSettingsViewDoNotOpenOscLayoutTool;
|
||||||
|
uint16_t oscLayoutFingers;
|
||||||
|
CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@dynamic overrideUserInterfaceStyle;
|
@dynamic overrideUserInterfaceStyle;
|
||||||
|
|
||||||
static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
|
//static NSString* bitrateFormat;
|
||||||
static const int bitrateTable[] = {
|
static const int bitrateTable[] = {
|
||||||
500,
|
500,
|
||||||
1000,
|
1000,
|
||||||
@@ -35,19 +41,94 @@ static const int bitrateTable[] = {
|
|||||||
8000,
|
8000,
|
||||||
9000,
|
9000,
|
||||||
10000,
|
10000,
|
||||||
|
11000,
|
||||||
12000,
|
12000,
|
||||||
|
13000,
|
||||||
|
14000,
|
||||||
15000,
|
15000,
|
||||||
|
16000,
|
||||||
|
17000,
|
||||||
18000,
|
18000,
|
||||||
|
19000,
|
||||||
20000,
|
20000,
|
||||||
|
21000,
|
||||||
|
22000,
|
||||||
|
23000,
|
||||||
|
24000,
|
||||||
|
25000,
|
||||||
|
26000,
|
||||||
|
27000,
|
||||||
|
28000,
|
||||||
|
29000,
|
||||||
30000,
|
30000,
|
||||||
|
31000,
|
||||||
|
32000,
|
||||||
|
33000,
|
||||||
|
34000,
|
||||||
|
35000,
|
||||||
|
36000,
|
||||||
|
37000,
|
||||||
|
38000,
|
||||||
|
39000,
|
||||||
40000,
|
40000,
|
||||||
|
41000,
|
||||||
|
42000,
|
||||||
|
43000,
|
||||||
|
44000,
|
||||||
|
45000,
|
||||||
|
46000,
|
||||||
|
47000,
|
||||||
|
48000,
|
||||||
|
49000,
|
||||||
50000,
|
50000,
|
||||||
|
50000,
|
||||||
|
51000,
|
||||||
|
52000,
|
||||||
|
53000,
|
||||||
|
54000,
|
||||||
|
55000,
|
||||||
|
56000,
|
||||||
|
57000,
|
||||||
|
58000,
|
||||||
|
59000,
|
||||||
60000,
|
60000,
|
||||||
|
61000,
|
||||||
|
62000,
|
||||||
|
63000,
|
||||||
|
64000,
|
||||||
|
65000,
|
||||||
|
66000,
|
||||||
|
67000,
|
||||||
|
68000,
|
||||||
|
69000,
|
||||||
70000,
|
70000,
|
||||||
80000,
|
80000,
|
||||||
|
90000,
|
||||||
100000,
|
100000,
|
||||||
|
110000,
|
||||||
120000,
|
120000,
|
||||||
|
130000,
|
||||||
|
140000,
|
||||||
150000,
|
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;
|
const int RESOLUTION_TABLE_SIZE = 7;
|
||||||
@@ -118,7 +199,8 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < RESOLUTION_TABLE_CUSTOM_INDEX; i++) {
|
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;
|
return NO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,8 +208,153 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (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 {
|
- (void)viewDidLoad {
|
||||||
[super 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
|
// Always run settings in dark mode because we want the light fonts
|
||||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||||
@@ -159,6 +386,7 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
|
resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
|
||||||
resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
|
resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
|
||||||
resolutionTable[6] = CGSizeMake([currentSettings.width integerValue], [currentSettings.height integerValue]); // custom initial value
|
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
|
// Don't populate the custom entry unless we have a custom resolution
|
||||||
if (!isCustomResolution(resolutionTable[6])) {
|
if (!isCustomResolution(resolutionTable[6])) {
|
||||||
@@ -179,13 +407,9 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSInteger resolution = 1;
|
NSInteger resolution = currentSettings.resolutionSelected.integerValue;
|
||||||
for (int i = 0; i < RESOLUTION_TABLE_SIZE; i++) {
|
if(resolution >= RESOLUTION_TABLE_SIZE){
|
||||||
if ((int) resolutionTable[i].height == [currentSettings.height intValue]
|
resolution = 0;
|
||||||
&& (int) resolutionTable[i].width == [currentSettings.width intValue]) {
|
|
||||||
resolution = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show the 120 FPS option if we have a > 60-ish Hz display
|
// 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)) {
|
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || !(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10)) {
|
||||||
[self.hdrSelector removeAllSegments];
|
[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];
|
[self.hdrSelector setEnabled:NO];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
[self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0];
|
[self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
[self.touchModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
|
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
|
||||||
[self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged];
|
[self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0];
|
||||||
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlay ? 1 : 0];
|
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
|
||||||
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
|
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
|
||||||
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
||||||
[self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
|
[self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
|
||||||
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
|
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
|
||||||
[self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
|
[self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
|
||||||
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
|
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
|
||||||
NSInteger onscreenControls = [currentSettings.onscreenControls integerValue];
|
|
||||||
_lastSelectedResolutionIndex = resolution;
|
_lastSelectedResolutionIndex = resolution;
|
||||||
[self.resolutionSelector setSelectedSegmentIndex:resolution];
|
[self.resolutionSelector setSelectedSegmentIndex:resolution];
|
||||||
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
|
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
|
||||||
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
||||||
[self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
|
[self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
|
||||||
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
|
NSInteger motionMode = [currentSettings.motionMode integerValue];
|
||||||
[self.onscreenControlSelector setEnabled:!currentSettings.absoluteTouchMode];
|
[self.motionModeSelector setSelectedSegmentIndex:motionMode];
|
||||||
[self.bitrateSlider setMinimumValue:0];
|
[self.bitrateSlider setMinimumValue:0];
|
||||||
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
||||||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||||||
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
||||||
[self updateBitrateText];
|
[self updateBitrateText];
|
||||||
[self updateResolutionDisplayViewText];
|
[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 {
|
- (void) touchModeChanged {
|
||||||
// Disable on-screen controls in absolute touch mode
|
// Disable On-Screen Controls & Widgets in non-relative touch mode
|
||||||
[self.onscreenControlSelector setEnabled:[self.touchModeSelector selectedSegmentIndex] == 0];
|
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 {
|
- (void) updateBitrate {
|
||||||
@@ -342,13 +845,14 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
[self updateResolutionDisplayViewText];
|
[self updateResolutionDisplayViewText];
|
||||||
_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
|
_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
|
||||||
}
|
}
|
||||||
|
[self updateResolutionTable];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) promptCustomResolutionDialog {
|
- (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) {
|
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||||
textField.placeholder = @"Video Width";
|
textField.placeholder = [LocalizationHelper localizedStringForKey:@"Video Width"];
|
||||||
textField.clearButtonMode = UITextFieldViewModeAlways;
|
textField.clearButtonMode = UITextFieldViewModeAlways;
|
||||||
textField.borderStyle = UITextBorderStyleRoundedRect;
|
textField.borderStyle = UITextBorderStyleRoundedRect;
|
||||||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
textField.keyboardType = UIKeyboardTypeNumberPad;
|
||||||
@@ -362,7 +866,7 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||||
textField.placeholder = @"Video Height";
|
textField.placeholder = [LocalizationHelper localizedStringForKey:@"Video Height"];
|
||||||
textField.clearButtonMode = UITextFieldViewModeAlways;
|
textField.clearButtonMode = UITextFieldViewModeAlways;
|
||||||
textField.borderStyle = UITextBorderStyleRoundedRect;
|
textField.borderStyle = UITextBorderStyleRoundedRect;
|
||||||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
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;
|
NSArray * textfields = alertController.textFields;
|
||||||
UITextField *widthField = textfields[0];
|
UITextField *widthField = textfields[0];
|
||||||
UITextField *heightField = textfields[1];
|
UITextField *heightField = textfields[1];
|
||||||
@@ -410,12 +914,12 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
[self updateResolutionDisplayViewText];
|
[self updateResolutionDisplayViewText];
|
||||||
self->_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
|
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];
|
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:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]];
|
||||||
[self presentViewController:alertController animated:YES completion: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
|
// Restore the previous selection
|
||||||
[self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
|
[self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
|
||||||
}]];
|
}]];
|
||||||
@@ -442,7 +946,7 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
[subview removeFromSuperview];
|
[subview removeFromSuperview];
|
||||||
}
|
}
|
||||||
UILabel *label1 = [[UILabel alloc] init];
|
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.font = [UIFont systemFontOfSize:fontSize];
|
||||||
[label1 sizeToFit];
|
[label1 sizeToFit];
|
||||||
label1.frame = CGRectMake(padding, (viewFrameHeight - label1.frame.size.height) / 2, label1.frame.size.width, label1.frame.size.height);
|
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];
|
[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 {
|
- (void) bitrateSliderMoved {
|
||||||
assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
|
assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
|
||||||
_bitrate = bitrateTable[(int)self.bitrateSlider.value];
|
_bitrate = bitrateTable[(int)self.bitrateSlider.value];
|
||||||
@@ -464,7 +992,8 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
|
|
||||||
- (void) updateBitrateText {
|
- (void) updateBitrateText {
|
||||||
// Display bitrate in Mbps
|
// 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 {
|
- (NSInteger) getChosenFrameRate {
|
||||||
@@ -528,32 +1057,79 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
NSInteger height = [self getChosenStreamHeight];
|
NSInteger height = [self getChosenStreamHeight];
|
||||||
NSInteger width = [self getChosenStreamWidth];
|
NSInteger width = [self getChosenStreamWidth];
|
||||||
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
|
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 optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
|
||||||
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
|
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
|
||||||
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
|
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
|
||||||
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
|
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
|
||||||
uint32_t preferredCodec = [self getChosenCodecPreference];
|
uint32_t preferredCodec = [self getChosenCodecPreference];
|
||||||
|
BOOL enableYUV444 = [self.yuv444Selector selectedSegmentIndex] == 1;
|
||||||
|
BOOL enablePIP = [self.pipSelector selectedSegmentIndex] == 1;
|
||||||
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
|
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
|
||||||
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
|
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
|
||||||
BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
|
// BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
|
||||||
BOOL statsOverlay = [self.statsOverlaySelector selectedSegmentIndex] == 1;
|
NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex];
|
||||||
|
NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex];
|
||||||
|
BOOL statsOverlayEnabled = statsOverlayLevel != 0;
|
||||||
BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
|
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
|
[dataMan saveSettingsWithBitrate:_bitrate
|
||||||
framerate:framerate
|
framerate:framerate
|
||||||
height:height
|
height:height
|
||||||
width:width
|
width:width
|
||||||
audioConfig:2 // Stereo
|
audioConfig:2 // Stereo
|
||||||
onscreenControls:onscreenControls
|
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
|
optimizeGames:optimizeGames
|
||||||
multiController:multiController
|
multiController:multiController
|
||||||
swapABXYButtons:swapABXYButtons
|
swapABXYButtons:swapABXYButtons
|
||||||
audioOnPC:audioOnPC
|
audioOnPC:audioOnPC
|
||||||
preferredCodec:preferredCodec
|
preferredCodec:preferredCodec
|
||||||
|
enableYUV444:enableYUV444
|
||||||
|
enablePIP:enablePIP
|
||||||
useFramePacing:useFramePacing
|
useFramePacing:useFramePacing
|
||||||
enableHdr:enableHdr
|
enableHdr:enableHdr
|
||||||
btMouseSupport:btMouseSupport
|
btMouseSupport:btMouseSupport
|
||||||
absoluteTouchMode:absoluteTouchMode
|
// absoluteTouchMode:absoluteTouchMode
|
||||||
statsOverlay:statsOverlay];
|
touchMode:touchMode
|
||||||
|
statsOverlayLevel:statsOverlayLevel
|
||||||
|
statsOverlayEnabled:statsOverlayEnabled
|
||||||
|
unlockDisplayOrientation:unlockDisplayOrientation
|
||||||
|
resolutionSelected:resolutionSelected
|
||||||
|
externalDisplayMode:externalDisplayMode
|
||||||
|
localMousePointerMode:localMousePointerMode];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didReceiveMemoryWarning {
|
- (void)didReceiveMemoryWarning {
|
||||||
@@ -567,5 +1143,4 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@end
|
@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>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight</string>
|
<string>VoidLink</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<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
|
# App Store 分发一次性收费声明.
|
||||||
* Install Xcode from the [App Store page](https://apps.apple.com/us/app/xcode/id497799835)
|
# Statement on One-Time App Store Distribution Fee.
|
||||||
* Run `git clone --recursive https://github.com/moonlight-stream/moonlight-ios.git`
|
- VoidLink 的 App Store 安装费用用于覆盖通过 Apple 框架进行安全可靠应用分发的成本。同时也支持项目维护者进行持续的开发、维护,以及公众用户访问并下载官方签名版本。感谢您对项目的支持,帮助我们持续改进和优化。<br><br>
|
||||||
* If you've already clone the repo without `--recursive`, run `git submodule update --init --recursive`
|
- 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.
|
||||||
* Open Moonlight.xcodeproj in Xcode
|
|
||||||
* To run on a real device, you will need to locally modify the signing options:
|
<br>
|
||||||
* Click on "Moonlight" at the top of the left sidebar
|
|
||||||
* Click on the "Signing & Capabilities" tab
|
# 开发者B站号. Developer on Bilibili
|
||||||
* 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.
|
如果你在用Bilibili, 请关注`True砖家`,了解该fork的最新消息。 <br>
|
||||||
* Change the "Bundle Identifier" to something different. You can add your name or some random letters to make it unique.
|
If you are on Bilibili, subscribe `True砖家` to get the latest news of this fork: <br>
|
||||||
* Now you can select your Apple device in the top bar as a target and click the Play button to run.
|
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
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "self:Moonlight.xcodeproj">
|
location = "self:">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "d1fc50a28d75017e32553df2b233f8a7e2cf1542da32728ce6722b200f745608",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "cocoalumberjack",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
|
||||||
|
"version" : "3.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "openssl-package",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/krzyzanowskim/OpenSSL-Package.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
|
||||||
|
"version" : "3.3.2000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "svgkit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SVGKit/SVGKit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
|
||||||
|
"version" : "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-collections.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||||
|
"version" : "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-log",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-log",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "bc386b95f2a16ccd0150a8235e7c69eab2b866ca",
|
||||||
|
"version" : "1.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
//
|
||||||
|
// 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: 7) {
|
||||||
|
// 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: 90, height: 90)
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption2)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.lineLimit(nil)
|
||||||
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
|
//.padding()
|
||||||
|
} else {
|
||||||
|
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.lineLimit(nil)
|
||||||
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
|
//.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 说明文字
|
||||||
|
Text(LocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.font(Font.caption.italic())
|
||||||
|
.lineLimit(nil)
|
||||||
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
|
||||||
|
Text(LocalizationHelper.localizedString(forKey: "VoidLink delivers better performance now!"))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.font(Font.callout.bold())
|
||||||
|
.lineLimit(nil)
|
||||||
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
|
|
||||||
|
|
||||||
|
// 链接按钮
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
Link(LocalizationHelper.localizedString(forKey: "Learn more"), destination: URL(string: LocalizationHelper.localizedString(forKey: "supportLink"))!)
|
||||||
|
|
||||||
|
if #available(iOS 16, *) {
|
||||||
|
let languageCode = Locale.current.language.languageCode?.identifier
|
||||||
|
if languageCode == "zh" {
|
||||||
|
Link(LocalizationHelper.localizedString(forKey: "加入QQ群"), destination: URL(string: LocalizationHelper.localizedString(forKey: "https://qm.qq.com/q/uM51CYWLS2"))!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if PublicUtils.isIPhone {
|
||||||
|
Link(LocalizationHelper.localizedString(forKey: "joinCommunity"), destination: URL(string: LocalizationHelper.localizedString(forKey: "communityLink"))!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 10)
|
||||||
|
|
||||||
|
if !PublicUtils.isIPhone {
|
||||||
|
Link(LocalizationHelper.localizedString(forKey: "joinCommunity"), destination: URL(string: LocalizationHelper.localizedString(forKey: "communityLink"))!)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 15) {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
if PublicUtils.isIPad, PublicUtils.isIAPAddonAvailable {
|
||||||
|
Button(LocalizationHelper.localizedString(forKey: "I'm an artist")) {
|
||||||
|
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||||
|
if !info.valid {
|
||||||
|
IAPManager.inAppPurchaseAction(viewController: self.aboutVC, product: .PencilProPack)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AlertControllerUtil.showAlert(
|
||||||
|
in: self.aboutVC,
|
||||||
|
title: "",
|
||||||
|
message: LocalizationHelper.localizedString(forKey:"Drawing Toolkit already purchased"),
|
||||||
|
withCancel: false,
|
||||||
|
buttonTitle: LocalizationHelper.localizedString(forKey: "OK"),
|
||||||
|
countdown: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.blue.opacity(1))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(height: 46)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK 按钮
|
||||||
|
Button(LocalizationHelper.localizedString(forKey: "OK")) {
|
||||||
|
aboutVC.dismiss(animated:true)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.blue)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(height: 46)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
Button(LocalizationHelper.localizedString(forKey: "Join us")) {
|
||||||
|
// 打开链接
|
||||||
|
if let url = URL(string: LocalizationHelper.localizedString(forKey: "supportLink")) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.blue)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.frame(height: 33)
|
||||||
|
.frame(minWidth: 100)
|
||||||
|
/*
|
||||||
|
Button(LocalizationHelper.localizedString(forKey: "OK")) {
|
||||||
|
presentationMode.wrappedValue.dismiss()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.blue)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.frame(height: 33)
|
||||||
|
.frame(minWidth: 100)*/
|
||||||
|
Button(LocalizationHelper.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,66 @@
|
|||||||
|
//
|
||||||
|
// 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, ControllerUINavigationDelegate {
|
||||||
|
func getNavigationElements() -> [ControllerNavigationElement] {
|
||||||
|
var elements: [ControllerNavigationElement] = []
|
||||||
|
elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: "ok"))
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
||||||
|
func navigateByController(forward: Bool) {}
|
||||||
|
func navigateByController(downward: Bool) {}
|
||||||
|
func persistControllerNavigationHighlight() {}
|
||||||
|
func restoreControllerNavigationHighlight() {}
|
||||||
|
func restoreControllerNavigationHighlightAfterSettingsModeSwitch() {}
|
||||||
|
func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) {}
|
||||||
|
|
||||||
|
func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) {
|
||||||
|
if pressed, navigation.action == "ok" {
|
||||||
|
self.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 430)
|
||||||
|
|
||||||
|
modalPresentationStyle = .formSheet
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
ControllerNavigator.setUINavigationDelegate(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
ControllerNavigator.restorePreviousUINavigationDelegate(ifCurrentDelegateIs: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIPhone() -> Bool {
|
||||||
|
return UIDevice.current.userInterfaceIdiom == .phone
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
//
|
||||||
|
// AlertControllerUtil.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by Weimin on 2025/10/6.
|
||||||
|
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc class AlertControllerUtil: NSObject {
|
||||||
|
private enum AlertTextRole {
|
||||||
|
case title
|
||||||
|
case message
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeCountdownMessage(baseMessage: String?, remainingSeconds: Int) -> String {
|
||||||
|
let countdownText = "\(remainingSeconds)"
|
||||||
|
guard let baseMessage, !baseMessage.isEmpty else {
|
||||||
|
return countdownText
|
||||||
|
}
|
||||||
|
return "\(baseMessage)\n\n\(countdownText)"
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
private static func makeLeftAlignedAttributedText(_ text: String, role: AlertTextRole) -> NSAttributedString {
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.alignment = .left
|
||||||
|
paragraphStyle.lineBreakMode = .byWordWrapping
|
||||||
|
|
||||||
|
let font: UIFont
|
||||||
|
let color: UIColor
|
||||||
|
|
||||||
|
switch role {
|
||||||
|
case .title:
|
||||||
|
font = .preferredFont(forTextStyle: .headline)
|
||||||
|
color = .label
|
||||||
|
case .message:
|
||||||
|
font = .preferredFont(forTextStyle: .footnote)
|
||||||
|
color = .label
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSAttributedString(
|
||||||
|
string: text,
|
||||||
|
attributes: [
|
||||||
|
.paragraphStyle: paragraphStyle,
|
||||||
|
.font: font,
|
||||||
|
.foregroundColor: color
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func applyLeftAlignment(title: String?, message: String?) {
|
||||||
|
if #available(iOS 26.0, *) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
if let title, !title.isEmpty {
|
||||||
|
alertController.setValue(makeLeftAlignedAttributedText(title, role: .title), forKey: "attributedTitle")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let message, !message.isEmpty {
|
||||||
|
alertController.setValue(makeLeftAlignedAttributedText(message, role: .message), forKey: "attributedMessage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 类方法,直接在任何 UIViewController 上显示倒计时弹窗
|
||||||
|
/// - Parameters:
|
||||||
|
/// - viewController: 要显示弹窗的控制器
|
||||||
|
/// - title: 弹窗标题
|
||||||
|
/// - message: 弹窗内容
|
||||||
|
/// - buttonTitle: 按钮初始文字(倒计时结束后显示)
|
||||||
|
/// - countdown: 倒计时秒数
|
||||||
|
/// - completion: 点击确认或倒计时结束的回调
|
||||||
|
|
||||||
|
@objc public static var actionCancelled:Bool = false
|
||||||
|
@objc public static var alertController:UIAlertController = UIAlertController()
|
||||||
|
@objc public static var cancelButtonString:String = "Cancel"
|
||||||
|
@objc static var completion: (() -> Void)? = nil
|
||||||
|
@objc public static var autoCompletion:Bool = false
|
||||||
|
@objc public static var isCountingDown:Bool = false
|
||||||
|
|
||||||
|
@objc class func showAlert(
|
||||||
|
in viewController: UIViewController? = nil,
|
||||||
|
title: String?,
|
||||||
|
message: String?,
|
||||||
|
withCancel: Bool,
|
||||||
|
buttonTitle: String,
|
||||||
|
countdown: Int,
|
||||||
|
action: (() -> Void)? = nil,
|
||||||
|
completion: (() -> Void)? = nil
|
||||||
|
) {
|
||||||
|
var remainingSeconds = countdown
|
||||||
|
let originalMessage = message
|
||||||
|
var isAlertDismissed = false
|
||||||
|
|
||||||
|
AlertControllerUtil.completion = completion
|
||||||
|
|
||||||
|
guard let viewController = viewController else {return}
|
||||||
|
|
||||||
|
alertController = UIAlertController(
|
||||||
|
title: title,
|
||||||
|
message: (countdown > 0 && !autoCompletion) ? makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds) : message,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
applyLeftAlignment(
|
||||||
|
title: title,
|
||||||
|
message: (countdown > 0 && !autoCompletion) ? makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds) : message
|
||||||
|
)
|
||||||
|
|
||||||
|
let confirmAction = UIAlertAction(title: buttonTitle, style: .default) { _ in
|
||||||
|
isAlertDismissed = true
|
||||||
|
actionCancelled = false
|
||||||
|
completion?()
|
||||||
|
cancelButtonString = "Cancel"
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelAction = UIAlertAction(title: LocalizationHelper.localizedString(forKey: cancelButtonString), style: .cancel) { _ in
|
||||||
|
isAlertDismissed = true
|
||||||
|
actionCancelled = true
|
||||||
|
completion?()
|
||||||
|
cancelButtonString = "Cancel"
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmAction.isEnabled = false
|
||||||
|
|
||||||
|
if withCancel {alertController.addAction(cancelAction)}
|
||||||
|
if buttonTitle != "" && !autoCompletion {alertController.addAction(confirmAction)}
|
||||||
|
|
||||||
|
if(countdown == 0) {
|
||||||
|
confirmAction.isEnabled = buttonTitle != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
viewController.present(alertController, animated: true, completion: action)
|
||||||
|
isCountingDown = !autoCompletion && countdown > 0
|
||||||
|
|
||||||
|
if(countdown == 0) {return}
|
||||||
|
|
||||||
|
// 倒计时
|
||||||
|
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
||||||
|
timer.schedule(deadline: .now() + (autoCompletion ? 0.5 : 1.0), repeating: autoCompletion ? 0.1: 1.0, leeway: .milliseconds(50))
|
||||||
|
timer.setEventHandler {
|
||||||
|
guard !isAlertDismissed else {
|
||||||
|
timer.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingSeconds -= 1
|
||||||
|
|
||||||
|
if remainingSeconds <= 0 {
|
||||||
|
isCountingDown = false
|
||||||
|
isAlertDismissed = true
|
||||||
|
timer.cancel()
|
||||||
|
|
||||||
|
if autoCompletion {
|
||||||
|
alertController.message = originalMessage
|
||||||
|
applyLeftAlignment(title: title, message: originalMessage)
|
||||||
|
autoCompletion = false
|
||||||
|
completion?()
|
||||||
|
AlertControllerUtil.completion = nil
|
||||||
|
alertController.dismiss(animated: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if PublicUtils.isRunningOnMacAsiPadApp && buttonTitle != "" {
|
||||||
|
AlertControllerUtil.completion = nil
|
||||||
|
alertController.dismiss(animated: false) {
|
||||||
|
showAlert(
|
||||||
|
in: viewController,
|
||||||
|
title: title,
|
||||||
|
message: originalMessage,
|
||||||
|
withCancel: withCancel,
|
||||||
|
buttonTitle: buttonTitle,
|
||||||
|
countdown: 0,
|
||||||
|
action: nil,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmAction.isEnabled = true
|
||||||
|
alertController.message = originalMessage
|
||||||
|
applyLeftAlignment(title: title, message: originalMessage)
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
ControllerNavigator.setUINavigationDelegate(alertController)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !autoCompletion {
|
||||||
|
let countdownMessage = makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds)
|
||||||
|
alertController.message = countdownMessage
|
||||||
|
applyLeftAlignment(title: title, message: countdownMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,5 +21,6 @@
|
|||||||
- (void)saveContext;
|
- (void)saveContext;
|
||||||
- (NSURL *)applicationDocumentsDirectory;
|
- (NSURL *)applicationDocumentsDirectory;
|
||||||
- (NSURL*) getStoreURL;
|
- (NSURL*) getStoreURL;
|
||||||
|
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0));
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -5,8 +5,13 @@
|
|||||||
// Created by Diego Waxemberg on 1/17/14.
|
// Created by Diego Waxemberg on 1/17/14.
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// 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 "AppDelegate.h"
|
||||||
|
#import "MainFrameViewController.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
|
|
||||||
@@ -22,17 +27,66 @@ static NSString* DB_NAME = @"Moonlight_tvOS.bin";
|
|||||||
static NSString* DB_NAME = @"Limelight_iOS.sqlite";
|
static NSString* DB_NAME = @"Limelight_iOS.sqlite";
|
||||||
#endif
|
#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
|
#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];
|
||||||
|
[GenericUtils installSegmentedControlPreviousSelectionTracking];
|
||||||
|
[IAPManager shared];
|
||||||
|
|
||||||
|
// 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;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler {
|
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler {
|
||||||
_pcUuidToLoad = (NSString*)[shortcutItem.userInfo objectForKey:@"UUID"];
|
_pcUuidToLoad = (NSString*)[shortcutItem.userInfo objectForKey:@"UUID"];
|
||||||
_shortcutCompletionHandler = completionHandler;
|
_shortcutCompletionHandler = completionHandler;
|
||||||
@@ -96,6 +96,7 @@ static NSData* p12 = nil;
|
|||||||
|
|
||||||
bio = BIO_new(BIO_s_mem());
|
bio = BIO_new(BIO_s_mem());
|
||||||
i2d_X509_bio(bio, x509);
|
i2d_X509_bio(bio, x509);
|
||||||
|
X509_free(x509);
|
||||||
|
|
||||||
BUF_MEM* mem;
|
BUF_MEM* mem;
|
||||||
BIO_get_mem_ptr(bio, &mem);
|
BIO_get_mem_ptr(bio, &mem);
|
||||||
@@ -222,6 +223,7 @@ static NSData* p12 = nil;
|
|||||||
+ (NSData *)getSignatureFromCert:(NSData *)cert {
|
+ (NSData *)getSignatureFromCert:(NSData *)cert {
|
||||||
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
|
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
|
||||||
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||||
|
BIO_free(bio);
|
||||||
|
|
||||||
if (!x509) {
|
if (!x509) {
|
||||||
Log(LOG_E, @"Unable to parse certificate in memory!");
|
Log(LOG_E, @"Unable to parse certificate in memory!");
|
||||||