From b5571639aad538acf8fe973596af2c8f04033b97 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Sat, 19 Jun 2021 13:50:35 -0400 Subject: [PATCH] Initial chromecast support. --- JellyfinPlayer.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../ic_cast_connected_white_24dp-1.png | Bin 981 -> 1537 bytes .../ic_cast_connected_white_24dp-2.png | Bin 981 -> 1537 bytes .../ic_cast_connected_white_24dp.png | Bin 1461 -> 1537 bytes .../ic_cast0_white_24dp-1.png | Bin 856 -> 1312 bytes .../ic_cast0_white_24dp-2.png | Bin 1208 -> 1312 bytes .../ic_cast0_white_24dp.png | Bin 856 -> 1312 bytes .../ic_cast1_white_24dp-1.png | Bin 888 -> 1369 bytes .../ic_cast1_white_24dp-2.png | Bin 1249 -> 1369 bytes .../ic_cast1_white_24dp.png | Bin 888 -> 1369 bytes .../ic_cast2_white_24dp-1.png | Bin 884 -> 1364 bytes .../ic_cast2_white_24dp-2.png | Bin 1266 -> 1364 bytes .../ic_cast2_white_24dp.png | Bin 884 -> 1364 bytes .../ic_cast_white_24dp-1.png | Bin 824 -> 1296 bytes .../ic_cast_white_24dp-2.png | Bin 1230 -> 1296 bytes .../ic_cast_white_24dp.png | Bin 824 -> 1296 bytes JellyfinPlayer/Info.plist | 6 + JellyfinPlayer/ItemView.swift | 2 +- .../OpenCastSwift/Networking/CastClient.swift | 4 +- JellyfinPlayer/VideoPlayer.storyboard | 26 +- JellyfinPlayer/VideoPlayer.swift | 282 ++++++++++++++---- .../VideoPlayerCastDeviceSelector.swift | 20 +- 23 files changed, 285 insertions(+), 65 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index b75b6384..ff3ba448 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -646,12 +646,12 @@ 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, 535BAEA4264A151C005FA86D /* VideoPlayer.swift */, 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, + 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */, 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, 53DE4BD1267098F300739748 /* SearchBarView.swift */, 625CB5672678B6FB00530A6E /* SplashView.swift */, 625CB56B2678C0FD00530A6E /* MainTabView.swift */, 625CB56E2678C23300530A6E /* HomeView.swift */, - 532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */, ); path = JellyfinPlayer; sourceTree = ""; diff --git a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b10bc540..77c500d2 100644 --- a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -29,7 +29,7 @@ } }, { - "package": "JellyfinAPI", + "package": "jellyfin-sdk-swift", "repositoryURL": "https://github.com/jellyfin/jellyfin-sdk-swift", "state": { "branch": "main", @@ -38,7 +38,7 @@ } }, { - "package": "KeychainSwift", + "package": "keychain-swift", "repositoryURL": "https://github.com/evgenyneu/keychain-swift", "state": { "branch": null, @@ -83,7 +83,7 @@ } }, { - "package": "SwiftProtobuf", + "package": "swift-protobuf", "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, @@ -92,7 +92,7 @@ } }, { - "package": "Introspect", + "package": "SwiftUI-Introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", "state": { "branch": null, diff --git a/JellyfinPlayer/Assets.xcassets/CastConnected.imageset/ic_cast_connected_white_24dp-1.png b/JellyfinPlayer/Assets.xcassets/CastConnected.imageset/ic_cast_connected_white_24dp-1.png index dcba78d8c16d00b7af95d9bc3690fd488fa77957..0b0d9447387e44b1c270b09b00d56a1eb8e8af7f 100644 GIT binary patch literal 1537 zcmV+c2LAbpP)Px)yGcYrRCodHn@xySWf;fjnzk~DBC#(bN~5x9lZ&8f3W?nGvC2)W5n9wPH|a70 z6Tw9#!sNzU6j4;mh*3#eM76M@Z%Gu3paK=j(z*Sf;hyVz?mYi<-gD16ce*q01Ap#& zp6~a6-}9c2nL9C&Br%W}NDL$f5(9~W#6V&oG2k%JadZ>iZrAkFJ#6H2wdc14bZ42v z*WiGm)9G{v=d^$z$XSQe>tOSs^AbP)i1|GQNxz2k%n{UEIUnM%+{no|F_In*SJH~i z()+a2YPphQ6xiI2gT4$Bw5@MhzJnY=gQu8nT1$09j{VBdaf)4$IS&%FU>7$>0&;#? z@VI}n=a$ZQ@Dv;OIXB*aBB+GYL{K8Agn<%sT0tcQtt_z@n$=aHFlZ5sb*{o`1(keZ ziJ%gKa;I3GrC3sDY|V97(8aNxd1T{t3A&LwJHbzp^~TPvk!P&ld!z@M{j=bS{~HTx z>rhwW(7zFaLP99IiQ|{RRH$FfaX8_yj~IgLo?>CW`7zCW3T!oP0bed~7&Ap~1;OZf z`nPvNhCjd$;5+aQILgFN@fj`TyBy~iz;@91ml4}YofR}lko$flXv7Gc1#g2x6rKxg zatKkA7!QJ*q*IHAQ4$nR!&cK9U>^;f3=Q5GlGdO%k{YZZnxGd5$ z;Il0YX!}$3qUwhJgPXKnb?lA4?H9J8-UjXjQ(z;=`Ey((uVQ?T^WUzFuF&8P3@^II z29=HNK}9_=nQOo_I1J`MjC_uhJ&s7h-uj-TimM4I|7bml*Mqk}3{fZXyF(?%b{TjN zv?c|$-I~h3vYzbsf?2mpUimwR_6Xt%wl(1Ad`NRgZc&t~g7P0)PoXW~xGLDw8%aeg zX%hRzk{m;s)x=aXt1_!vn=B5x)MI#d)`)utw_@+kipg0h@{GuRF6 zl#qgbe-_|;+8ww1Sb~0&<3F@YHK-CQ|0)D!xsNhfs}f>c1&TKAJq5AvW<~0L7SwQ4 zEB|2#vH~Z-Jt`?SySk}c0DHi_)r&>{5NJfa{H9aMmA?h71{=T>*bY7hG502C@hc+9 z?vs?f<~4+K2mGjls_4D^rc*hTzpE$7T44LlUn*fwZ!4+jbek3N|Dbp8jnF?YVk)WK zi8Vz~mfR)*-YMzK_9S=XMsJTLb0U6MRww+#_gYYNyCBl#)nU$XocFqP6#7&ilV&GS z6SXS4RZ?tknu8;DhduI_`c5D7D%F3qo-A8|BFSz6-1{W-_7*_JZH{iBAxC;~rIGYx zww0vBUe3Ehz+n-z-wkL{Df;Njk_6b*=vy!YO21Kj)ShHlKKHQ%{WS38d)Zh^2CoUz z$#PD>5p)|=05;pW)zxjU7l*&SnN+9Q4PH!JI{I)$)^okP-9OPkpn|a3Er2J-3u2CW z0qC}>m^SpHhujK^xpG`dbM{J9tt5L~@czPjGcoRIhW`;U;oF5?-qfi{#^^;Yd*#!Y z7=p~%NSf(K1q0r-{KnxYfhrt68Xg4WuYz$nw7;S(w%8Bs`Vvc!1x)g-sEzyme#ZPj ztqA*v2v62~$`R`hg!QTrWN}Z1!NJ(x0(x?tr$l!`=&us`QK{_*b+rh(Uvzp}=oLX{ zgn$2MNW$Kx+Q_k=Wac*Ml@%*qFZ_QIdPPufBMf3kP0%@N+kR-P#{E%m&|4ZN1`-2_ nfy6*!ATf{_NDL$fE-~;QWUP;~)&@CZ00000NkvXXu0mjfqf6=a literal 981 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 literal 981 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 literal 1461 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 literal 888 zcmV-;1Bd*HP)Px&GD$>1RA>e5nagVwK@i4wec%hh2b$8k3Q>F?|6?wQ%tYL!$o zP|ZL!1EraPR;$%OZ=y}KY_bF(VQbh4t9!?FJjLJXV=Ls517lYNiY`@Efubw0 z&I9nf+WUjjR!gYpN)3Dk!XDs3H^ppzHToI*HTr@5(e@N1RvHNeK(rR!;?x5?gz*h> zBGQD&iti{(YYAjw+CayM6e90vpF`w*R&u}N|GEcu0B`~#Cs9?aa0zdm08%wJI^9IO z3*C*bN7Ljx!MT1dEAX}LL&i<82LfpbxkeSztnWvM({hN-bFamWzTCoH5wN3k0}(lb zZntq~FG6BIC7xU8zX^CG+~@%IQSY0!rF((-XF0WFEc=p|H3 zM$5s9evf&OLgFRy_{rS2Dr7zf0D2H$)988Bihdb8#q0}sBIN3bJ>y*8|X^e>^yRqwlYoC&!Y z#p0*&5ILC@j;`(64Fni$3Y|pvp=|>@5_h9fu1CJAVE2vQVz)&(xx0Y?ch>iS20VtI zhJ;@1tX#&i07uCg^9hjDGSjJKWym_cuq^?5f35Q|TL}Oz5W8oSu+K&QO5|^j{2KS{ zN}~JdjUkaN0_qfrktA#NPfTEPwJWi{M1EYwsJmLzY z1ayq+WK5(J0X^KTgwnZSd{p+5z%pa%_zwS_vA`<3nt^Htsu?I|27UroeYoWv(6MU( O0000Px)6G=otRCodHn@y-yRTRhH?Ym4O?b}pdWgqY|gM>&L2+<%!p=N=Du&L6Z9ObB* zM6f1byin8=a|WCEE$CXfka0+~Q2kO>$BdWLOcetur|9feMj-fa1Gz;>1xj-k`i zUavPF#?%K0ICtUnKKe}P-0|mw#J3$tKl*W202L$W01itHj$pag+LaUMY#P=Y|BIFFzVC_x}me2rh;)dQg);Qw!-f0Ld==L-FeUG7ivOG+w$h+N-&t}XON(p%6{04QL; zpg$-DnE5@Wtk8i>2q+cOG9099(F*`OMS7ykBWqAV@F?X5au1;Q1L`E{aX@K=bxN)T zkOQG#x)k$k$BBbhCY{E%1o+nyz6>=?nLiJ1+0nufzuJ%Yvu7^sDOMLyjUaa zVYJCqx`5y3K+@mbS|uX`X_{h%Qz??*RBqYuc3zs@(yP- z(xcdZHS99!vine^tTpD#1>^$QMCJ&PHaLTyGKw@MXVBP5dXl>Oe9~G80qNeQUv)F8 z5poz+kaeP){3kV#^kp)(AonO+SNHO9m#KrbQcR&&aoUM$TEygg3N|uoezF{R&r{f~ zGm>`BkEs_MW!CG4Vxv#yu1#M`F5w93YghrJyD4}O-Ph;vzd<8uA8p1KZk?$tQD-Kt zf;QNafLtembP|1m=hsn_Ja(-~Z=!udUERZU2(@GtV9Q_4au}DbF(B7L0)318i)ake zE`nX_{DX8hcy^DPy0^K9Hl+gtQs;rRmt;>Q5FG*Du*#@6hds9KH~Dm8y>_Iw%CP2_ ze~#94p;Qp33Fzm#*75(4^5+d7?R<|@zwZu59j#T58FkL#f5@Q!SFLV8M4)|$yDit=Mx3w`HVVy%x|D6Ak|sHPk6gu*~{x& zktOOWJTRoAx_42q4wF$FJ<=XS}?hjrpJVoxFVP6z;iDsbi^Wz2US_00000NkvXXu0mjf(w>o6 literal 1249 zcmV<71Rnc|P)Px(n@L1LRA>e5n%iqtM-;}56}4!MwSrf)D5m;g@m5h#>rZ+#tGS- zpdaL2oMVAhn+Q`$ksu zrU7vqkO{6rxjd-5>RBphoRX7rGjV=tVimVjvW0|WyYU&OLP?H!5te3&SS?}CEu7MM z;nY-|1>vUBmxIQ|2f1zomNKNU&H6Kp-;KIETM=29%-S|r+eC@h0~ zFXc^$YX;;>hC(U^_euY;t3OInJ6_iQ_~X?8p)5rY%mvm2x2k=<3{{1 z)0aM+Wm4*%!%75r2>yh@&p_qtDr}UD4UpCv&!p5P97YZ4w?OOlG`J07YgSWyO;$+{ zi7-S41k$g;UnmzqwKcmE()vW2De;5}?f~cko!}%m2v*dJ7SfM+ejeyjVnrjpmOg52 zc(%=m>z*)g(gvorw;Fr{O5i#H-vft%cI*m)v~FF$fqyF=4sLLo-r;*9Zlh;vBoLjo z2f!Ty-v=hz@F4vkwyPjE(%QY3*fy=w8zJms)8C$sfQyix?~JZYNMAvZt?9+I(fQtT z846(+>?$f(Ah6CET$xO7k+!?HyPP=JmX#sl!$hHr#Tu{&`~<4~B-AD3mnx*Sk^K#< zEWm%NHIBCK32RD%UT_s01zQN_Q*aDujatzZ{Sgus(;K_aXnQ%)jcqAtsd&P%D3S#1 z=Gj`%9r4jfpMZ2~Bcq+j-ir9o!MEDgG~zz|rkaG^>mYEA`WHa!(~8#gUTZ|Z3v^^z znP&~7sW(Je0YUl!#a0j->F*i2)H#j6Hm)9EMf*g{#1kgq4)7Jw?Pwm*noCJSdOJqD zrgda}mvSCd^ku|KJAJ2?fOQy2-4o^|BB*QNETCz+iNK~%rUcU0F&s}B2bCQWn?3-> z&SfR2CE|Y%N8YZBzkcfd3Q2vz45ftfEeJaBi6{C-D z$vHGpZjQ9|Nv}m%?LQ1QF_9OEq2|kb{-!z{SB9T!*vu+ zX=k&gMmo09oUo4Q`r%>ag*Bj8ER~4QaGCn)=#uNK5yJ9~9VMNvlc0C@m diff --git a/JellyfinPlayer/Assets.xcassets/CastConnecting1.imageset/ic_cast1_white_24dp.png b/JellyfinPlayer/Assets.xcassets/CastConnecting1.imageset/ic_cast1_white_24dp.png index 090d9226bed7877560af281bb459f5e5de0f0904..3e3ede88b67ae7d82e525ccb3a2b25636118d327 100644 GIT binary patch literal 1369 zcmV-f1*ZCmP)Px)6G=otRCodHn@y-yRTRhH?Ym4O?b}pdWgqY|gM>&L2+<%!p=N=Du&L6Z9ObB* zM6f1byin8=a|WCEE$CXfka0+~Q2kO>$BdWLOcetur|9feMj-fa1Gz;>1xj-k`i zUavPF#?%K0ICtUnKKe}P-0|mw#J3$tKl*W202L$W01itHj$pag+LaUMY#P=Y|BIFFzVC_x}me2rh;)dQg);Qw!-f0Ld==L-FeUG7ivOG+w$h+N-&t}XON(p%6{04QL; zpg$-DnE5@Wtk8i>2q+cOG9099(F*`OMS7ykBWqAV@F?X5au1;Q1L`E{aX@K=bxN)T zkOQG#x)k$k$BBbhCY{E%1o+nyz6>=?nLiJ1+0nufzuJ%Yvu7^sDOMLyjUaa zVYJCqx`5y3K+@mbS|uX`X_{h%Qz??*RBqYuc3zs@(yP- z(xcdZHS99!vine^tTpD#1>^$QMCJ&PHaLTyGKw@MXVBP5dXl>Oe9~G80qNeQUv)F8 z5poz+kaeP){3kV#^kp)(AonO+SNHO9m#KrbQcR&&aoUM$TEygg3N|uoezF{R&r{f~ zGm>`BkEs_MW!CG4Vxv#yu1#M`F5w93YghrJyD4}O-Ph;vzd<8uA8p1KZk?$tQD-Kt zf;QNafLtembP|1m=hsn_Ja(-~Z=!udUERZU2(@GtV9Q_4au}DbF(B7L0)318i)ake zE`nX_{DX8hcy^DPy0^K9Hl+gtQs;rRmt;>Q5FG*Du*#@6hds9KH~Dm8y>_Iw%CP2_ ze~#94p;Qp33Fzm#*75(4^5+d7?R<|@zwZu59j#T58FkL#f5@Q!SFLV8M4)|$yDit=Mx3w`HVVy%x|D6Ak|sHPk6gu*~{x& zktOOWJTRoAx_42q4wF$FJ<=XS}?hjrpJVoxFVP6z;iDsbi^Wz2US_00000NkvXXu0mjf(w>o6 literal 888 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 KMNUMnLSTaGJC;BI diff --git a/JellyfinPlayer/Assets.xcassets/CastConnecting2.imageset/ic_cast2_white_24dp-2.png b/JellyfinPlayer/Assets.xcassets/CastConnecting2.imageset/ic_cast2_white_24dp-2.png index a49ee68df824f8036f4b3224c8761ae10ebdd517..1668ffdba6f9b5a87673546cb2a2fe848cfe55ea 100644 GIT binary patch literal 1364 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 KMNUMnLSTaGJC;BI diff --git a/JellyfinPlayer/Assets.xcassets/CastDisconnected.imageset/ic_cast_white_24dp-1.png b/JellyfinPlayer/Assets.xcassets/CastDisconnected.imageset/ic_cast_white_24dp-1.png index 62c086f1459c1195631f7ed81e23920abeba8ca0..ce167c7fe4a64e25118c14710ef1d3a29f917a7b 100644 GIT binary patch literal 1296 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 diff --git a/JellyfinPlayer/Assets.xcassets/CastDisconnected.imageset/ic_cast_white_24dp.png b/JellyfinPlayer/Assets.xcassets/CastDisconnected.imageset/ic_cast_white_24dp.png index 62c086f1459c1195631f7ed81e23920abeba8ca0..ce167c7fe4a64e25118c14710ef1d3a29f917a7b 100644 GIT binary patch literal 1296 zcmV+r1@HQaP)Px(%1J~)RCodHTRUqMQ5fBo#43s?2JwN2FMJ?2S_o>2AlQhVHdYZVEVc6oC?cd# zArNh}QAAKnk!Ubjsf8%MARvm0pn?j%vz{Y6yLZO#%$+qm`@A0<=Fa!H_xny}?!7ac zDHMVP1_A?tfxtjuATSUZ2n+-U3A-sS&s_v=)c203M`i$x2QqYCDT;_8TCKmcLHde zciO71yaCo4FUsYG2`VyhAY_zd>i0iVIHVOD?O2B|jUdto~X98f;c-v_p|!sT0u!)-}wX_`c;Rp^#W-V_X!o-2)!y! zj;#Vb`WLWoRGq@c)lF>y_<*}Y#pbQY*rv#F;oS$U0tSG?z-@r@j!!E-+Waami4@n(r!1kW| z3t;Y(pyyiv6*rhUfjT)N7gOreJ$mMqq~x3$1OaN+22W^9t~sd2Px%@<~KNRA>e5nLB6{Q51%=A*B{#r2z{C3md^o1VMw^SXgOcDQG8F+KZhwwyD)d zh}c;9K-40Fjb#cgglrmPigco^-x=o4ocZtUV|2!ug*ou=-1E5Saqry6?k+6!)Q>6G88~QZ`T|BiZwxY>m$%in1?gjl{QN8A^>fqi3ut?)`Wa|@ z29`nsVO0lyUY`9c)b>ox{S0JlfP-$A(X{JCz*pKYz(>|c;S@yHJ+h;E3~eD_1?Sq# zT62qXQG+r3JLD&$&i_H6`9vYrU<$v6{&3`^L4oEILS;8j4E7?l>$F`{4}tUGBG?DY zXb0gH*2k+dW!*9>!26u(+@jACSOL#K8T~?d(>rdet-&kD!NDo;K^>+1!^#dTcF?#L zyXWROx&>_Hl^UH14VGt=wlLTQ8{iGN3r<8vx$On?JHV3_)!0_+16e!DO$}1s3c4H_ zBS3x(Xc0eKgKSb32uL1kUbZ>OTxbtzOdL$r-{tUi(mWJn9#TaOld`2Ppt9} zE#P#nN?PB9PkuobSsm=#;ABz(W^*kVrq55p7yd0lmyJwn*F*u^_`V}~jGbEhqMs4d zZHJwwN_unTJ6CKEv5)>@%986F{Rs3U(2qbXBk%`{5QN!E62hne0000 NSAppTransportSecurity + NSAllowsArbitraryLoadsForMedia + + NSAllowsArbitraryLoadsInWebContent + + NSAllowsLocalNetworking + NSAllowsArbitraryLoads diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index b27f6679..2eb8f9a2 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -34,7 +34,7 @@ struct ItemView: View { .statusBar(hidden: true) .edgesIgnoringSafeArea(.all) .prefersHomeIndicatorAutoHidden(true) - }, isActive: $videoPlayerItem.shouldShowPlayer) { + }.supportedOrientations(.landscape), isActive: $videoPlayerItem.shouldShowPlayer) { EmptyView() } VStack { diff --git a/JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift b/JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift index 0213d80d..e592a8f4 100644 --- a/JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift +++ b/JellyfinPlayer/OpenCastSwift/Networking/CastClient.swift @@ -156,7 +156,7 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable { kCFStreamPropertyShouldCloseNativeSocket as String: true ] - CFStreamCreatePairWithSocketToHost(nil, self.device.hostName as CFString, UInt32(self.device.port), &readStream, &writeStream) + CFStreamCreatePairWithSocketToHost(nil, self.device.hostName as CFString, UInt32(self.device.port), &readStream, &writeStream) guard let readStreamRetained = readStream?.takeRetainedValue() else { throw CastError.connection("Unable to create input stream") @@ -255,7 +255,6 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable { let message = try CastMessage(serializedData: payload) guard let channel = channels[message.namespace] else { - print("No channel attached for namespace \(message.namespace)") return } @@ -346,7 +345,6 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable { namespace: request.namespace, sourceId: senderName, destinationId: request.destinationId) - try write(data: messageData) } catch { callResponseHandler(for: request.id, with: Result(error: .request(error.localizedDescription))) diff --git a/JellyfinPlayer/VideoPlayer.storyboard b/JellyfinPlayer/VideoPlayer.storyboard index 6bf3db88..09975b3e 100644 --- a/JellyfinPlayer/VideoPlayer.storyboard +++ b/JellyfinPlayer/VideoPlayer.storyboard @@ -17,6 +17,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -214,7 +238,7 @@ - + diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 4c6cfc3e..1e5b30c8 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -10,6 +10,7 @@ import MobileVLCKit import JellyfinAPI import MediaPlayer import Combine +import SwiftyJSON struct Subtitle { var name: String @@ -24,6 +25,11 @@ struct AudioTrack { var id: Int32 } +enum PlayerDestination { + case remote + case local +} + class PlaybackItem: ObservableObject { @Published var videoType: PlayMethod = .directPlay @Published var videoUrl: URL = URL(string: "https://example.com")! @@ -35,7 +41,7 @@ protocol PlayerViewControllerDelegate: AnyObject { func exitPlayer(_ viewController: PlayerViewController) } -class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate { +class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate, CastClientDelegate { weak var delegate: PlayerViewControllerDelegate? @@ -62,7 +68,15 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe var lastTime: Float = 0.0 var startTime: Int = 0 var controlsAppearTime: Double = 0 - var discoveredCastDevices: [CastDevice] = [] + + var discoveredCastDevices: [CastDevice] = [] //not private due to VPCDS using it. + var selectedCastDevice: CastDevice? //same here + private var castClient: CastClient? + private var playerDestination: PlayerDestination = .local; + private var castAppTransportID: String = ""; + private var remotePlayIsPlaying: Bool = false; + private var remotePlaySeekState: Int = 0; + private let castScanner: CastDeviceScanner = CastDeviceScanner(); var selectedAudioTrack: Int32 = -1 { didSet { @@ -85,8 +99,10 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe // MARK: IBActions @IBAction func seekSliderStart(_ sender: Any) { - sendProgressReport(eventName: "pause") - mediaPlayer.pause() + if(playerDestination == .local) { + sendProgressReport(eventName: "pause") + mediaPlayer.pause() + } } @IBAction func seekSliderValueChanged(_ sender: Any) { @@ -111,53 +127,87 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe // Scrub is value from 0..1 - find position in video and add / or remove. let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let offset = secondsScrubbedTo - videoPosition - mediaPlayer.play() - if offset > 0 { - mediaPlayer.jumpForward(Int32(offset)/1000) - } else { - mediaPlayer.jumpBackward(Int32(abs(offset))/1000) + + if(playerDestination == .local) { + mediaPlayer.play() + if offset > 0 { + mediaPlayer.jumpForward(Int32(offset)/1000) + } else { + mediaPlayer.jumpBackward(Int32(abs(offset))/1000) + } + sendProgressReport(eventName: "unpause") } - sendProgressReport(eventName: "unpause") } @IBAction func exitButtonPressed(_ sender: Any) { sendStopReport() mediaPlayer.stop() + + if(playerDestination == .remote) { + castClient?.stopCurrentApp() + castClient?.disconnect() + castClient = nil + selectedCastDevice = nil + playerDestination = .local + } + delegate?.exitPlayer(self) } @IBAction func controlViewTapped(_ sender: Any) { - videoControlsView.isHidden = true + if(playerDestination == .local) { + videoControlsView.isHidden = true + } } @IBAction func contentViewTapped(_ sender: Any) { - videoControlsView.isHidden = false - controlsAppearTime = CACurrentMediaTime() + if(playerDestination == .local) { + videoControlsView.isHidden = false + controlsAppearTime = CACurrentMediaTime() + } } @IBAction func jumpBackTapped(_ sender: Any) { if paused == false { - mediaPlayer.jumpBackward(15) + if(playerDestination == .local) { + mediaPlayer.jumpBackward(15) + } else { + + } } } @IBAction func jumpForwardTapped(_ sender: Any) { if paused == false { - mediaPlayer.jumpForward(30) + if(playerDestination == .local) { + mediaPlayer.jumpForward(30) + } else { + } } } @IBOutlet weak var mainActionButton: UIButton! @IBAction func mainActionButtonPressed(_ sender: Any) { - print(mediaPlayer.state.rawValue) if paused { - mediaPlayer.play() - mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) - paused = false + if(playerDestination == .local) { + mediaPlayer.play() + mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + paused = false + } else { + sendCastCommand(cmd: "Unpause") + mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + paused = false + } } else { - mediaPlayer.pause() - mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) - paused = true + if(playerDestination == .local) { + mediaPlayer.pause() + mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) + paused = true + } else { + sendCastCommand(cmd: "Pause") + mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) + paused = true + } } } @@ -175,19 +225,32 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) } } - + + //MARK: Cast start @IBAction func castButtonPressed(_ sender: Any) { - castDeviceVC = VideoPlayerCastDeviceSelectorView() - castDeviceVC?.delegate = self + if(selectedCastDevice == nil) { + castDeviceVC = VideoPlayerCastDeviceSelectorView() + castDeviceVC?.delegate = self - castDeviceVC?.modalPresentationStyle = .popover - castDeviceVC?.popoverPresentationController?.sourceView = castButton + castDeviceVC?.modalPresentationStyle = .popover + castDeviceVC?.popoverPresentationController?.sourceView = castButton - // Present the view controller (in a popover). - self.present(castDeviceVC!, animated: true) { - print("popover visible, pause playback") - self.mediaPlayer.pause() - self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) + // Present the view controller (in a popover). + self.present(castDeviceVC!, animated: true) { + print("popover visible, pause playback") + self.mediaPlayer.pause() + self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) + } + } else { + castClient?.stopCurrentApp() + castClient?.disconnect() + selectedCastDevice = nil; + castClient = nil; + self.castButton.isEnabled = true + self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) + + //disconnect cast device. + } } @@ -196,11 +259,105 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe self.mediaPlayer.play() self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } - + + func castDeviceChanged() { + if(selectedCastDevice != nil) { + castClient = CastClient(device: selectedCastDevice!) + castClient!.delegate = self + castClient!.connect() + } + } + + func sendCastCommand(cmd: String) { + let payload: [String: Any] = [ + "options": [], + "command": cmd, + "userId": SessionManager.current.user.user_id!, + "deviceId": SessionManager.current.deviceID, + "accessToken": SessionManager.current.accessToken, + "serverAddress": ServerEnvironment.current.server.baseURI!, + "serverId": ServerEnvironment.current.server.server_id!, + "serverVersion": "10.8.0", + "receiverName": self.selectedCastDevice!.name + ] + let req = CastRequest(id: castClient!.nextRequestId(), namespace: "urn:x-cast:com.connectsdk", destinationId: castAppTransportID, payload: payload) + castClient!.send(req, response: nil) + } + + func castClient(_ client: CastClient, connectionTo device: CastDevice, didFailWith error: Error?) { + dump(error) + } + + func castClient(_ client: CastClient, willConnectTo device: CastDevice) { + print("Connecting") + mediaPlayer.pause() + castScanner.stopScanning() + self.castButton.setImage(UIImage(named: "CastConnecting1"), for: .normal) + } + + func castClient(_ client: CastClient, didConnectTo device: CastDevice) { + print("Connected") + self.castButton.setImage(UIImage(named: "CastConnected"), for: .normal) + + //Launch player + client.launch(appId: "F007D354") { result in + switch result { + case .success(let app): + // here you would probably call client.load() to load some media + let payload: [String: Any] = [ + "options": [ + "items": [[ + "Id": self.manifest.id!, + "ServerId": ServerEnvironment.current.server.server_id!, + "Name": self.manifest.name!, + "Type": self.manifest.type!, + "MediaType": self.manifest.mediaType!, + "IsFolder": self.manifest.isFolder! + ]] + ], + "command": "PlayNow", + "userId": SessionManager.current.user.user_id!, + "deviceId": SessionManager.current.deviceID, + "accessToken": SessionManager.current.accessToken, + "serverAddress": ServerEnvironment.current.server.baseURI!, + "serverId": ServerEnvironment.current.server.server_id!, + "serverVersion": "10.8.0", + "receiverName": self.selectedCastDevice!.name, + "subtitleBurnIn": false + ] + self.castAppTransportID = app.transportId + let req = CastRequest(id: client.nextRequestId(), namespace: "urn:x-cast:com.connectsdk", destinationId: app.transportId, payload: payload) + client.send(req, response: self.castResponseHandler) + case .failure(let error): + print(error) + } + } + + //Hide VLC player + videoContentView.isHidden = true; + playerDestination = .remote; + + } + + func castClient(_ client: CastClient, didDisconnectFrom device: CastDevice) { + print("Disconnected") + castScanner.startScanning() + playerDestination = .local; + videoContentView.isHidden = false; + self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) + } + + func castResponseHandler(result: Result) { + dump(result) + } + + //MARK: Cast End func settingsPopoverDismissed() { optionsVC?.dismiss(animated: true, completion: nil) - self.mediaPlayer.play() - self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + if(playerDestination == .local) { + self.mediaPlayer.play() + self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + } } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { @@ -221,31 +378,45 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe // Add handler for Pause Command commandCenter.pauseCommand.addTarget { _ in - self.mediaPlayer.pause() - self.sendProgressReport(eventName: "pause") + if(self.playerDestination == .local) { + self.mediaPlayer.pause() + self.sendProgressReport(eventName: "pause") + } else { + self.sendCastCommand(cmd: "Pause") + } self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) return .success } // Add handler for Play command commandCenter.playCommand.addTarget { _ in - self.mediaPlayer.play() - self.sendProgressReport(eventName: "unpause") + if(self.playerDestination == .local) { + self.mediaPlayer.play() + self.sendProgressReport(eventName: "unpause") + } else { + self.sendCastCommand(cmd: "Unpause") + } self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) return .success } // Add handler for FF command commandCenter.seekForwardCommand.addTarget { _ in - self.mediaPlayer.jumpForward(30) - self.sendProgressReport(eventName: "timeupdate") + if(self.playerDestination == .local) { + self.mediaPlayer.jumpForward(30) + self.sendProgressReport(eventName: "timeupdate") + } else { + + } return .success } // Add handler for RW command commandCenter.seekBackwardCommand.addTarget { _ in - self.mediaPlayer.jumpBackward(15) - self.sendProgressReport(eventName: "timeupdate") + if(self.playerDestination == .local) { + self.mediaPlayer.jumpBackward(15) + self.sendProgressReport(eventName: "timeupdate") + } return .success } @@ -255,15 +426,20 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent { let targetSeconds = event.positionTime - + let videoPosition = Double(self.mediaPlayer.time.intValue) let offset = targetSeconds - videoPosition - if offset > 0 { - self.mediaPlayer.jumpForward(Int32(offset)/1000) + + if(self.playerDestination == .local) { + if offset > 0 { + self.mediaPlayer.jumpForward(Int32(offset)/1000) + } else { + self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000) + } + self.sendProgressReport(eventName: "unpause") } else { - self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000) + } - self.sendProgressReport(eventName: "unpause") return .success } else { @@ -299,11 +475,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } func mediaHasStartedPlaying() { - let scanner = CastDeviceScanner() - - NotificationCenter.default.addObserver(forName: CastDeviceScanner.deviceListDidChange, object: scanner, queue: nil) { _ in - self.discoveredCastDevices = scanner.devices - if !scanner.devices.isEmpty { + NotificationCenter.default.addObserver(forName: CastDeviceScanner.deviceListDidChange, object: castScanner, queue: nil) { _ in + self.discoveredCastDevices = self.castScanner.devices + if !self.castScanner.devices.isEmpty { self.castButton.isEnabled = true self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) } else { @@ -312,7 +486,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } } - scanner.startScanning() + castScanner.startScanning() } override func viewDidAppear(_ animated: Bool) { diff --git a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift b/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift index 86596090..1ebccda6 100644 --- a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift +++ b/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift @@ -43,7 +43,25 @@ struct VideoPlayerCastDeviceSelector: View { var body: some View { NavigationView { List(delegate.discoveredCastDevices, id: \.id) { device in - Text(device.name) + HStack() { + Text("\(device.name)") + .font(.subheadline) + .fontWeight(.medium) + Spacer() + Button { + delegate.selectedCastDevice = device + self.delegate?.castDeviceChanged() + self.delegate?.castPopoverDismissed() + } label: { + HStack() { + Text("Connect") + .font(.caption) + .fontWeight(.medium) + Image(systemName: "bonjour") + .font(.caption) + } + } + } } .navigationBarTitleDisplayMode(.inline) .navigationTitle("Select Cast Destination")