From 81dc4f1e2ff49807eb20fd6a7552afb5e8f794ce Mon Sep 17 00:00:00 2001 From: Mark Santos <24554186+markksantos@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:12:41 -0400 Subject: [PATCH 1/2] Add article: Driving FFmpeg from Java Without Deadlocks or Broken Files --- content/authors/mark-santos/_index.md | 16 + .../authors/mark-santos/mark-santos-full.jpg | Bin 0 -> 25480 bytes content/authors/mark-santos/mark-santos.jpg | Bin 0 -> 8722 bytes draft/driving-ffmpeg-from-java/Batch.java | 58 +++ draft/driving-ffmpeg-from-java/Cancel.java | 62 +++ draft/driving-ffmpeg-from-java/Carriers.java | 27 ++ draft/driving-ffmpeg-from-java/CloseTest.java | 16 + draft/driving-ffmpeg-from-java/Deadlock.java | 31 ++ draft/driving-ffmpeg-from-java/Drained.java | 25 ++ draft/driving-ffmpeg-from-java/Loudnorm.java | 64 +++ draft/driving-ffmpeg-from-java/Probe.java | 55 +++ draft/driving-ffmpeg-from-java/Progress.java | 57 +++ .../driving-ffmpeg-from-java.jpg | Bin 0 -> 83618 bytes draft/driving-ffmpeg-from-java/index.md | 414 ++++++++++++++++++ draft/driving-ffmpeg-from-java/make-media.sh | 21 + 15 files changed, 846 insertions(+) create mode 100644 content/authors/mark-santos/_index.md create mode 100644 content/authors/mark-santos/mark-santos-full.jpg create mode 100644 content/authors/mark-santos/mark-santos.jpg create mode 100644 draft/driving-ffmpeg-from-java/Batch.java create mode 100644 draft/driving-ffmpeg-from-java/Cancel.java create mode 100644 draft/driving-ffmpeg-from-java/Carriers.java create mode 100644 draft/driving-ffmpeg-from-java/CloseTest.java create mode 100644 draft/driving-ffmpeg-from-java/Deadlock.java create mode 100644 draft/driving-ffmpeg-from-java/Drained.java create mode 100644 draft/driving-ffmpeg-from-java/Loudnorm.java create mode 100644 draft/driving-ffmpeg-from-java/Probe.java create mode 100644 draft/driving-ffmpeg-from-java/Progress.java create mode 100644 draft/driving-ffmpeg-from-java/driving-ffmpeg-from-java.jpg create mode 100644 draft/driving-ffmpeg-from-java/index.md create mode 100755 draft/driving-ffmpeg-from-java/make-media.sh diff --git a/content/authors/mark-santos/_index.md b/content/authors/mark-santos/_index.md new file mode 100644 index 000000000..5a961a6c4 --- /dev/null +++ b/content/authors/mark-santos/_index.md @@ -0,0 +1,16 @@ +--- +title: "Mark Santos" +avatar: "mark-santos.jpg" +avatarFull: "mark-santos-full.jpg" +bio: "Mark Santos is a video editor. His studio, Mark Studios, edits YouTube videos, Shorts and talking-head content." +description: "" +bluesky: "" +mastodon: "" +linkedin: "" +github: "https://github.com/markksantos" +gitlab: "" +youtube: "" +twitter: "" +website: "https://www.markstudios.com" +aliases: [] +--- diff --git a/content/authors/mark-santos/mark-santos-full.jpg b/content/authors/mark-santos/mark-santos-full.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2ebd31315e3c7a683e0bab854fcd8874d4727769 GIT binary patch literal 25480 zcmcG#1yo$mmoC~kBm@Ehf_4);Sa6pB!2<*cZow_MyF+l-Ai{m+%HPuK|b%NJxmtNGK?O zhe7ay*8`AoP;lR|ilDwx)JLPR!(;P{%|@pbt?a;8nmDCmH?a4|ctP-%kcgO?hL-L< zJqITjHxDnL*e7ubNhxU=<*zELYU&!AhDOFFre@|A4vtRFF0O9w0f9lmAwNRH;^Gq$ zlaf*tLvNFyT5wDzt4ZE1%LmSX8%Pm9JpSH$jC^@Xn*xWKy>-5I1Vz(J62R&5k)k8 zyEhbUe&~3jvDuX!7?kWvr}zf;6E6s;IM%7p{;KwmX8&i3`Tu{?>^~IyUwSP9!~lr@ zBqT)mQ$j+5KPhB*LP10Mo6ykF{z>TnmR|gmUj9w5{v$oZJ3)Z=0T)Jvzg}UWVf;tk z|8e1Y8E%$D&kF!dBm}rIA>jao0r!((bl^R)v)?&kbb$XKGt%?;|BIo(PEIzaZxkFT z`iLWv3;>`Enn^W7=@uM=u(cY6P$13}KAWU|#2O?q9NPR7t7X5=E2%AZHDvvJZ|P)4 za%RY?wxV8on#+;-hL`+qjwa)`*P@ACTkfzJR^5TZ2lt=d1TP=cPMJ>Sf=s9oyX`+7 zqrvh6p~Ho^3x%{koR)W3Z7-m^2Rz{BJBYg}NyyGeM+4E{RPLmZLDg2))@Fr22i_D_ zby@E+7iKA5_+z<^LKn(39z>@Zbs4CQGck-`)X846@D2z{wp!K-C2ybp7=j{@C8*lk;CoSN)}1k9+rldoFkjX$Sm2Wv5OW(NW8Sa@o#6&e6Gs5 z?gAUtf=zJeoBmLZOEz6wMzp&2(+Y>oHe)^mew@0=Q7hwKdQdkBYM0V>o8Xhul@pv~ z-ywKxpi@R>PQ-~TSJ&FJ@3Kxqc9M_S&kDPVO~AkaEBCWSdK=eS)?}enLLU6}G~uK7 z6?^3lT)9q^v_pTioH1hOtah=%?rRVFXSCusAw~JewP5T)gw%$yCLsGQWYbfwZ(^85 zx9^o}?d65A=zy!@I;+s+A0=5BueqmTxog zpG>!gHP&Jd>px3b796MKw)|g6M&S36nr2$|K8U!Kb6zLffRa&95()8bbS=-9O)iBt z2AHtb$2ni&oJ7`Xum;re@a)YtdWlJ8nti^~A3|hnH6`7twaZM|Xa;pDbGWBlzNc21 zeg;%GoJ7mqG2iifvB|m_IwvvYONc(utou!S*&T%?l^v>NRM@9U#Fh_zNFxtTqS0>} zpAVag)injUDAeFq9HjNOFM>&Ypj>G1K6&D7!~kDQWuQej4x%HXSFA>e}Pc z7SwFBp*Cfm4KZ?3&qZdZL+`0a#u?uUxP>I%aOmp2ua9EW{SvyRbc-h{EUxR67!F;hP_nDe6YzF62tt*;L!-CJOY6cyZRFZ1MzlWrN z{p5yt&6{R!@6INyj5F(1eeecSL%vuw*NTU&+eQNu+kI!%BGXJuClyRTSAt(jt_9&J zP3uv&S5nc3N(K$^z8{SgVV~cmHMk95rWHlsqHjgDy0H|f!VHXLlm%mWRNT2|eRl`-=idhuhK$=`f_~-D|zbfQT7GC0fEG$28h3suD^9wu{~5~sl2e4^t@Nw>RFBClx9ArsB=sM^k`_{nfEV1$Y`7W*i`IvqOO z_LF^SR--_ug4t%dCD%K4@5%H?LtghckJOhi2)KBk59K=WGG%2|y{E|^<0ET*RJvb3 z?RkYuH;!iKHPR=!S?9BdvNfal%wJa*<#M!L?#`)JR~bq$>X5r18_KklF(8LmtI=DU z#1RWt+esT8#;gGuuy44i65}kq4?e`F6$vfN(aNedEZ%SD$V|Im<_*15X^`Yq9!Jlj(v4;~K9>~=vW zzD{hD&3aLuxZGGiEL)iH`yjNZgt+i@giZDEi5r zS|0Yx*7!0JOLJV#ra#hnMX0Oq>kR{TJ)StBjfX2_5QW8y;({ohp1?PiqqaWbvC}R+ z9>QH9AXQizP2E=LkDk0k_K=su-=cW>ff;?8gD$e7Yw^yeP3n=fQZs(o#k--i-#t5i`Z13EvCemvh>dQxSXfW5Ji!0!7u&YJIfPz)|}Rw z;>Z9&wbbTpdIk)MN03@kIg$GdrGRr7t(WsXWTU7)629g!z651)-Uh9IAlT1*JM(L% zT(qAe{yO$z5E>hQ&?kR+T^JE)!r#KBlw5lj97n`P<{a5KMM+5nta(IgZ&p-z z8yrPk{S5FRW4r@1Up-24PWV5_~MoUeVqB) zec=~P8Kxf_rw*eQll0gK1d&7I8Xhc3KNS)yC2-dUCnRW3WgQZD2Sv9#Je{8lg6voTd{M?ZNmqSr#STt}Of=@Mbuc zdNRpNt82mh{%x>+(gw><0(16cWSDQ?GhjwLE7UX;+&zeSHQ4>jnSO_C;ABt9RrOvv z?c%ZKg>O5C$mnD@GmD@I)y`xqd$Menl(l)|>uHLmlK;m5afTWQ{Am!dXUAj5f!0bL<7!{Pd&UZMWTXm3#s4A z$Py+ZkoC%=ZE4bvSess258rg!yjnVmpT_fw7xpCr6y1Xxt!aX?5vi9wXjD5sX691*9go>@on`&n&6#dzG@lG<1F2mQsmpj)UgM- zrq`|yd$R~}3+Bk5dhK5jtnE|+9xZIJbm~Gbb;`S8+$Z=RSY3pD->`VE%HEW>-!)k* zBm^AMOUadBD6t`7v0fZA=t)gi-~X)9q3PJ<1zL>^r@&VRdJdY7Qajd1Ra45{5fKAS z&Dkpc>^;Hs`wqb!YgY&9Y6WIMrleV^ODl?9O3jp^Pi3Akf24QcjjV~Fv0-c9PBYVz z%1igJ$6q!xlsRvuj<0P-i={h^JLSxt_sP##O`4t$kO#+r(j5i_E_n{7XHzWB3pwfd z+4(LKvWKW0F6%m68s(dekw?#UPeVX)UW$Z%_?o4-rU4EYapPindf)bYvjNuSqpG&?|B$U{o$KgFGjPtV{_$JOw$TB?0C`iKlYAP!8 za>r!u=G*W1p0Cb|k|szPjB>6M{xexG9^}v5R~{s1KiF()q4x}F3f2=QY1>EjMX|V6 z$oAYRTQoXyCk>Ahu;PXA5|Z7SlAc|FV+8a!j!e5(-X(Vv=Xb>IYw!^Ea$Ve==@zb? zglhWF^;mIaTYuT0*x9tNUkf+U*Z`JSmr6P7y`BM`x%>(u1A8mYTRLLWnGv>=#Sa{op8RL{!+J&`!{*<@ zH8q-G1raoL(=?hQv}e~i&cj^SO_S)<;F5Uu)Q6-?CMjVj#{;0{UEY}Sr;c%s(*z9ORn4Xo!S&1c6&J@KM%AyWQ2EY zxG?@EU5&AlRg-c&7@?*jWQQP@Qb)G7_eqM%{zbc1T-SUDbP9j3Q6Z$};9m7!?ia3f zM?AUp7!o-h+S#M!$)e9Ztiyikv4n{pB#@P+3Y>#ZomO=At*yj<@6N^v=mjiK6$rk6 zG?4QsO7snL`6(d5($SXJf#Brs)kznx(?kmZKyMVzs_Shu(bLNAtG4pxl6TXvxX%>qtrm zi5Tcgbv{HWQ2QSGytv8g_C}v>=C_@YNMW`O@+Aeb2tFR{(HSJF)3uEK=v^}wwJMMP zIQbyG?)Bkwe1Em{vD6n|ucLY7Dz3LknJKhIcd*8E<^8(|7tBv=jRlCO&5nm`*oB7L z1sU0Tnmt!>LAee!;Q{Hs?6+z;v70`fo!ksABax25gfw?=&wVm#Ag-%Fh z!G2r{E(;lD@^)xDB|rMrs0etn4f9~un|$isG-C>f62A9He5l1l45!bkzPA^U*lymxble_m5TNLgFt3I*#T{L^wrP% z(uQ}R%YG8@+#*-Fd%W0h2EZ*M+sW0(N8pEkG!h|O1nUEe4~L5FUF}c!n$dB(rK+`W zW(D(fxn+6%8IUvv>X(;ZBenGsMDdb?-@u$R-;ceuOafH4ljEe@>ApW5?Ww+3f9(sD zC)|wd_>DkYNeC{ezKAnK`~(2?9AqY*xi++#cs*%J(_8Fb3S-|`46J=U_vIN~10Y55 zRE!8v7Y-17{#GZdfOf06-tRqRmhXK^Xy7)m(OA%;m-un-F74cCdCWIgFAvo@n&$pdmU|Ktgs{A*`tyckWq%w>I~0tD zZ|Z`dY8}9<9Yp12am#Z6nQSsxkj&yKjYzP$m6!0iQ_;W|ef`mx({Zb%I8K5{K~q7o zC(pr!jYm}-B8Wy?R5oC+l857O>1-BF?J@hQXH-%wY&-4PMWmpbHNTGh)Zs+^`l{7{ z-7v!Wz%jby zn5dHC>9BiBjXKQk0`B5l=|juT2W1RP{&*IYcwXA$tfGsG2Wm-=OkG298Rnfn&0I50 zu2f`~@Ob)SfMM}qlCUpobE$Sq^}UqG7q#I~lSs%-RR<{QmAR)Yo0gWk_Cn;~q{7Qv zFZ(<$ull_`J>mJAD2>C-x;Rndnh#@zRyZBd*d>V z=_4~!CX89EfVG!hFKiT;rJbOxmH6l*QYX)JB?%kRi(IUSlRU+6rUwu3Uoyh;EF5%l zo$l5ysdF_ju2Mx)0&Z&BALq zrU0LdTY;j%v*Z^p4w=4&oY&6ldKBJswGJbvwRigq2m+N7>!>5c4a)UVJcRp_GPjv5 zg}lc2(_|TK>tmK7ruZpJ+sEF-pO`jMX;)78Vmc&6cj6&ah%Ikqr;VeuEPG#W%XEGm zYA5WSfwt*+GxQ&20JnEW_+{Q3Bl*?iWWkOZCLTDhAW2CT>ahqaDuSB=XN!b;dLx8` z^6$C4=YxyiIz~5ogNCQfD`mh<`mgFE96?~tOW}|%aXhU%maWS47SGtzQE>tUX~1uW z{f}c7cZ*$rVv3uwn|c+teJ~+?8@UxByF{K4`ZmLP$3UwRssX|w(_a>QOonuG1|YY6 z$gQm>#?B=4;&(#ncgj zBF7*xPFyNUbxFR=mzk7V@Qd(75Td6> zl!wRM;mOx(IfwW1b}^z+{7Ah^7x?G3HLpgKOZ&UFFzxXW*~%YyC}Ed^4ErZhG$)EP zt8yM_t>gV0uiJ1eIb2Q!=Gjg`Q-E&f|qdw3UaJd$!CIx~AQRYON!Y zui3S{@`SdqJpXuKu}AOe>3QOujqbEn;+x%MmAzw`^}QM{2zu-{BptvH zEjU{0doJ8;OTW)!DNT4h0d~>7ZN|?4)2yImA;bx)ER|4$#>RX8?;V&qJio<|9<=7G z^z=Km`IXs+4br?v`RZ;zq{2wW(#22$tvE-oBqzCkm)BHt9Zb?qvR;V4e57}`UYD&` zyI6=!jJ=6ZoU2wcQmj|5dKoTGvo5hNo}%+ooJO1o4*Z^+c-vgtoMAQ5f5-cO6eOP^ z_qoG7xo#zkyDOG=E(s3`DOJsn<}PNwcfcs+4_AZU zg==qn+K_|;YxX8*bgc4Xhed9_HBg#EWA_d;p0GbSP}gQ6t!oXF<8%=AwOL$^g{FBI zf@}H~!sYOArRka%deF!0B?lcI4*H(RkVCp}RLzxM%Zv0WuL`;z=?z|c6n(JTLWa*o- zX81Kx-2&=LS~abyl{QeNs-jc8xx0sWWn1u({&I^GRg4~HO2897;a={)3mX}6l>B5q zus;t|x$aQ_?OiK;SY*!QVZ?hc)ql}J64LNWMl~=tJF!Dr0)Dn~`CiC$`Q2#^6B2q;+9`w+I}=pwPhnf3sNn_;`CPQKlEt@A zjX`_df4AC^MSAf8M@y1yxN>ob(yy8u;m4Z22V|m=ru9!rEZYEyp}unLh_{r;ItGUe z>^n1(mW&7r_6v7^?C(DDdO8m@XEcTAVWS%o@q_y3l?F`;*@A*q>Vxupxvj~q4JOgG z-YdEkgVN-0`rOZ>i9JPXCf3Z1-6#?}5d6BVuLRkTd>KkKE~TvkGu6p=&?fA3MWBo5 zR1wSa9hu~-lmM})`RlU%*?4I#EBQcu=U5o{N3-FEX_a^)nx;cd2Kb zG&ZN!YLrIIje=!6GYdhEnM`xhHbFR<7J@v)MdRT75nYH_0|Wq6+$Z zy6b3Na?CS{6@`g2ZVcCwzqGLxG;do$(P>+C!K>=y7qJ3}KJN{hXDH-EMg089L406= zIMzkpk_FSFm(1lx%r&l@ULen2Y(5QvMMXBdI?4+wU(1SG?6>H}gItoYFuf7&Z6hki ztj@4IeXv)h8Gi>E(v6)MF@ODVx^`7T=E?L7K+g+CXE8|lcGgZCBZ>buP{6JDfG;8` zyJyMcl$nB;>KQO4j=Tn&+i01!!rXVa9HGAy{ivU`W~Y4?Uz6uF7(ue_FU<7}Fw3CM zG2^t&?RWfY?*M-y!m)_0pQDes3kbg-LFczi-rslMhLA(sC{f!!J#p^h?CZX4lk6Py zn^7Re8;Z_`f+FFQbadC5k9k+I6p<=#FR}bre1WSmuzbyd{yyD=O(o1VjafYiYiD2h z@H&4tJbO>iDr9J;oGHfSQx`5O3$|E5{DGe8$pgch55i4+_g!5CBgZ1GdiA2Yz&sUs za!f$_=4$}&-iX&~8|`FUZZp$preU%Su75;vb@hIEuD3fV(lsRB#}t}PH&DarpKgmrn+>)lh@P zWY=SCdi(_#E$sb(F=3{-%fN?$%s%V^fg!BC!v^SWR*d^`Wyzj{Y@S7fS*p5QMNoUHV! zc&8rDyN2}Y;I%(nzY!7Qd-)(|&PcD*wbcyo0=PQ{aaBkfg_vF}M4%(_(N;!5)f6j#flZPLw4_pOUX@2GmnL6tn?=;uZJOW?a$Mg^ONGrM05*=C| zWg&ZltoYZ-TAl$5s|u}28oDI1{>G@w(F2WKhs%`_kOD1N>*e8VocZ|?JN^Jll8?vt z9uW@>*I<$A@fzN7F~)ZWU${aP`;T8lq3!5q^79pH#udIW2bySg&Cty_(aZ1No_snj z`=hBX0mcl5Y}Q9D564!x3mEx2vmPzxJ3e72UUWM)WhJwh^V}?P;EFB{!Y2W#nL#3jGKh#4rvql29X2#P zir@VX_hax}{T#&umOm{!pou1Jj;L@uIMKC)CN8>Iu^hZ9zPre$JlFM8U>MbN4!2ho!I+YK2K@m_)zfoK!)?R)DOsP`i<&{om~n{QJ$(4JOkKN*^0jVsv%_xT8?*{x|URK{%qJ9 zUq7TKa_9gNc=F$fm+fgr)Fn-{RDa*3M;r+{WRH?yI9#R`OCuxC?^veDCOzwA)fq`j zwz4Fqr_-q?V@+2q8A2u~kHJ>%yWOtmjBgTDpNb#|*@5&}0VO6cJ$N8nqMLTpIC@+U zM5i2SKctxnU%KWy(R*vZ>OPm8Y#qEQsO)w^0s>gsK^2n<=V0U;X@YDM{mKE?oy7Ej zX~ga*yAz#?+^=QPz)T=p&wHVeR#!B*{m-wkG}lIAh~Rmk)4tG-+L=?Ee)=X5plg$D zUW{5Xg|df8Ajm5y)Z(LgB0?7ABX`4;ET1IH`1u*IwVBaEQGUDh0O}d;Hpu2=c13&qI1k>iZ^8Wo0>S^bl6!8xL7S?j7ngO^1<|a1$??n z-;L4A_}B0i8~~?(4h>y*dKX? z($#hqf-lLhei!M-{5qS;_cpzuf!Sp~dGI(HLz2E52#z~0ldzu{LFul+cQG!=0P z4($_5U2V8X6;#99xC4FaqEhO9Qvq1RVvv|!;QC?|NfKfI3r>q`7bH4QqNfAl8!()^ zT>UbY--E~wMy|*dFOJQrCJ+~L5NcUi#|;tl82ScTdwH+R6QVblmJs=fv+07V7{)Wb z_U`##7rGb@2&lN>r)XJq+40f}N|w82+%x7~bm2sJx!!p|heSFf(@I*u+p~; ztxW_?DXblWH^9WK56Y@u@7DGr(lm>$0bfvEi7Fg=Lz_>=mTcXfNY#wjO1Z-*&B9)y zU`pCJNQQCP2`GJ9V$p^c0LryVKEW&pZSSme@k~&{)6g5wN4# zR=c`5ErxClv;h*pJZ-~>7s9VnH&L;{-_V81!n`2b?D0l_SKVfxEDK5HYCsAAbJK^u zagc?S85LFDV@ms%Li#tHUXQf>E#BJ*msA5$ES_C2%kVR-qRRGNH)E2@M~0$lnoyBm z7PK|XUQb0AD(6Xwh?hVlFTSsRb~DbQ>mVFv0uReN{dfvI%H{Kh2X=LzSJi#>kat{X zI4C99Y@#grzC=6uvS7`TQth5Xs`L?NDxt8vq$ia_V)=L*3yR7JKU3WIsR@R|?Z^SE z@j7puBcm9ZWM!13PB_^9kd$7I&TrmgJgp%SUf_>oU|S4UA}}cvlAPa=A~bJex7(>+ zKh-==bP^eK7}aaDt6Jc+m8$tfjzL#whJ_k_8nC`?w0FLcA39~c4XLGrVawB3-03_6 z%Drgct-~Xqxc35GD%X=XxiP%qT4V0qX{BxHUr1eYW3+p+Snd^@uX|KhWq?s4FoNbm z1vqXuA}{`ll7yyP^-G%iWVpk4QHC{fffsNj{)5XOo_)`gcZE(QRU==Q9MZUfmyaQb zV>=ZFEBo9ka+`_`MOXU6TFbKaGlG4yk-c!Y(9=A5%&B0PYj+8EAFMWZ`O9CgiMi7a zIXqalnXNf~mSjRBlurI$F&*bReO>W#%e6Z;gZCnIjN@WOT}A3vqAV16A?&x^>d{ye z|7lvdb|a9mBC(v4uz#&_2RnM5D(w!*tpixIlgU)wRBbw4o?{tq{@dC>d`TtP$1N%k zc(WJwpw+AUraa}FIrKDUJA+}FX-W4P-~btaROnox%=hb6KnqP~q}Lod%&WbhIMoX! z9{YT@d`RpqG~$fAEiCWKa08q<$wZnJ?>^PysE^g66}%w?Q{EIhy z3EcAMAqfSfqfnH)iYXGfXA~#LkCX1CKLfG`+mcH}2IH(ijpJ#Bq2_`_ceL3u0{J^+ z*!Ty;+5;bs7m-O#(KumxL6>tHhT%RVe)U-O8!hqjGfF7hTwiKk!EZAc*bkxRB0w;G zQKX6pFR7jO%;t}X9^V1NffB0?!Z@0ht&UwjgWplVY^cr$NH9j-XTByT4iw6jkFneY zZH`WejoXkk`b~Q~ZC~*v7Q~kOu@J#oaW`Stb6ZP2Xyn#$ys=H`Cm#pA`j{)gMl11I zU5}NH6VDrkNz;}&qya`MeYMKX%SXYz^*!GGAKeDo;=11-!%k7#@^#dCsU!y7V}mK% zgnVb(@GObpCk-NVJF~5Z?c&_kc4)g+qwdTnbJKW@J_920Qn6!)Fb!{2Ln6Job#(TW zy7I-TCS_PEi%HV24!I&AdICrN69aE0l$|O($}4hBD=eDj zR4wvhn{lbYm73e=b!?F5rzxA}aU50@OV6}+b77Vp?TVcI@P9#MBM32(cC7&en z>xzeB{U!JSML`xc8fDzlo9v{#;$^lZE^NYVuwu5X!VS{oCKgO_c*~6=-B@BHRBg>W z`WKGcOXhHd_eLHaz@r0$l@oU)kY;i76~|k_zss0#kD?_(ivrl~L_It#NU3WQP=pI} zSpUVf@blCi+yS_iWa{S*TPHqd!5s#(!A^?ViigA3hc3yKzujxCn7d?Y7H$Kl-GZY{ zgbY?-aDIwAO`uD%>Mg5{TmF(oR*E@ynqbOp_|GwSo$cb=;AobK!=7R76*-a-W!NjA zsr$bwXXEX9zQV8d^ACfD^<968R?S?<4!uEkOQPu;M3u)@)eK)FbPdSMR9->^?kqJZq^V&y|Lycx(HXO z>c<|7u(F!(b%30L<*R2d?%gnxU2OFw5DADtzWbJTo=)pSw%LV}{Go%<3dN;?#$*)Q zRw{>{85_d$t>(s-BgE3=iBWoH$fWMDz45S_)57*h37W`Dw=HwZH2`9it04Aw7jRv5 z)>~Jl<8kjhn#5aUyDEm8jojG>{k05EYGL!}QF@u2j6USE1NhcyzLmUB(ZJWq561!l z5;sm(IU!DUYB3et)b^X$Pc`mBSq8sFscPB@0mTQ;fP)Ge81zH%ca_aFW4YWHqk}xw z%RYCPqo8OZRD=0gLsK5BK%Dr!G0mMspcPT`Gr;k1%%*OJiCORP89?VGfN9V0M*Fwl z>%le#f}O2{OV0DGP$3>OXK%`>&GAdZ^rc~3DHm*(V;*F>O>c?TxM&^A5q#F^BI<@w zbyHXwt$1gzW992&4^sK;EBHBIYBA8%WW)P%Yenk%MrWP=dNTM%x^&~00Zgn-(qKaf zy*mU1eObRAFgTd=iw7yj6`JL~m(xfYk2m|rWXwO8)$Hhl`F3otAj14J;15K9n|Q04 zBTmaKsgh^GnV|1}$K|qom9_K{bse)|?ff$hQMn=d>g>W3?U^tRRF#&aTTM&G{vMp| z;6*a#Ubqd)Cwa8o6HMK3c#D+AomRL_`o)OqACR^tyOnsuG0EY<3VX|)CUt>N?Z_2YKrcIk;wf(_?m z)u0b2jDUNhdB$ry*^4xLSfTZA9S59@31KyKgLirl9|nG3{V)B}3dg)89h{DywBgi) zSLP=0hLemi!zpa|=jIhlscimNM?QFPX(3qHEhV%(d{>u#@MvN&hcs8_}*;DEP-2c8GDhk<;($neCR{ zyFR@h9nZ0Xb<8c}`FuW@_rb)S@C}y_<;WrNs@g={rhSLQ`I^@&7d>pSq5sW7E87kl z@8}_9$mgYi?w#?{3h4|Czo)_Ms@5Tqhmx#Ot;WaP$3%}g_xqs%CiWWZh!5(98OIWp z&$n-O(hE64KOsV(IoE<^@>WKslw)$IvC1bE_^&<`{M+!GnaVO0KAvGt?XsuSm;6e4 zw}car?he`I4iu2KsxT`V-1xxDW!2&iH%a0k(ck7#P zd~3*P9`k!ZI1T9$qHX#Ub={4P_jVk&>eH{^DW>*`g3(1_pK)QMXaWmj?;@XdicJ0? zZhZj#PwFr1d(%;TvlPp4@_+wy$jWv$em)NOb4Gq1p}|l(mn33Ke5W3sr{Vfd)@X(e zvGbCN(NYt7duIh#A}GV%v$(0HIqa#gn@%@;`>8S4L-H}!hH5HSy}F$x>iyfsj^( zph!FC^or}sGeC06rlPQID$h(;@&|iBqA&}H!69PJ1)4T1tah&^uyDc7(`NFdmC5_z zu=4c!Q;5d&rPrT@AJ}n|Raw-CTd9BAanT=2`-CXqY=HE7hVZA#-@!zANhHBR6x7W- zFS~5JW3Uh4A#|uWjYV2J;(|s*l#6VAVqp4fKzKb!30pud)Z$MMK>9@}a~>nb0I%q> z6j`u`a)LokSmoh*y~`VVYu5%ka5pU4Z?y{5|2kQS_6$$hyg4?k;AoM8%27G9^x!~G zZzM+~&-}m&d$T5W~S(S18a*nbcw2`+bd`Y^LuHT9%{;j)(*m^lX~ zsF5CZPZbR!X7wJ>AgbdF_sU}trjlKZ-qLmQ@p!DOSlA1Ng@T;dToK%8PkU5bW^@-i zhKD8Aq{o(NC(9mqCkqm{&&bwzQ>=V}L%mW@qHP~fVZo*ureAFbJVJ|Q9~kudY*KHp z$?P!ghb!{6?ng9*zz=nTL0SeuU*Ny@6J(YB3*^Y{wif712Fjbu(c{C)=Br#UwN;5b z&;JDCi+>@PWJC=PYqKpZgWkWh`St$|#gW@laB|g#<3B?kF0lG97r+f~3x>meVvw}& zdFE=2C5?MVn6(H-+}6O0z0GW}4U@Pl+~x!tD#9s+F_t?e8&2gB3kB99IIVkjY)u_w;GxcU3_)gNCD_{`6V@0MiltuTWqhR z|KYsuyJ=y*5Bg2KZ=qPIog;bqiQY2+QTnOy(gq1iuRz@eNw(zPJ@gK7ec1yM0?y(t zsF~Cz_(3=?Zl-h3yU*$dA0_S4!M+a+8!zRY7v6u1p$b60JA2~1r1h_P+fbukV82F6 znl%*mFoYvBy0dpfDwCjO%6ayti)DpDS1Vwv%7(t>tN$wpAm~i+Dub=h30Hv_r}8Wj zK#Ikmd3lk)5^}X$m%|apYuwBJguj&r=}Kiz87Qoq-=eji9>pNzGtD#~FEw&cV0B`- zVFy+mNEBug_=$*D_^Gndq=nQ(d5iLir6l4`+J( z((7428b4>51m$LOS!3ulTv0g|F6;@vz6^aD+M=#_iO2=~c+%11SS)sDnvV%Zn`LVo zzR1G6nPnKGS(L4gV#;!gM~aWC?`tvL@@9OTUDFU#Q*| zbvQRfkm}Pv(1qsS=#of{2O@nNdx2)rTjRm-X}cR*sN=!I@ToJHf+$~<)>@F7Tae=6y#OaXNSD4et0~S)L zHM#}2RSBRB7Ay7dL@H{-eySaqkn|M}0XOdQQw zdMzY&9%Zn7C(!jbhWcUeHhgjOclJ-ij4=Dh@O@tV z6`jo;`OCxAHBa6q!bRcnHaR@>zx!hJ4-U!|;KYH)2+62}iooBFwEc#Co@Xz0`t~GX z2P$<~4UcB87KJV!q5VFLFaj51os~p2Xa0@ImA^ECrCZ+eiHg0T*^1y_A216&kgdPs zgx60ryTftaQ@)c=13gf2!PeyVnCORx{O9J}q}wIO!0M)!UAVvsOm11F4&Xj%4Xm~& z-(foDu;x^@z06-dk)&$~=Z`KY8M7!+cYmt3?$6r@8Fn4(^{}ZC&n@0(2Az?}c^+-F zA&QsH;Z!kxeH7`lX4tg>>Aq6v=FYXDPn2w+1YNN->xm9PMKo&WpYYidJ zNI+EfuzQzv$!;rtMfF(Ieot#978f-V=xRu9T2E>Gi&Vt)*Le~5kb&5`PywWpSxWP1dL!~#jK4rII#g=95bFhW#soz zjqxFij~+c3c`3N3g>o919`QQG4tsuvY)pNpc8;N2aHU1Q0*6!2=&3cj2-^z1&16;e zbc1?{^)=R4Rhw!Nl8`(DS`h}27Hh;U^@HRB5cqIeV>KuJ1Dyo4#Z5Kyllt-=c!Ina z-Po`%%}RXM)P*NoJnXf*A2r(?GbS*x^np#D~B z1Hr=A5S<3KCz){mv2v2olBN)_g1gTAcAMg+5Ozvc{SHmCUCGaoa+H=`I5y@TR^sh0 zOPYL7WSdt0o}D-rs%{!lft&F%-1Kh4wW75rnxih^wwMcl#iGGkEe=%5uo9B-e@yi+ z%ayleQM(>h4eyfFZ&Yi^Y{{bH#2s$tgUkk_&j5EMV2zsVL7L7#IC+L#ym|_bx4kT4 zKasA-x_rFvhfnE_>dPmxZ7ZeLct(nc;|@9fKJIeuurH=7@f1;?NOvpi-6xuzIrS)# zg)ZRW5RbgcgD=2a*JZQdOnL?Zx5uOnutv`|f2@V{2k(T?dS-P~N$vW9gyiQ5cy9#U za5g@|sibE{#K0DfmlnVOz~{B++x7l59aGJ!+7}oLCaQ+fAM?^@RSIPiwD$+7mv^-( z-?yqWv{6=NZqvQ<2wkGGD>gOZc~Lq*b@$??L?K$2=_c}w^hRubtDB2YF1^g8somT( zj6HZkiX4^75cJeWcXM?Ad1&K+7rxsI=7Pw^BxLRd4_LoNwgR!zhY0y*J~C{LA-MK^ zIh7167mzLN_gr*YrpWwMS+^?Izb|-jyVE^}!=O^gh$^O10n_o!xpymn#Gb8Z2rO1E zB~`V0S-SUjY6Lv~s4?~3T3I*axK7GmNg9K937fjn>jV~drKR(pm}l9=^8ki$ExEPO zJk`m!9ShYFg7mmkgVcV|D}Q9T`54Z4)JzS#2Nqq)V` zF1#^aXnE5qT%Gm9Q;?^6dc(v|<{9vY6~*h(ZKE~hEXyBvJryXNvRP#)1g-Hb--r%T zV!o34^EK1$)#YB;W${dqxJAgwY}a(IkOeM5Jnx*m>91JA1X-Jk3cmqU~%pGA^O`9Ud%816Vezh~%eBbE~ z%GjzD6`{>>`dzsb#kmb5Im>)&w6%NvAjUE31eFl6*RR?n09W2DV7$|OOk_r2zykb`A z2eo+%me#DE=;2s+3pCx>ow*786ya(Z)2*fm+0SIn&OLGWZ3vhPEF6YEZx#)eY79cME!GA;I9(Oj9kV4`9F7Q=S6`|ymi8SL@REOd@&(~4|P}Y^nddYJn zd2Ag)8gQhkSx>R>*+1mJqxwA*Gx0gR)nX3f0^R-Zm4?m#eW7%P9{<0&sC6}hdoQx9 zxo>t1x4sD@T)scx=>vBU4&XEiCBfaCQKZdvfCNOGlM}_e)^a33B5`?bwI24?NYWf8 zJ){kKzT-H5LE$e1Gm(fR-N0G>=}CV@D0$WmouFGB=%5EWj-Z zws3T{Frl)$A+LLr&80=jnAy0p_L3)MwB44q_{>0O>B73&9D3dR!Rj;OKea7}I7Hp6 z0*ttjAw$5!u~2T1{qg5l_Wxwu5T7V{cs+^-L(-4OZCG5g3;Yzn)G+E0hM<|>klEA9 zB10OlRhit?q-GdQAY%KpSY_0!zZY!^A;U=ZR>pCHvfjXtG6{Rb=@$-8lg)%zzMVBZ zUlVd#hbM||v5MwL1K~*P*_NL(ay_cXs1-LM4U1oP4Y-*Wkhm{IyJo$8dKdEq)fs*Z z^`b;tFPGHOupS>?ED*WI3l7x;RZiPd?Vv2H-TFlsc8as=b@GM$;?8D3>R$Tk z{zwxI!t=EioZLLu3xF@Z#Rs{U3N*n!Y*EbCu)NmP)DMPJ@N_1LVGg%qCp>Pey8Y#G z=$uoj!cAF(?S`SY(YLCeR7TKK+#W#`7`Kn5TOF(9a-KKzXZc`c{}4eZsY~`TGx|aB z=*s2h*gY%qYsuB@GdT2ZyNLa2<-6{rBv|G9EE~ZA@!?UIL9=&RzFP5{Gr;q{9J*$h z-o5^P!eHR5_9afYD8B@<23lH2$v&+7;v{;K1;-ag49UL)Sc(({xKA2c99(K{PXRzH zemKkPQ~h;fl|CmQ&Gyu9@-kds$;lW}s{6xpL+gbvcKaIDdM5=l!LM?udEJ~i)mafB zHsCP1&@ya4=47>?%IWC5ih|l!mqJnL+(TH?#X+i`CrV;Pc`=8>ygVDvW?Crt$W^s_ zPnq_hxJ%idfIClXf7ZX25U^wWV4}j0&_6qyH1zU5E^nH0H|?7tlLv;GGX#GvXBE+% z0Qa&8it$1`NddwQV>gO>VK%7;?{eGi@+zw;-an~h`RJCkgH27*J-2ZBqq#JnMP8&Q zwUGdwiE8c5bg?=SJE~|r*ume_zn09XLq_37mcxLxgD%<62rqy9&|R0^Rxz2LT*b%P z&6p7GA9%VKzJS3Ok(^kDOsM7Xm^JaB z$T$~kB`FCIH_QXFunBAW{OakGnW>E$sT&e}l*NH;l24XT_uaOWFpf8X{bqE9Kl)HEe75T1FJwc6W``EbgiWxqC*ZI3v%l-GuF zw?~w1>M|{3ji)8b8Z=Eqsx4pbY+{M3%m3(4tkLr)?~S7SC8f_%0L1tINM~5M>%dJZ zS@^sAiRce6JlvJ6dQ^Uy0TEqsCwbl6Vv`}yijk=xog?a%K(8ZvXaV`Uk;@Uy zWkt47z)0)6$UVPNU|F?DK)#*I55g~JHVSNEfZ?}SxU>F>EIJ8kSM>+`5=$Pl2M*DI zxm(OeAHE+VgEV$S`VD$iY-g)f_+g^;f7+(Y`^4px@Vr3BqZ04ma?cO5?&K(o!YtxV z6e=k~H1?f4=U<9D|33Th)3mE&N$1|w*1!n7JnS>^-bEu=v80T{zst>UydrkSgV%OM zxtTVl?UX2X5co}OXngy`jJL%ZLKi4MhhT`Ea2;;MElOt@FkkLi`#IykF153r7j{i%Z76gtdKT=~md zPyCZLZR}C1-4!Htg5Hp9?=(1V;y~NCo7;o_L3xS3+2l6%r zbltN-*Mn!c)12UpJ{JMM`%mG}%;O&mJjEcH9&djwGhQ+h>1mB4}FW`W?*<^|tL z%}F!nALd<*>ZLQysxGovl46#+r3 z9(cWtEhM#KZc>HUrgU7tNPy|T7N8ly~i^1IK2s2s|Lc62PMA8I7neQ#&| z8~6`l5L%kIcLj*^9j3P}-%0e9&F@HQGfiW|qts3&aX{(ii5HP|!0uq{(95w?!s!L} z7uJUIL$_Vwonz?sk^N~%8|Db^OG3!7&=cjOipg5SvvB5Vp*+SU+EE@+qqh9#(aLF| zPK*EI?QZP1xXOH8g)L$p9fqVh&w@EF#_wXXzCNn;{9CTPnhxI|C4Naa5} zUVu!LUQ!RCig%9l=R|)Vo*Fl`b$&XkSG)O%Z&-_~@HvgYo8Vkc{ZkD$gH&~u2_l5&W>-7Oi8Tz^BA{PgI1MF`7 zhn4+W;-=vK(OD?BbY8>dE!%*O7$qBbcHa@Z>|!`;7}zC;{6X$|e|_etohk_S4@|u9 zp4zs*c9OR&97BBOJ!!qY^*P1)u5C5rxu_2?*m9K=iq5f59kS9_II2re6yE7RZ66Jz zp7%VaSU#jFAs__hj@vcP5Uv5^N8iWZd^ifDt&>X;GP5&y)QK=R9SC-2-fxc&S$<^uv9r$s5=!1e5)C_`wIRl$HXH3|{s_EgC5HpFZ31#XI4mW6J*a z*~61R!Zs6~UmV9Q4-)#M)i5+N7vhFme*6!tml^J6R`TX(cT?#N{eCR zIMce;{h6$4e?X>Y;-`eeO;^GlwVS2)=v(iBP9kT<^urvvgPlYe^;poM8G(1t}Qj1dV^HR?qxWmdut2njh32UWnj)rNI z6=HHMiA)J?8Q24)hp&J$@IdiZv>qHNXAQ5Nb9<&_%o9Vde2~v zpsa>#gaXfh0r;arN_rI#6Ghb#M!s~SzW$MS{?X8A?+3|yQ@pU~8dNVi2At!nn$plq z){M#YN{w%tix=pETUwap$&SyOtN$PLtSQuQD23IjEUu)k;j zC<6)-2TDrVKV|DaD4j4qG%PtdhksZ$py|YjG&RT%Z=?;g7>JqxLB8| z;xY@roC~Q?b~h@64KK`&WamAYFLd5@k93c~_!%T#>)6UxpZEzy*+8^3Yu^esIc1|- zIpO6Nq*Y9;GO070lrNG)Sd$KQ3u)6@uW5oy#+5f1w~%aTpy8|WfrU^P{f`GEPOh9Y zD!ia2=28kP9{5Fe@gGo0;z8#&=N5-q+}YrAG6z92cX*fP3NQj)XnGJ!VnbxwKPudA zZ4eXRS}ZUBsfm3Px~$x!m`Uv3XN{>-zJp^`SSXf$;_JN{3;A5d9pOXX&a!XW#pWbq z_cY@3xq5ne=!Y|s(ud!w+X{>Cm=nAos*%`TIj^zao^b_8!C{f!Q5P2zc~J3Hbfrv< zl#3uVUEn|m5?Hbs73=M-^~UQ-;-y1#F`ij8xt5kuOQ<~+n1ztZ-U&N)Vtx&i!d&y= zCvl{@sn;GRkGG0K52bhy3=6>F0SBUeJZUH{7fD6hP#1Q|hw}^t(iPN(L2P;7ke)*z zCX~Z0qFnbOXmQ_Y365M<6UEeIIcFU)3#(9hMH$ANOPZVi;f#M3?OxU8;5%iRYxG?mC zb`1^=VJpY^{Y5KK?NWLbb0}`?6PCw^A;`y3lC!bkZ3aYpSO1fG|Mi1Ou3-Nn7~!QH zqkON@yr|}Lww!!!(N}kC7TzsHa?~(BV*^9CEx2iP7rP|GW9Zw;u_aj0Q5A= z#cwqJZ1DY5g+CC61YQ9+3LjCUK7rVj@vz5NOK-(<0D;anS)gtO*+O!PS?2EYB_XXi`wR*O0 z25-7OBV&{UIZr;FzwZuH2V4OHtvsli3AfIA7=g}1Fwx!$%EK!k4CuRq}OGUw** zM<%A%1gZL`&aTuuA2{D!F_(Vh<2ia_@gwvzkt>@fP7~TJ+tuAu(5mOa3HgFyB5np zcuvg31s5&q}>yM%tYw#^kYCD(wNECRyX9S{i zAwm>&E{1Ff7Q0*9#CvT);{d%_n9HwVUm0NWwY_^n$Cd)(J(hLSp9Mzr$PWE?Vd#$_ z_8y2!tlUdS6)NdLfk?Pve;U1{p-dLbkiywf;bTWttK{T~ELEFrHV@sJDdej0&-n~| zt~ydIxsW;}=c44L!J_=8>)n)JKSX)KxwfFsfDfdGbyA@jFJl2#*fuvT1lfS(HB!1SX@CXlV~`;vRU7ZCUy zYN1B%wNU&~z5{2{bG>g@)o&yTIhX{(=PwyZ_3fIFFQ&gpom9pyd`lz_ znMZbl_K3D!__Wps0>&k*K*u7Pzrfxjtpf%mR0qMiAc1iPHoNyYQwQ~q=4+?|{mL(U zTCGy_S`&x}&4omq%4zJ)pIf}!gB}5vc8E^v`NDd4!wi>)y^>zZqaVYjD|{n`LBG%M z=y6iQ^c}t4!-wi~t$kcI>*QQC3K5wPOATcCmIQt9$~O4WmymL9#`*E(u{FEG05J?@fvD;KV$pH%^g9LCyfP|IGN|IX-wh7?A>;HhU0 zw=FoCyEGYoh>bw~{jK~1kKZ`4s$b)fTPqOF|68W91IE$4ETF?uVrVfyxM#kE0Fq6V6ByZ z`3eBb-^V66H%ARmiNmM2S?kmDSF5M01tvfL9P9tc4-AEr66~4e`=1_p2W*kaJOZVfs zDh?ey(YvY%Q6=IsUvWO0-?Kx&&f!WD&O%ry-N)Ip@UG`GiZ*eemFZD0GH~@a$fQ?HoG}E&VYSY2Mj|$kEWZV7pW2z;pGWR^LOvSrVid! z;dBdyKC!qF0KO49of0y2|6hbG8R!j+P8$`f-a*meEy6Kd{6g{$MXNe$jiQK|LyA8& zj^Zc7i;0ZvIv`fSAyRipo;TP;Oze54C2DWPyeIN~^6W(6HTQlDGe^CHNtbj(mcv+N zt?pCp#VY_|P@1AW2=1~m|JrCs2kcP5D%g<}`Gmi^n0oNgXUkQhvD;dmj<2*2_628A zoow7hY#g#K)DWUkGDOKtzReRL~VM%1@9Y16!=XP#2e^_Bajnj6rSG6Aje_RE3w@c$lZl92jAX?Lq zKSw0{ptSPU1W_(gk7^iwDf5?ewRgP`c?B?MgYyI@g|&?H0Vq<_k19ITSCd%td?A|w zC;Y&1uaQju`Hf_2arU$Ul!w|czM`fiPOipM(Jf8R3eismD6FM;Y@Gi!`F_GR=Y1Ji z{;etHC`}rOyld55B{<8>J^qzrnSO~jMt1aB$Vi3Q*Dt^winxW^kgnW@Gh;(f#BV(j z3bG*BUz=MX?z&gz9S&v<*WQoSdYG_u6JC}mqD!WKK?W@n-udlm9hB0N!xUhy>0Pu* z?$>Ta+=1088HVddIC6EyWiazdtzhMG1}Zdk3mGjySRfW|9h4vu1Vy#pH0~FgZB z>e7u3zltpdxKMk@24(^F=dDp{JE8;W?7f&%bC0i{kP^CDafz`Of7}yCmBtql0Z7fk zch|SOH6*OawYXg7KC@^t8_wkm|72jwUnJSbwaNG)baxpZlON-m-rUvWu}++hA@D_X zMbPZ32%yMGi2}1;?`HXpGqf|87RA^nawYdPa8J5$LP`sv=$F!~Aa0kJn=V$gJ1Cin z{JvS$P6_FaLFqyY?$Bal(U3euTAK=!+9P`UG-;9{p;Gl1(}bOLvZ1i@g=^y1gqNun z*VLb_iYl6k-n@e^Zcd#->qK}5{332X{Z^jvE`NV0n8&p=VCybBh_WfjA+uJ8?Z!g` zZ^avVL19*xjU8uZy+u3s6jB7fR^o8oYO$cQ7CF_XD+hSV7gANb_S$`0rl$#Anegz96 z2NbEwSCd3Y)7vuUU;qG5D0osrhM|azLyE;Z^{yL;nctoJ<^T?3tC+Q2_ac=_3}e_= zI5KQ*PZVoM+_`YsKrO7rSN)J#2Cch_m^ssprGVs5!$rSDVEAh1U{Mdr0iCJ47JR&a z166zb*78lG@Wq>Q)z+(2QkG>g_Z0BUMf%=lG()0;aFGrq^)9{r^)L%XZCNc|phIS} zgs#_2O?H(Re5mA=mqf?Z-N;fH&E0l|;e^POGxL^I^u7jOgwN;~seSERSRfZf)XZu2 zeUHdj@0)%ljdV{FhFB>Mia(yUoTHVVQ3~&u8YR~VT)3b-QyVC$$qZbyula7My}6mT zI7Sh<=~7qBpX8N=Wg6)e^b8t@?f5$`I{FoTv^zZ;eJ=3uMKl?vy&R(84qD3-8!iNO zi`JwGw%>U0wu5w54qW52D-4Uvg&bd`UGpQlX2ldVV_WG3MXW;HX=YIr`3tnotJIulnHfaAL8aiNGKC!1?+N)k zT~jKY`;ppnd#$bO?$1&Jn5KrT%d50K6=SIKk|epOi*Ep?m|$|`^WDNlV!k5e^9-uH zM$w8>zSLAX|3o076^5iz_fipQ^=Caci~BIws8c}CHwW@c7>#fmKUGa-@1Ys}H|N9e z!OWCJ)oD}An%=HkrZn8wO=J0!B#-$K{YBl)(HLYHqCdY;K)tQYn`HmL?T3DAJ9Qzq z2Ww}D1HHC7DGp+YMB YLmY0H)gNu^ItsP@n?^{JTwl}w2Vpk3CjbBd literal 0 HcmV?d00001 diff --git a/content/authors/mark-santos/mark-santos.jpg b/content/authors/mark-santos/mark-santos.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9aa4a5f3a85c100556b3daf1fc451ad78d5ab472 GIT binary patch literal 8722 zcmbVxcUTkMw{8$HD7}7wNK`=jr!=KUPz*&tk=~-xL3%F<9Rz`heuxGH=}HS7q=jCj zN$S>X@ax>VA6c7@fbis0RX6z z7XWDy@I=ke!4?3})&__G000^QMYIEeifoZX0NmsT08oCV08o=_}=b z+rI-zI{?|I_O4#8p7yS8H>Cc)3y@XS(5Cu58X13Vfj=hG$o05M1fYy+Bjn1bZM-U+ z)C6Fr0W<-cC@F3ME-_P3GEZ#U)BA>dQcyD_3dB0hLUEOB9rp zm#8SIsehk_0!Ho!P%%@p+>}tc%=+vl@RmE9WN2ar4WDXhGrQj4KEKo}kFYCOIsW40 z;ug3qC?qT*EpzX_tepJAM{18D>KdBQ^$iS-j7?0ftZi)V>>V6Ey}W&V{rm&MBO;@s z-^9ee|L{-J$K+3+k(pW9Ik|b?zUP;fS5#J2*Zin$X>Duo=M>i9vmLwj!*EXXTN!oujjvLk+=V1_CI(rlX+dDqN1b%{^mt-$@e#LW-98N z5|>$2o&jIFv)+;nrD0P|%qVTX!Y8G-&;H6|@G1wt^r8UvH?=>^{&&Q}{=b<0o7jKz zng%=sT>1mbOXRymNlCs_RAf*Csec1V1N;M;|HRclp#2T{e}hCGgn~Q<`QXdsmHrCw z%D?;m*9K{pEK54l6o8JBf-ELVW&jvK7>X4_Z9K%5WX1{s{=2n|5p@^m=`*`gj`zl{ zE+k2HvHB=Ujb-MjKSwCeCj02$6mB#wDEEoo=Qz(%{tM!+c%S?Iqmmy_HD(5OndoOO zWO^(v#z+7LXP5RevpG$kMILIvISkCbpO3ZF)m?7u+*4AwmW#OHmLwp)ucLsvh%~nP zgJ~W6SWPtFOn*rj27hm%6ddU_24eTpO3^5`gFFOKCp}*B>wmx{dn7ZerPQSF6RF?p zXyaES`jehFN6pic2kR5Q)S#@StM>$@^SK}M@6LvLP@SLKFY!PR#L!gYD&5Vnn< zEbhp16*V@)2x_)x5B{rxNWcD{i;-I{B5W%Qu>LTiS1uCP`+#iyqJT0Jz0Y&K6hI7{ zmZ1HbG^e^mfxIau`ZtmWlj?$sm%!WyQmK2iA79P}Jt@j6+nicQJQVK{U3SdZ|EpkU ztq08-{2D}ye`p4yd{piHMCDGhyDzh{5s;2KL{4-b6dA;h1uwaL#y;=3Q*^srCaDn1 zui{Lr=57g=Zcu!3-ZRTzVUg^4Y>v9o^1@k!`J~9HF*joBd?x|LcJJ5e zQ_=kFI0_bkf0LXn>e+bq@KI;%?(r?_slpVlL2Cxdyf7sg&4B4D5hh)G@7vh3iATq! zrG5)Ggvfmj)w@2`ma>J@1J`oPbIdv=m6B#rJq#qkT+4di4c6x8iSsfCRkgKBW3^Jg zkE`pjE}3eh|LlefaMIU3*uJ{J*jWT)TtHoeEW*@^tVX!!0*wv1#P39D6?^0fM)mvc zY&!Ky55$<>d%HNW($cGez0%LlkOI-P zx2bW`M91eQ>r{~dA@KBP=AF?*tsE!hYol75A1lIln801dcC{w`n+w&7BeuNk+X0T64Qd(d{kwqSvBcJWG-4Gi6J}pK8#`~_RQRlU%1sxTLHS;G0~&(Q ziuZOOY;Cm{2sR=CE_H4x(ndaQsV$q+)m0+_E-x-B^aQgxYC9KL%J+tdxpzlCrbQ$@ zXac6)Re-}$1>H_Lj{BVsHnh_Zta`0k+CmSlS61wD4EK|p8UoEmGDdbX{YDbswE8Hf zKOv+B8V$?)ZsV3{duk%`UcYx6D39Z<#~_JsncqfOWDbnn{cK@U@Kf+D1h%+i>neG{ z@+&+t<~Zgh&x&Z`&$K4YzRc%hKfzXCjutbYcU`9kFC|_+0H*HD$eoSPPeHdsGcDBU zs*#ax&%aT}-89e%2xIWHEX}#-OXCQkG{-DEiYikS2Y=YPAkk_v^{P3B+p)mCJ8pK z|El4adIAsb%5{T=8viN%fe1K#wjkWBh(o=vms$>H#J{>GGir0$WW?VsukR+*+zY_F z)_@#dHfn__OWe-!Io|15weA7!7qz@Y69$YX`TzI$uTk|$fSyIE>74hm9lnJwm#614|% z)6+|+U5sy~v`BLeIvPfmDnUyf$dfvux1KbKeva)9q37E-egI6g-4j^RKt!SqA@ z&eycUcD$VoYGN1p2%7DDa@4nt)4-zkfMrQ=uk040c;Wrr9j47I9$!1%nyydmA)qpSE zm&mid7gvZ_s04g4bKYn;yFA(jqbsV&L}AnJ^tgKO4!h07TKh{^l@6V~A1mmxv7~&w zk2YAl3b9>v$64$!>WPNiPSdM^1*rbP7a4>;XDt~ zj*h1ljn3Am|CW4ibqeH*iLx6mi~Oj-%$2^AM$TIMv-%4W;JM@h)b^bh!Ab-xvp*hr zggoK}@5K1wx5EO#{*x^7IsvSdxjyztXj!BwOi2Y5dm(M_%${Z56Y0$B^`0d-foDhd z$l8$W`uGMEdP)MAF4Vg(@-6i#I~m&~+)k6a7W1K3mFC9XqnBx-2P6PnjVSAv7Y-v$ z5M-sWg=mf+I_CCK2+1mLbshho)BlQhiHyXP7xQ(kt8`rJ?C!a|PYBRIZ!JiEM{ zp65sasmHk7Frsq%0e*+-AprnT3qRVna{Ch(t_+`Q+jZJU6GRH0SVFiuK9s#Ote407 z+o{Tc>=v|>b5k$FQa@i*dYhR3{47tnP3$p?)Z3XY_zgOHBcYmWDzZ35Bg*}cW1@XF z?d?CsHZ`5Cx}Hk<;R@1jG=6BSrQQkCTA4L(3#z(U+|1iR;t!@D;I|gVUoE%!6met< zG&$LaTzkMt4FyZ!sEot~xHPwqM73M)sS}Jk@A;hh)VYieegF6vECdd^t$V<}S*zVw z=P;;#ZB>L5)Pe@TR=kh(?2vCqzriV-9wjnO-J*%B(-l`ym<;t|;N^jf>JVbM~cnOSeW_}|Co3$G*+ z(_e=oajr~G_s8JYlEpeS0{)ll_f&_SWi0fZ;!P=}9^`j{WDP%zP=+^k(8GYV*52iX z9cdOSIl}WsWo6C{iiwBBXQ|JAmPmGtYJW3RtE}&f{(FHJUoRGhKEn zP9IDbU76x0RAH_G@xmWIGWIX=?ICr+TaXfR3O^n3A5Fx-><`b^k12L^Nuy_seP@Gf z=T7(g&jvB$$MdCN1}-MIR|M8s1W?AW zKwtn2Wr#XEQDKP$pwsV>+AKpf8=WMcR(q2Gq0m*Uv$~eun^97tc~^p-o`8B>TRKey z<1d8qzU|J77Lb-bUypm7E^~{9Q`WBl)_g!fNtYr*^Gvjb<>J?09;clxhQh3nn^4JZ z>R#uHVmG+ehz}ofMG-~jIkgj1R*eENqfPa)brm-*hBPQQbUa+%Iwac-#oP>oL~vT@w^mRiW0c2Rgo*E8$0%%7Yl4ylvR$3T~acx@8z8ut*GGW{|a;%<)T= zhkxf@ExeI-+Xu#CNa%C6By10n0C^>nfI{N_y#%l+CP-YkCTdH+yM3n9wYD<}h?Oe_ zS~k><;i`sTd0w0UY4tq)2b;ebq!GxA+xY0-{L+a%sbrc>h#B~moiHDwD5_T_nd|*5 z@Sowhpp_x?Ea+T_NZSGDj}Uq&l6$_PYoU`-Pu1Yo$!8b?D|yHzWu8nmPtQ z^;2uDsg<)at@(6tSg}0U?YasdD0N-XO$Y)n!^Fk;KARs&l&*TF$=k&qpKC1wqblX- z%5o>=PfJSU(byp==;sT;eNYUX6CZuSf~M?0UbUXXe)|bF56fO^K(vCYija?xzOhBt z!^OiiCZQ5l3acs|PFT=O-vM2H_zx}a?-zTlSr)og^$PolmMQC$HeK(#a)-CA_!tT6 ziAKu&jED@yb)OW{Uga#D#8empi5F4mRT6?i@AN!70&$%$2*mB z=-QqmaALhFO0bP3oFEZ<9{k*hED-y{267Sod3f=Ue+Sy`7`PU+J$i`l|1USzcyqcs6k0F%bI>akmp^gR=0*9O6qU8nP_$G`(f zr7b#5$^Njj$oe#IWDpaGRC*I==ybX~_F>?;{;g2!d?ux!Y+a6BT!PEx?cNy72adtCLJ5#UuNeCyZqNwBW+9#L8u$0 z{?DcjyZAiwfz;v`*eneS0bSRcw*qw>budcybv6RPYc+Uj~>yqlvxIR^>sIazz z$3^Qy7<;~Hnf<+lU*s zV}&hew@wLQ>|e5d*P_AYxfi`&!+pCXz#TqZTtS2f@v7b`Cf(oZL_8U@<~7d%zv)N< z&<7a+-+1+^qc?ZIy08TL+$iILNix2oK+PZ`dGGFbwz<-pI;C;fCag{AUox(y6k}n( zmX)Z|^&(g~6HQ?o%@^R2hpO~RXMhX&%`Ym~ATi;4X``)_(o`R-Yo2{ohNA+05*U zsaBV3U8B;gbz4zv?d*s)QGZ3bZ=j?+1IbAV9NiVKv%5_=R9@CtJ?(M|qe5$K)tCJC z5e+JAUtC%=&h{ARyE4-^mpr>a{I;%=Knj z*sPRzduu*pZ{mk-5%bi!l*H{(p~>~cL&hEIr4$l?(sW%}XmHXd_uW^scN>@k6V3R0iw(u@<3I6|%W zgRoX&9kN56?Sm@vBXoqjw0X7TQU4flq5T=UQEpte8kk#S< zhbiqm!{%LR+XKOFx85HZI<&uTDf_R}M%-CD$jv)meJX@0NjmyZt~Ep_<;w72juff(5GQeZ>3PPFSReIDCA>dfQ6* zar|i-CyJxc)Z?|{70;?c{Fi)djD83PCv+nI;*6eq6kKxh4MBGwi4c?RRd{bh^Y{OD1MzEUy zu{TZBBx@B$<=q^O?R`720a-V4h-oxPI2AEyR{$xQW~6>B_=$e!k|27WsB*Y| z(;qtGi<=7VTYt?4r+ZrUOJC`C0u4y}uQj44c!8B*(%^yXbv#CAUofh;{^2*C(}%B_ z85_j#f>rC9oDVnfa3y*k88(I>cQ9iaw-b0 z+;@;ysJOcW7Ap@kSUut^Bfy+GZczG?KXfUYE<<+;`xf@eQ%$?fDT^HORJ8i3(+F`s z1n)|?PgTmEyyqy278r!126V!+%niQMTq95peN~h@%CVdJF+k-ZdXTlto3j#Qyf*+u zatmzCWmbF$x*otC{Q2<3k#dr$@y9RQk67__&)>K6$vsnDGsz#d;nxKS<%}vZy`E77df?+R}@=1i?J(V%TZPpH=fvt$%TJoL_WBuBk)G z(PF}~{!HHy&wt<2(ZtZFW_?7MYQZj+fhrs6qUFZm-*+D$BfAvZ*<0G`Im|Y`%scC# zuT^!;(crwzNZ;!TbHWEZ)wpqppkhB#c>gx4(cUDKj7GLd)wtps$-^|QMPca zMAw(k&&Mf1AXOj+T8bA)Q@kCFy8`y8b-I{KC=K==?}f5m|%&PafGXdLy6Ih>(lA72^Wa?Tae zm8jy?j81WF+OQfr*^~UfpLR9hC$6wkrs`j9x(v+AF%x*Ln_KnLMoC3R=(W11ftU{O z19>}XjMbXw%u#N7ZKObfOnHYcJjpz$5aMXyrn(&Go$aR4wf4R4eNr{AEYDL&AJTYy z1*qKMs1hC?Tfou=^-M*Y_?sVPcVpOZEhZN$H*AFY*r=Ci^hq4=D!#ay^iY)dQ|`|O zzZLz7+(FJ8*41n^n1nCFAu9p4U(($Cvh~C$>qG*E;*y$WxOv??`?Ol(5Jj@59|Q;K z_{I^>46^(0%G(xOr8^FMN}^L36sKn9*?D{;@c_98XY#A?19#jrjoYaj3qCS?A3&2M z`jPzW>mB1S?!%31Ck*r3qs9!k1~St|M?bC^lyLquo>8b+7)j3emF#%2u~ahLYG@Sq zL3a&aRk-+%WXa8t8{zL%at>izofoWrr9s*Y&hm=`dpk^~Pry@=4}e&6?xS?@b$O#R qwZ!EOTYnf&OLVKAh}K%w>FZ#St+DARF@q-p&{%p@9q|NAn*0yY0AHs7 literal 0 HcmV?d00001 diff --git a/draft/driving-ffmpeg-from-java/Batch.java b/draft/driving-ffmpeg-from-java/Batch.java new file mode 100644 index 000000000..da0bf649d --- /dev/null +++ b/draft/driving-ffmpeg-from-java/Batch.java @@ -0,0 +1,58 @@ +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class Batch { + static int waitOrStop(Process p, Duration limit) throws InterruptedException { + if (p.waitFor(limit.toMillis(), TimeUnit.MILLISECONDS)) return p.exitValue(); + p.destroy(); // SIGTERM: ffmpeg still finalizes the file + if (!p.waitFor(10, TimeUnit.SECONDS)) p.destroyForcibly(); + return p.waitFor(); + } + + static int encode(String in, String out) throws Exception { + Process p = new ProcessBuilder("ffmpeg", "-hide_banner", "-nostdin", "-y", + "-i", in, "-c:v", "libx264", "-preset", "medium", "-c:a", "aac", out) + .redirectError(Path.of(out + ".log").toFile()) + .start(); + return waitOrStop(p, Duration.ofMinutes(10)); + } + + static double runAll(List inputs, int limit) throws Exception { + var slots = new Semaphore(limit); + long start = System.nanoTime(); + try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { + List> jobs = new ArrayList<>(); + for (int i = 0; i < inputs.size(); i++) { + String in = inputs.get(i), out = "out/batch-" + i + ".mp4"; + jobs.add(pool.submit(() -> { + slots.acquire(); + try { + return encode(in, out); + } finally { + slots.release(); + } + })); + } + for (var job : jobs) { + if (job.get() != 0) throw new IllegalStateException("an encode failed"); + } + } + return (System.nanoTime() - start) / 1e9; + } + + public static void main(String[] args) throws Exception { + List inputs = List.of(args).subList(1, args.length); + System.out.printf("%d cores reported, %d files%n", + Runtime.getRuntime().availableProcessors(), inputs.size()); + for (String limit : args[0].split(",")) { + int n = Integer.parseInt(limit); + System.out.printf("%2d at a time: %5.1f s%n", n, runAll(inputs, n)); + } + } +} diff --git a/draft/driving-ffmpeg-from-java/Cancel.java b/draft/driving-ffmpeg-from-java/Cancel.java new file mode 100644 index 000000000..98a54cd5f --- /dev/null +++ b/draft/driving-ffmpeg-from-java/Cancel.java @@ -0,0 +1,62 @@ +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class Cancel { + enum Stop { QUIT_KEY, DESTROY, DESTROY_FORCIBLY } + + static Process start(String in, String out, Path log) throws Exception { + return new ProcessBuilder("ffmpeg", "-hide_banner", "-y", "-i", in, + "-c:v", "libx264", "-preset", "medium", "-c:a", "aac", out) + .redirectError(log.toFile()) + .start(); + } + + static void stop(Process p, Stop how) throws Exception { + switch (how) { + case QUIT_KEY -> { // what pressing q in a terminal does + p.getOutputStream().write('q'); + p.getOutputStream().flush(); + } + case DESTROY -> p.destroy(); // SIGTERM on macOS and Linux + case DESTROY_FORCIBLY -> p.destroyForcibly(); // SIGKILL + } + } + + // How many video frames can still be read back from what was left on disk. + static String check(String file) throws Exception { + Process p = new ProcessBuilder("ffprobe", "-v", "error", "-count_frames", + "-select_streams", "v:0", "-show_entries", "stream=nb_read_frames", + "-of", "csv=p=0", file) + .redirectErrorStream(true).start(); + String out = new String(p.getInputStream().readAllBytes()).strip(); + String first = out.lines().findFirst().orElse(""); + return p.waitFor() == 0 ? out.lines().reduce((a, b) -> b).orElse("") + " frames" + : "unreadable (" + first.replaceAll("^\\[[^]]*\\] ", "") + ")"; + } + + public static void main(String[] args) throws Exception { + String in = args[0]; + System.out.println("supportsNormalTermination: " + + new ProcessBuilder("true").start().supportsNormalTermination()); + for (String ext : List.of("mp4", "mkv")) { + for (Stop how : Stop.values()) { + String out = "out/cancel-" + how.name().toLowerCase() + "." + ext; + Path log = Path.of(out + ".log"); + Process p = start(in, out, log); + Thread.sleep(3000); + stop(p, how); + if (!p.waitFor(10, TimeUnit.SECONDS)) { + p.destroyForcibly(); + p.waitFor(); + } + String last = Files.readAllLines(log).stream() + .filter(l -> l.contains("signal") || l.contains("Exiting")) + .findFirst().orElse("-"); + System.out.printf("%-4s %-16s exit %3d %-36s %s%n", + ext, how, p.exitValue(), check(out), last.strip()); + } + } + } +} diff --git a/draft/driving-ffmpeg-from-java/Carriers.java b/draft/driving-ffmpeg-from-java/Carriers.java new file mode 100644 index 000000000..53baf23fb --- /dev/null +++ b/draft/driving-ffmpeg-from-java/Carriers.java @@ -0,0 +1,27 @@ +import java.util.concurrent.Executors; + +// Counts platform threads while one virtual thread per child process waits on it. +public class Carriers { + public static void main(String[] args) throws Exception { + int jobs = Integer.parseInt(args[0]); + boolean read = args[1].equals("read"); + try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { + for (int i = 0; i < jobs; i++) { + pool.submit(() -> { + Process p = new ProcessBuilder("sleep", "3").start(); + if (read) p.getInputStream().readAllBytes(); + return p.waitFor(); + }); + } + Thread.sleep(1500); + var threads = Thread.getAllStackTraces().keySet(); + long carriers = threads.stream() + .filter(t -> t.getName().startsWith("ForkJoinPool-1-worker")).count(); + long reapers = threads.stream() + .filter(t -> t.getName().startsWith("process reaper")).count(); + System.out.printf("%d %d jobs (%s): %d carrier threads, %d process reaper threads, " + + "%d platform threads total%n", Runtime.version().feature(), jobs, args[1], + carriers, reapers, threads.size()); + } + } +} diff --git a/draft/driving-ffmpeg-from-java/CloseTest.java b/draft/driving-ffmpeg-from-java/CloseTest.java new file mode 100644 index 000000000..d11a145d2 --- /dev/null +++ b/draft/driving-ffmpeg-from-java/CloseTest.java @@ -0,0 +1,16 @@ +// JDK 26+: Process is Closeable. Leaving the try block closes the pipes and then +// waits for ffmpeg to exit; it does not stop the encode. +public class CloseTest { + public static void main(String[] args) throws Exception { + long start = System.nanoTime(); + Process p; + try (Process running = new ProcessBuilder("ffmpeg", "-hide_banner", "-y", "-i", args[0], + "-c:v", "libx264", "-preset", "medium", "-c:a", "aac", args[1]).start()) { + p = running; + Thread.sleep(1000); + System.out.println("leaving the try block, alive=" + running.isAlive()); + } + System.out.printf("close() returned after %.1f s, exit %d%n", + (System.nanoTime() - start) / 1e9, p.exitValue()); + } +} diff --git a/draft/driving-ffmpeg-from-java/Deadlock.java b/draft/driving-ffmpeg-from-java/Deadlock.java new file mode 100644 index 000000000..18a9b0470 --- /dev/null +++ b/draft/driving-ffmpeg-from-java/Deadlock.java @@ -0,0 +1,31 @@ +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; + +public class Deadlock { + public static void main(String[] args) throws Exception { + long start = System.nanoTime(); + Process p = new ProcessBuilder( + "ffmpeg", "-hide_banner", "-nostdin", "-i", args[0], + "-af", "ebur128", "-f", "null", "-") + .start(); + + // The mistake: wait for the exit first, read the log afterwards. + if (p.waitFor(30, TimeUnit.SECONDS)) { + String log = new String(p.getErrorStream().readAllBytes()); + System.out.printf("exit %d after %.1f s, %d bytes of log%n", + p.exitValue(), seconds(start), log.length()); + return; + } + System.out.printf("still running after %.1f s, %d bytes sitting in the stderr pipe%n", + seconds(start), p.getErrorStream().available()); + + // Proof that it was blocked rather than slow: read the pipe and it finishes. + long drained = p.getErrorStream().transferTo(OutputStream.nullOutputStream()); + System.out.printf("drained %d bytes, exit %d after %.1f s%n", + drained, p.waitFor(), seconds(start)); + } + + static double seconds(long start) { + return (System.nanoTime() - start) / 1e9; + } +} diff --git a/draft/driving-ffmpeg-from-java/Drained.java b/draft/driving-ffmpeg-from-java/Drained.java new file mode 100644 index 000000000..305193181 --- /dev/null +++ b/draft/driving-ffmpeg-from-java/Drained.java @@ -0,0 +1,25 @@ +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +public class Drained { + record Result(int exit, String stdout, String stderr) {} + + static Result run(List command) throws Exception { + Process p = new ProcessBuilder(command).start(); + try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { + Future err = pool.submit(() -> new String(p.getErrorStream().readAllBytes())); + String out = new String(p.getInputStream().readAllBytes()); + return new Result(p.waitFor(), out, err.get()); + } + } + + public static void main(String[] args) throws Exception { + long start = System.nanoTime(); + Result r = run(List.of("ffmpeg", "-hide_banner", "-nostdin", "-i", args[0], + "-af", "ebur128", "-f", "null", "-")); + String summary = r.stderr().substring(r.stderr().lastIndexOf("Summary:")); + System.out.printf("exit %d after %.1f s, %d bytes of log%n%s", + r.exit(), (System.nanoTime() - start) / 1e9, r.stderr().length(), summary); + } +} diff --git a/draft/driving-ffmpeg-from-java/Loudnorm.java b/draft/driving-ffmpeg-from-java/Loudnorm.java new file mode 100644 index 000000000..d14fec639 --- /dev/null +++ b/draft/driving-ffmpeg-from-java/Loudnorm.java @@ -0,0 +1,64 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//JAVA 21+ +//DEPS tools.jackson.core:jackson-databind:3.2.3 + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.PropertyNamingStrategies; +import tools.jackson.databind.json.JsonMapper; + +public class Loudnorm { + // loudnorm prints every number as a JSON string; keeping them as strings means + // they go back into the second pass exactly as ffmpeg wrote them. + record Stats(String inputI, String inputTp, String inputLra, String inputThresh, + String outputI, String outputTp, String normalizationType, String targetOffset) {} + + static final String TARGET = "I=-14:TP=-1:LRA=11"; + + // One ffmpeg run with a loudnorm filter; the stats are the last {...} in its log. + static Stats loudnorm(String in, String options, List output) throws Exception { + var cmd = new ArrayList<>(List.of("ffmpeg", "-hide_banner", "-nostdin", "-y", "-i", in, + "-af", "loudnorm=" + options + ":print_format=json")); + cmd.addAll(output); + Result r = run(cmd); + if (r.exit() != 0) throw new IllegalStateException(r.stderr()); + String log = r.stderr(); + return JSON.readValue(log.substring(log.lastIndexOf('{'), log.lastIndexOf('}') + 1), + Stats.class); + } + + public static void main(String[] args) throws Exception { + String in = args[0], out = args[1]; + Stats first = loudnorm(in, TARGET, List.of("-vn", "-f", "null", "-")); + System.out.printf("pass 1: %s LUFS, true peak %s dBTP, LRA %s LU%n", + first.inputI(), first.inputTp(), first.inputLra()); + + String measured = "%s:measured_I=%s:measured_TP=%s:measured_LRA=%s:measured_thresh=%s" + .formatted(TARGET, first.inputI(), first.inputTp(), first.inputLra(), + first.inputThresh()) + + ":offset=" + first.targetOffset() + ":linear=true"; + Stats second = loudnorm(in, measured, + List.of("-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-ar", "48000", out)); + System.out.printf("pass 2: %s, output %s LUFS, true peak %s dBTP%n", + second.normalizationType(), second.outputI(), second.outputTp()); + } + + static final JsonMapper JSON = JsonMapper.builder() + .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + record Result(int exit, String stdout, String stderr) {} + + static Result run(List command) throws Exception { + Process p = new ProcessBuilder(command).start(); + try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { + Future err = pool.submit(() -> new String(p.getErrorStream().readAllBytes())); + String out = new String(p.getInputStream().readAllBytes()); + return new Result(p.waitFor(), out, err.get()); + } + } +} diff --git a/draft/driving-ffmpeg-from-java/Probe.java b/draft/driving-ffmpeg-from-java/Probe.java new file mode 100644 index 000000000..74c1bcf37 --- /dev/null +++ b/draft/driving-ffmpeg-from-java/Probe.java @@ -0,0 +1,55 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//JAVA 21+ +//DEPS tools.jackson.core:jackson-databind:3.2.3 + +import java.util.List; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.PropertyNamingStrategies; +import tools.jackson.databind.json.JsonMapper; + +public class Probe { + record MediaInfo(List streams, Format format) {} + + record Format(String formatName, double duration, long bitRate) {} + + record Stream(int index, String codecType, String codecName, + Integer width, Integer height, String avgFrameRate, + Integer sampleRate, Integer channels) { + double fps() { + String[] f = avgFrameRate.split("/"); + return Double.parseDouble(f[0]) / Double.parseDouble(f[1]); + } + } + + static final JsonMapper JSON = JsonMapper.builder() + .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + static MediaInfo probe(String file) throws Exception { + Process p = new ProcessBuilder("ffprobe", "-v", "error", "-print_format", "json", + "-show_format", "-show_streams", file) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); + MediaInfo info = JSON.readValue(p.getInputStream(), MediaInfo.class); + if (p.waitFor() != 0) throw new IllegalStateException("ffprobe failed: " + file); + return info; + } + + public static void main(String[] args) throws Exception { + for (String file : args) { + MediaInfo info = probe(file); + System.out.printf("%s: %s, %.3f s, %d kb/s%n", file, info.format().formatName(), + info.format().duration(), info.format().bitRate() / 1000); + for (Stream s : info.streams()) { + if (s.codecType().equals("video")) { + System.out.printf(" #%d video %s %dx%d %.3f fps%n", + s.index(), s.codecName(), s.width(), s.height(), s.fps()); + } else if (s.codecType().equals("audio")) { + System.out.printf(" #%d audio %s %d Hz, %d ch%n", + s.index(), s.codecName(), s.sampleRate(), s.channels()); + } + } + } + } +} diff --git a/draft/driving-ffmpeg-from-java/Progress.java b/draft/driving-ffmpeg-from-java/Progress.java new file mode 100644 index 000000000..e219292b0 --- /dev/null +++ b/draft/driving-ffmpeg-from-java/Progress.java @@ -0,0 +1,57 @@ +import java.io.BufferedReader; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class Progress { + record Snapshot(long frame, double seconds, String speed, boolean done) { + // out_time_us is "N/A" before the first packet and again while the encoder + // flushes at the end, so N/A keeps the last real value. + static Snapshot of(Map kv, double lastSeconds) { + String us = kv.get("out_time_us"); + return new Snapshot(Long.parseLong(kv.get("frame")), + us.equals("N/A") ? lastSeconds : Long.parseLong(us) / 1e6, + kv.get("speed"), kv.get("progress").equals("end")); + } + } + + static int encode(String in, String out, Consumer listener) throws Exception { + Process p = new ProcessBuilder("ffmpeg", "-hide_banner", "-nostdin", "-y", + "-loglevel", "error", "-i", in, + "-c:v", "libx264", "-preset", "medium", "-c:a", "aac", + "-progress", "pipe:1", "-nostats", out) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); + Map block = new HashMap<>(); + double seconds = 0; + try (BufferedReader stdout = p.inputReader()) { + for (String line; (line = stdout.readLine()) != null; ) { + int eq = line.indexOf('='); + block.put(line.substring(0, eq), line.substring(eq + 1).trim()); + if (line.startsWith("progress=")) { + Snapshot snapshot = Snapshot.of(block, seconds); + seconds = snapshot.seconds(); + listener.accept(snapshot); + block.clear(); + } + } + } + return p.waitFor(); + } + + static double duration(String file) throws Exception { + Process p = new ProcessBuilder("ffprobe", "-v", "error", + "-show_entries", "format=duration", "-of", "csv=p=0", file).start(); + String out = new String(p.getInputStream().readAllBytes()).trim(); + if (p.waitFor() != 0) throw new IllegalStateException("ffprobe failed: " + file); + return Double.parseDouble(out); + } + + public static void main(String[] args) throws Exception { + double total = duration(args[0]); + int exit = encode(args[0], args[1], s -> System.out.printf( + "%5.1f%% frame %5d speed %s%s%n", 100 * s.seconds() / total, + s.frame(), s.speed(), s.done() ? " done" : "")); + System.out.println("exit " + exit); + } +} diff --git a/draft/driving-ffmpeg-from-java/driving-ffmpeg-from-java.jpg b/draft/driving-ffmpeg-from-java/driving-ffmpeg-from-java.jpg new file mode 100644 index 0000000000000000000000000000000000000000..27f227fa5869658c769f5a071bf5e6739ac139a2 GIT binary patch literal 83618 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

?v96}s?v>*wnuMxu%)e~2M#=NKZpqt zgP303*YEf+HhnpI@Qd`P_;~2}bTQ%qt^LH0+u0wkeq-&dIC&-4W%Y$SPiqNiP~Jkx z6cg;}R|%B3Qu&xE%W!-!FFg`v6jyE^Gpek3?*#uT;cm5w#IaT!`%z3Ej60Kup1||{ zykW@l@=SWCLbB7vI`>t_!fk+wed4YLz@bUK*gDMpkHKGc;T()5`-+R)Et#Bwm4c6E z-1YghzLpPQDuS!7zm|$KG)VNia=)|mhFGeDQB4JW`*Hd4$>kK{L!5HG-{V8{j}*Jq zyRf1Ma6lKIj0V6@)EW6)jU-s+}?Dv@Oqefjg|-rom~dKGyTzIKAfDmI4tPINLX+}baEJzPz{6?YYP zc$6=Fx>Nwt6W@;P+8|7pVEJ1z_Vo|7zNV<&21jQz_N9a+_7BA5%E5BTRm%RIYGyM7)f8wx~CUeb~*-RX>v*-7} z{z5P4DV_83TqGUv+DB0rR1~O8sWyua8kQQsLhrB~sqoD0$p$l;p39+F-BgW=!@v;y zg-@aqBr7@tW04hqP+T+3+7)!Y?lRaa-C@dF;PrtQ*Y~9A*6sf-JY+6l(MZZy9jh1CG_}#IaPI6I?y)(+jjRTrC z#{bDcFsk7JQ$BGhUE*>8MAFBmTUX)IK54jSx#~FsJ3wSIEF)4HoE$EmrTZa}0YA1^|z~3;t#>rImTtNhRH}5atCUR(MO4Yd`jI^%q4)p@*4P z$9&KV_$FGuZz%g0g%1_e`(&I5W;~zz!R|OXZ^&t!Pcl#Yi1DUz1-LoKm>|~iWQ!Z4 z@;dsmIYzkbL|~viJzc5Zlnp_x)$Ap%vlE`vK5b|;Y^o3R2>vMN*F`6hqDNT`%a9@?dY{I!TZ52jRcXBrcc@NF`4po8u*caR8K*u@yYcV* z%L0?w8!He9aYJ#hyG&;5*lq#k?15oI@e|+E(jT2ZLMz|poPJLHqL6e^S$r7O{aI`_ z`!uV*IP@gxec^tE0-_L5>V~_xjWS2Yrnt>}cn@%6J4g>rRF^MU4hyVm83jfDupPTx z@ry!wNQLV=G$ zTZ(I-(bPmB*qAWnKy?Ggr>0JHX2Vq7_QKbpWprP|s!%Wyk#iY$<6OZHva@Cu%BcE1 z_Gg(;n$hMl?a#8O){Tx4yQ4Wq9!!p28f_I7vpME=&#*=p)c#Js;n?E`wvgF3Ks-&^ zKrd|~`v``_E#s$&4p ztX+t%UPRk8fnDr&T2vhh)MkT0_}a<^g37;}Az_u* zv2UfjD{=uxil@`~5Ys~~y`Y1WeQ40w<{x6}yK|{-d5eJ%9>GDB`y8z!K8(!n$RiSB zAA<0MKSzdEQy;rN>lCvUST*OI=|>cgo$2Y|M#r63s|rqlzkynH=i9SK8eE?OTA$uQ z7M&=n=Q@1+gbkG`NjrLufYFc`6lGIoR4h#Qkn|bZ9!jKsIzwba$&%}_xYuV~t=V(a zWRQA5@Gpucl=qEO{Qi4$H`E}Hx}tdcH{1;_^3mdvQ>e^O)}OCW9Zp4lJpByJdhjz2 zxD_^+i$be!@UHBJHI|j@>WksQmE*)uC^|wqbWH2d7HO<(TrJKPesrjc`$MF0S9B%Y z<%CBO-F3=f^m|a&6Z3b)r6jp#5E@sT?>bZ9m~M99#`5jeH-qm28O!-yc9^YV4wLOS zgYJbTsqd&9qyn8_&_Sd3n`2sdMvb-*IjDi5vE`O$M7feX_(ae-V{Nms0fTXnvb!dy za4CkVpDB9OF6Ch@fTD)#|J^4(mRqK!<0%^8Tc6z(z&;Fv>^R>0UF+hjk zIJBh_@Qk`kd_8~BO9`_nF7_OqkyY?)a+VfxcM^4=H7N0R!X2fr*JIyy&n0hVpUX1C zW>~xm)b>{!7!2APVjwtyC}h{d^lmmb%|p?Fbg%xHdhESrQ6IBtaqT!hKF2>)Fe@)Z z<>NOhJ}5in%5BvFz75T`mVP3!uDkA|1%X5AX%E4lJCe?HfLUK&46nc_v+_vTsQNWW z&hqB56x~BeN!R88S-!KBBnhausUxE=DGL?Ag)HdJ%O4a0X}*O(F+}?<+R6Nkn=tB) zxY$OiatY9kAVOEP0cUNqPG{ZWO!tEu%R;iN8Qs4+*G>cSaT$+6$<}kiid7Cl{dd0$ z#tpn_ot(NcafWtar}@N~o??kM9a>|hsMNX2n;m54*^mzC9Ruy=TUi~PidwuCo65di zDK;MZZF$wC6X@)J6y9XK@`c4cKyx;noFGWFl0z36t;Zv&)5fV*U3! zN`GF+c2-p(#=;vV6O}?^`q5dQ%b(S&eGhueP|vCiz$~wY#YNQDq|C*RzM0h&7}v<& zurx0Fu}v4$q0{pOaR%G#FVJd{N_vi?JjUFobA+Kny^(BK!1i|mqqY)k*#uNoY}NkA z*3|7-A1tHgteBjseXJ)7(uZZPn2myQQDvU3|m0^uIO3$yM|AB5V=0&Tv;eM1StenJdx8#E?Qu z#pPDD8``z1H|bB2WVi`s*K5=2<>>|i2oz0xd|4hrie-OB+1V+*Jl9j@`OK85At9%h zJ(Y47`~}}LI?eXe>2$o|hMtQhpQP}8#riZ{p3wogv|y6UXRMXB(zYX4>1jt@Suy){ zj*WqO=WD&!_X{)2 zP4-AHIR1T=Gq$B_fFtgErZViCcs!p*N)?g)IJa=0?oex6O{MQ-sn3`uhgk24?UxAM z3ojRXsabPU&z78%M`6Od3Ip-;%!dj-8ntw?kW%Sda=z zv$5@0NfW^hQQ4gfl=`eyBUtLTUJJl6yt4do55a8I$xg`ykMhWR>uq9utuYTwG(x#Q)0 z*MlyhuQ;TZ-|=rM9i5=GBRUMlgHNlI_p4R3zQlSI%}_kEbi*xGoNf)vqj~idd4(-v zk91=Sm=`bMF7Y1#yYU-QdkI^y+1*fU{ne!RExnn}j8`6*(J}OAxB9Cj5}Bn3%<9{0 zOx8D#Gd;zLJamt*A0*eyWTyr)&Z$WjU2pNVJKoB@Yh4`m#H2^;LBz>EHH1D{s?XKG zXLLYBgB`eAu@wnx%PFz6RVl5Q%}s51uQDNSYE5 zdVQxgtk41z)|?aC`0Cc&(7e@rp5WwfD&TuszbLYm+bqO8OG8JesAZwc3H~-q1!HIW z$avKgNy3-=eI46PizOH@>CKHd&iZ$K@J*~+E)$qJrX>cW(jR>mwFiCUSaED=?h2z2 zxU!rbu8myb6BQMMzze+7N2#ImVmQeEN%zgGc7<1i_b)^8#=0~f2IXrcMU9~Gkq=vO zZ;b$lF8MZH3XmT`_?%x9Ua=Nl zgIYNX_H0)!*DpQtARSAbx9Dp9bhN6qYHFQB4#<-pI5TyQ>eTu1BgTSGZfB9$-pLss zm+nix%}@gu4(SPcG0(f~KB7aM} zG&Vs5An}#)4IsGKsHFn_)JLpHtM9{#tvKcn&b|foc2`tviOD^xz)zOxqUDs=*tWh< z=AmTE-tUg}$gJ@Dx;dPyfHe?;BNPE+2J%RD)MDZqcrqVUa-J*A;f0yRawHRBHPZ#= z?%eF|S2i_mA43ZYSYvmkhQ`KP={6{#Cp3LTyWSPv52|iTt_P5Wp1kf7s)3=AA+Qj#qE+;Lj{2b)j`JKYVB{&$#Bf zR>dHt4|*hg_a(V@9xH1_YZRkdaXIwo$art)HFtXc7;-&l0#m~QW=Gm4s6i=w*GydS zI7s=P(SZURZ;4f4lfIt$3D0k_H@{ac?~@#CQED-oZHO5iNQ=4#dGw}Z*idn)srK1= z1Z+^eXv-LCv_##Us~K~HL!(40GcD8Uv!;U#Q6w|O2_5NOj4?h=e=ry+W=Rs-P5$!q zNLMsJx6iGysV^Bm)cE>FggKzvRetHWN0eO0A+NA1M3^L;*?hyEs?HPhxWJ5GZ#_uy zA!PAMIruQ*auZNlsj+mKk@llptd;VR!jq~N)b(e7Hl~FPamQY)%>y z|6MiJtc}{}dR)xu?Ni~0Db9G&T^`Y<1x#@C<4kMol!gsD{bFVo*;!LH|B9TN60pVuu9AtO`9sJHPV(oM({RT*tjMC_~K8NSt-(mUTQ9b-p0+ioE) z)V@F+=kh>iU9A~J5ePp; z5VTJC*93ECv`I&b5R)BNsk*Eu+p8bZQ1w6>9i~4*5zqUlEy~M;KEBeDqQ0GEyrWW| zH#7M-L|_agF@U?o6IIhK9nGAMnJkBwjXMl@`#4WIU}w(P@fD=qPHUNLNIt_T(A^Hg zj2Uy~@4ekyd%720_6=pUujgWIHvxX<)u4_)CWyLNt+tVi5Ylw6nJ+dXb-H-To<;S= z*hmPx%3-(EYio$CT`x&Ru~k=Pj4$e!#Q7(lT`K={>)45&&O{bj5+2rWI*TeB z_J*bNlmti#@I0nRxO=sjRpvc5TANtn@|<1`(i+UsyaP{yma~X&?6Svvy|y$D{eC+Y zrv-zk1dwNLWM|m+!2i_J)3nkz8&B;4iI9j|p~5_e4FD&ov-9<+(4WI4WKuN&6AzVp z*k`J*VmPQOtp4i}Pfr_>FxyCq;xilHS`Y46ep0&GGy59bIJk8$y1-%U@%67`v*(*+ z16a>XpQJIU@aPEKO!mrm5qnU7TKj2v;d_2{;%213FZ71=f`fUZEWjb<(BmpJZG zv@iZ(<_Qs#moAz2MuJhQ@a1;{!{@@vpOi%FZAMQz%@{bhMU~O;6xF^rHXJ<{=M!>L zY-J!O#8CRmOAN8cQby4z+O!o39e7Q(^Uz)468eO>=ckPRIoaD)gb7j7 zg-<=c@w8u$`g!$Ye@>8tXI;xAIAAqXV(mw!sKXKGI_OxDAX*3I^>FRj ziWYU6fw8O83FYMrX*>QWVfw!m5RS-z^ws(5?D%A8iL(N!zpqQd$j$L!+|I~*lv~?N zcBU)`w>&w2caa~(!~hvZ)#xSZ$=(m5RVZIU$q+eTUKbrs^=JHYFxOC<8I&(9%5IOW zY>>1{5Z%+@oEn5IWVb^10{hkGX^G0()TGtnGS;_F;|BA^AT3{CFPOUidw~iy{=?Ck z+d$Topy*Ry>3p$2tmy6>A%_k-RVC_rG;0xX_)#lTsgdCLyu(`Gux z#<=_n7^Z)2lm9jAG(rn91)cLkA3S$XGZS$;`y_gDu5?o$LZgl`9Dd6n%>IoKDWH?t ztWM`3yM>*(Xykd#QJZ3%qWI=~>?5fe$H2iGgKSn{fXLXJ%WEalt(Ve2z<3=I_FHkB zXRe2H`3f`|);-WZh zxU?@cNv;qkbsTYRwX=l%l7sh^0=E&62^()k&e93xHEkha5P!e6qkX8JBfTqz(UC>^ zJJh-9xUUj%DX0_hO6~kZ;5j$Dx=k|*0f?S->v2QIB@N@`68?Y(<3o1P!<-kMvJx#h z`(_ba2cInG2%_rYiO6A{9)fv3tZncD&q4fmu({P zGcHL6nC3<2!@F|#=dQ%ccSG6z92RS~w%X-hM&Rn4BIOgytipUQ#~G3HRRf!3zRnEdePP!Hi@KytSoQx4$rEm2tQKGAf^gTwwW*a<8i!cX&3jm^(ujx zX>Mbc=B=2#OlL3L+Za%rY-WloH2T3*gT{A9)7LWSj%0`El}*Yy4(-60p0CPvfmo^} z`6%p~spNH~o=HI~M8_J}YB_(5_TCQoF`?Xm{YjJ;BSp?(FPm>w`7*m{7Kp87aiV1= zu|2XDGU7mL;zXB|SWCb>`Cl?98G6koJU3$R^a35eo>y(yJpE`814h8q6RaZNw;>vE zcP=>$B0iQhoVA_E41yhLCl8hD&w+#MEr*C}LlV1Y$=gZW5 zu;p}oz<3S?LIA}TCDM3gQi@|gjF7KJWAKSE#a6bJ-19y5yUvh zH|33_0?)p&&4^?MfHj;33%;bmb7*W!cuPU@2-hnTjp#Br!K-p^csNIdfztNym2o;9 zAxR+J`!r5*PqR%57y4}9!!zndGlQMdgbI1^gnlE#C#^#Q#dSvKE~% zs%3CgSng9cJd>y9FE~ju zO8e>hMNDPwpg+vhe+zs6g-x8e$)usEVe4n0JZ;hNZ8>q;IscQz`U}7}?*x%l<&@hz zK;FELb#nsyez63MEopC4qj9TJX_m=4lbWN5XZ3sbpig+{Rs>*1s0B^NZ>O3+S+vt0E|92MmF`?f?;q8Rg`TViyu0^j<(z*2|z|NrOz z%XHZA*k#fD^5y9gNdMDu`3s9rr>ZyQ0ecak<^|%L^74!wnCs(Uauobc%L`LiiOt$X z=4dW`h8mrd%lot0rel@wfXo**9fH^J?CJ>k<&LqMCR%b(V*++U%GA_Yz&R#Useljt`86B zL=F<7YjeW47P4c{ETuaZ6k{%UGrYUZU5X#W%OSdXTB0jTSAF>5k@rrOt#-M5^vriT zLq={~6*Hcu_L49#*)XZTbx==4g^+GD+!8UHiSX-sTSA|#gy#$>cfl+Cv407^4L-jT zu@TnHi7h)QoBBt0|0KY1xh`73uA9kX7^+fCxBbnxYpxAQ}7tyxvvKo(tjq@|))e-Hg;n!6k21IuJeVGwsGuphP79LL89K;;f?oEW;L3DZo@wZaOV*2x4f3~b zl$xfy!`(y4BltX zH}?-TQ>)SGrB+6jbNOK@?avTfw<7xvQfW1d! zYJ|a}z*l-1C2j=3ge!lBL6mw6Ois7{!R_c7ur*`P<$GRM)x8?Dte8sC$zbj!^(NHZu00HzaJD#uxA0!G{!{fks9J0^A3xCEl-}lPT7{>a!(Grp`*BvMc5dMHt^crwmhh>rC$( zYSAuVK6QL+tP+^MgH4l@+(}n}fscAbokWgB&;p7d@MFBS%lCrpI}C;=2JasAUiVLk!0z6O{K!P7$~Xbj%hc=HMR4d2)zAC*)LKRT0Kk1yq4>mu*@lGF^BA)FtP1N=r&{7o~Rz zlWq=jmX>HL8*7csj;Z~sKkd=Y`+vCG7%rNeY!KPmqPU(Mi9JHP$C4PRV20@!6qe$$ z?uVXKSDh3s2kwd4cc51j zhNcGqxASbuap%peT~(;O7BnYtk9ge!jARb#ti9w(XZ;3)G0U>6OU`BF(Bk*@LSc=T z2Zo>wVs~Fd6(`Sn78{t5Z?)3sn@si{O3K2Aw}&zrDkedQO0e`lCY`n!hB zmLWLhu^JINvd;Drr?ugR1G*x?EA**XAv)iltxnD~159NN$$urdMCBf}{9`C6PxF?) zihhk#2s#~UnK3`nrTWSXc1URAPFc-4a*KB6bb|+c9vZI*m5aj$>b7hTbTyjy%JPoc z55qr|3`3bZk9SQ0$GSwuocj`k@=Hnt4a_lHy_>GQ_eGDfae)jElT4e2MqOBAG`6#+ zozYp*7fVMz@zw~Oqu2ezjJ%qb;Vw6i}D5y+*SSI6jxBp z_#b+`FQDGG?=@ves%nX-5(!2W!ySDPdr6$|y?%I->ll{@xcTB9T-;Bj0D2@C`GS+@ zY1jWuI$j!9^mk`4qjEW9lhF@~rtweWvUQ2q;T~gP65iKoRpcC7i<8KuEiR9SKl*fp zt%|rz?mw|Ct84OFALY$6XtO`f$L+BEPxzqqw_g6q(#ToBaqXy?`s&fDK@@U}quiW_ zOQJ1O;I_3HKqu&bB{XmUv&^$dNXfqajyClYn-ug*gDP2T4)Px*(vIbF%hquIv{e7+ z+DOvVC@-nyBD22SA?R?`8s#$+XOH>OlHZ=1iaoV^5l>3=P52oXRNrDh=EQdUr&dl)=)%9>BYS(ZWdq1k7cq7S_X#esQySgKjN`|X$SoiF}?@n0j zw79~`+O4ZBa^}^~Ib7c4&54NgvtCgdQADIP>bH<`E$P2v&CZUoG7u{pum-~At|O%_ z774+*>!Faji9m-HQ1B(cw8}3giBG{HH$p*~&;&!|1Jo zoLD2LxW=?n(r4#eob~LZN`O|71 zHaE%13gl`tTs=$oBwDmsz;?3L7|b}15m-f!rS|LlIMmTKeA*-YoQf*uTnOu?O%h1Bms*bWJrgwyTab-%$6!#=TASw0M(yuN#mR zFpG$@FF|4Y@OjJ_g_9JL%heu!3CVdjJ+-2?4sAb2+0EITMD^kX^rF~h=;TG;@F)UF zzON#w%E0O_l8l1<+hD$%YCY}D_(uv|Epjx!DB#QE!l0Zf;Odh$bR+@x5aU^6zcyt!X$*$R=laV6KL&Qv6v!DG2bg>TB!Kv8(pQ}Hd+(_(aP#wrU;m3f(j{H$}Udeh&|(%dv% zDRB-$kSLDAt*qMnK>gXF3#2%)(DhtA@yNx+6V(cwJa73#Nvt4i?LBv|P;B(7$W(}1 zQxy8!?~H_y6Bj8UUYiMpQXXi8UXz4zM47W+a1wPine$huqQf@FnDzLk)xFSJBxZ{X zjnT1c_q3S`autEJ(su|qa00B*p?>}_wB3Q^eZCa;y`?` z8{|;4-klu;X?D?3eTQ7Ls!o|(^sU(HRXi6i3^;MvsPODVq8KiU%&-|FCf-R-**(mj z$ly3_-eb`g*t+LsDH@z4@h7k`=K0pl9_Can`dJ ze?i|Uk^lAIUiGFK?aOe?y2iF%zE{1%zP1*BxonLYHABp2aTvIQp(M}I+Q`=vku;d5?T{XMcA8G} z!fge4WzWU~t%khi87yC>Xk0Id?tGo}Q+dCOraV1hZ;TN%q%jakd(qdK9>rSSc!S8T z-|FSSK1Mz_iP)2a8asIHiAZN<)0p{QLdgu5o0q-w8arNHtC|l`61WrF_Mpba3ETfJ z%%6~66gyx{Ca8emj$yHYro+wV{-=l)_Ti4=hl@Aq`&HYE zgN)#df}2V4kpKr%TB6Y)^`&@mr?O;2fc`~#RqA0Mk%75xCOd$67{KpeG-I#8R#i?0 zyrnIfyYgkr=X}v&4~|a>R;6a1-YBCC+?kle2-^*yB{2G;h`@BwZSh_Jo-5eJ5j7cS zr#QGUxp+Z+D>)z~QCH)_z=Ym^(!-lSsQ)_WF+jKJimN!vUaB-{JGVY5zaR1ByENVw zIcQRTgWkjN+I>%jA~@ZJ$5`+Bgm;tklT%|1gY}ZxD3=cNRQH2Z4p4)5RD*db*WNTo z#_BojBJ252J^4Ns5;~Yuv%pmUs|uGjAJa70O-Jh;`VA_w2K>QzYjh zjk2uJSa|oQ?<0}{8ttLHH96=Y4UX9_EB8UU29oP=igRyL-0|yiy!&)nL8u3&)Ix~n zDc$$h8C^r#HUS_#*{*Fo?FUk5TX!BH6_ zoJ|3((+dW}5_*8prYALIr>PT=S!=^XSylDQA9yKIML7stPJLl=M1K9l*GSeQ`rB&A z#+IE(As_r|+h6e$T0c2MQ2#`1v$f9(<%!vV(^R&$*MY|TgVY6;ML%=d_=I& zVL5dlQ#K7$s_(Y5NiVK>jc7 zq*;gdXyaqcR3By4=$k$f>()V~l}Tx=pJH9GVI2?nC=Ol;eHFDxrJfE@{?VB1C#i&N z>n?@%2zL@Q44Xt^EqQ+gZ?Q=69?AhxdA?V`(V^Wy z9ix!BW9mg}Kh6n6!E1wi0x1iGaE&KI1cvsT8}Tb=nih$1dMAMmLneKIh=g*5lH#*`$ zBZ4=@h=r{yshIG+37B}aN@;dB!j;|sNcUg(8nqm!ndnGIy-_B;q)tn-qOCBdEg3zC zsN2*)>0~6`iEoqjd%yNtiW!vEL_i;Du#w7{v6LG-^ECDQrbNnVj!| zdaf4C?+V?fVZ0vEVvSccyatdptHbt2f)LTy>~p-r)3E;UwbR#Gq|#Q5=k^8=>c?)l zbWL(lcRffh$SQUp=Q8eq|6YOmPb%#;U2Kgh+Bm?$xFhw~8?Y9!z7d&K8?KUzW zPyq*zp4pfyA!~E<`38UmoKp1Boy{2Ila!}1*&dKbJ8qTI%hQ||*3AcM!GVAbY)#dR zZ90x|irB(wGRhL4$D+7qds#bhX_3x`x2@an)6OOq#5g4J zvXvizucb%o9s;Q8oaWTX`xGik1bcBJrB)A<1yGlH3Aqf5upS~aC4U^4mp5zGWu zAbvC#bR;zCXy<1hwN+*P1Q&C`XoYpW@vud;SflMRi`;keG4;`SM1t0!!x*_VIMN|n z^mSd^M0d&Au~w}{iHBYnT;bJ4RE9s_PH<xdtUKL!1UIQ`+Ur$ z-WaQ?7S7FeXVg}E0)Ghqyme>mMNqGn$hg@FmG4B$2-@b|li8O!IV0o<_(_Q{n~-~w zjIa6pa~2voR8y~exShCwK^jp~RT73+bn!P$0Jg?fBN`q{b^W5ycHq{RRE@(3m}XpUxqN9keZ z24{tE{7o+xQQqA%C7qBf9lUp*a(qaR0eT4fgWj_)YR}s&WmLV4D0$z<^d3Au87A7U zJHc`#`;x6sd}`AUMU3eM+!R|;j1Iye*|D4kNy|RA9(kPJ+g)>}5fD1Jq51Y!Q$Y1>`Fd=uG7t+>mewBZ1<;;t{=CswmA+%syIMqr^NPq?nQRhdl(C{@roZwzK_@` z=GitritJjRYvVVmKnw=Q)24=hHGEvz1@%iyDXM7E!H+Ze)TdEtTgg5Lj_Ghv zNzPUePnu(})3CtkTWcL2G*i{C^2!DH(A?lH`S>_==b#(;uJQcciRR@r@-ur<`RI3Gd zro?Q4M82n2Hu=*dT(F#BZzSia7|=RN9<_8p>v zj50unE{7oXw~DF+WM#WIrm0MbAxnxt!M1@|vaj-s;xH!U@WV<%0$%S70su4G<6tsq z|5-R-`9#;YCh_3q)q^%m|0|+>7`(1dH=WrHs2Qn!ql9TT{r39OW<-?&R$QUF5P&CF zr(3$GoKznF^p$!vce;^K)cB~XI0hs}yeb8N0HseJ&HiBd*yOy^Pe;wURjCL}_Z2c~ zTcD9_qT_@NDTUI*_xiRN^l5&q&{cyh`jI1{g;#&} ziDbad7l$cz4gWr~Y-OaEv=stTp{|HJ&`vBiFkg=AtIg2(Z16Z0a^BXk#6&UusxZ|7 z;W0UL^aN%~SaI`4*}Ix|DA`C|DRu>4`RQBwC!(UqU~dQ5%vP?SAiqLA6RJn6q9y8P zHOOa#MLhfVhzz^ez~L-HDkcetbH=`obs67jF=+ zulp#J-%AukhYOOCUmY587kDn2^=4*_Tr!F?5tM$4zUaL{Va!Gs?9i{Y*jTRHLq^>5 z$wcmiqd##hRnv`^nE9vPrgA7Bu=PN@MzQYQvtPvp&vB>)bi)*#0Xs406Hrd)`yNxlae7UcX$d1=C$NNmH?J4?igIOaT$DEBNEutw-jeH5tFnQbAh zF|uB}B@aq>%C`PMz_A{(`n$DpX5!Moog4w64v@|_jq&BSOQsG($Z9f<@m??Dg-09y zfi}1ebK>}NP1yYNa_UzIhwx+OJ@qpCMEGyaMRLN9lsq3!`BH2}4$vjHqK%UMpoAZi zW!mxWFUZ9=MuG#i=h-)U8!YH>*{VVCJ3T1NcC5?lJ?&t3{xwlM-a^hH26C~Kt?(-^ zd)f5((uJn}zQm9mP?$yFCsOUFoJ*NHhAzdf0L~e(CTV^53?bygW9FwwJ_?$~>2lHe z)42B~MSAgw4Z9qpxECm6D0U{-+7ylWPQ@LdPzkRfqhcSZJm5yIyN~)%q=zTxYQ%a*jDP9X| z6NN{l11c%b8$IE={~IWiwz$6_3w}nbQ2ud_eIOXg6sBQAUgpX3a7o-70J6z{^559- z)8ntL=mqgZJ?r5{%R^>a6+*SoRLw(8%#_AM=ENJEZ(%8af3|*^19(}>H|3`b|q7pjL?q3uyFiA7w zyaD~r58MaDva1=!>o`*oXF5<~zZb$Vs9G;CRvku9A(k4s@X=;BzZG*B_R^#-bM#G!z=!J5 zt<9d`*!1zm^TQqf8X)NO&6%eEng@3_ik_nM$8&KrDy6MB;w$nSt92d-S3ah z%7l9j%KPOgyF1zJ;fD;zHb;*3or)Jq&grHj-#`vOm-*+c8<+5%UBn8lC&9_7y}z+B z9S+%dnp`l4BkH%P_TU{V^r4bf!3!_BcEJF~5x&Omd&)NyR~DvjdWZGTrW-N;g$q*e zCg)89iFqEuC+>_kdr$3|7kA{@g1>XnwdKC@O!AVO-L`k^2{< z@XHx^*~X=XX_q|_p1@beK=Z;9s=jz$v}h!-Ym@&s040Or^GY?i`upwlOfueOe(GNy zIe0@VGU^cy+)}AFZiqaCuK@_wEw@IL{5fTZfvemf25nJxM?LQYEF@eoy{VrEMP2NM z#XWU$0>c?2>N2cQ3QeR?sj&C3o?X7azV_*yo9}DCSTo+AsDe1O{((>dX1xB?C}X{7 zk9_X0Ic=Qg$>PFy(qL*QV5La^GUN|HNUT*XyMvBk0gAs z|AW1^fU4uk_Jz+mxVyW%y9IZ53GVI=!QI^8`5Y^{d^xx~fa}_Sz(@>2Rs*``Yn`AOxQk{nnh+U9zFDPMg~5W#d=x zB`jFt$#hDC{KyUN6t&uI6<&Snq|3C_`)zRwpO4$l*RH)kt`?GJG-;8qL{-ZO2ig2F zT%^FH?Ql5-UhTs?9bqFLRobl9JI|YwLhy2-%PS;@TAM5^_=My>q6{Z;)e3+2>vWq( zQU#gDNil1++^_1p;V}L&v?!TO=S;gNSK%z=G zs^dk^UF#BGt#4+#hP%-^_8-rGWblXgUH6IDeuaS1NEOl~9jsl- z8LaR?{ki59-3LoGgSVSFrBsEci_(s?$)e3Zz4&z2C|T)V#Vo+J!01vHG{(e(62o%G z9?wd{>_Ygcnq+s-gQ4$>{X7~&X`^-m9YU=ab^`^Qao(2^;bL)i&~{X2`+hV!rOY=X zOyouxOOe+L=-;1J$t9wRONxV6g)K@7A~Vu0sr;Ak1sEQRS?<#WK_3*u>4Yjjs3t%A zkOCp7&}Ymuv$)SxxByM@0}iY^PBUQ3D63K5v7eKPRrO3rKTYuTu{?^%&f-eVp_>o1 z!hl?DV*9o6nP~0TUN%agQ8(F5 z4Wxsd%UJ0=*l1=0&)=b?nGjHs>#N>Sfd|x6!%SA8eLFDoW>cyPXpP~~_fv2>3y+@F zO@)qM34e{(F8Sal=%RlVt3L5@F&h5?<}xiocM#*)U$Zup9xhr+{-_t{zL3=(g7haq zIElrs{{Ug*;{G?dVEoR%!fA$ePxuF}ND4MwL)g_Sbggl={RB+DFBBLciGH&{q|m0! zW-+xKKQP3vLX4zCm4GngrAtZo)=Ajt{Zp{^U=Oz<_B{-Z`K4dMAv{CC9;Kd z+j8Xv7712=KW@8x!kCX54+QMVpjCWifi%BDeRe^e#=(I*I3i0$kO@=CS-vmJl}ft` zh_wsc)%QvK`h7pZ#H@j4OQ=KcaI)wyV2g&Hl0RC(e|$9XGlz@qBq#{jycr>Er#`~- z)!S#UGB42|8F`<=rXYo=S_;*1{_CLrj$m{~FY?YHy4aV?fsHWIVTwgP&T*k2PAN@z zk>ll%jvKRHaqUz{X3^YWGoBY-k|eJ{9`H_K8fWgZ$%B3eG&n{t+X)HLB}N&8rWgb)~N?v;P& zq4&MpTPC-HMSxA*c|PyK+O`1naZ18iEE+v{2s!Ijwg49nI?3UIWsF8x)l9vEp8p3= zv4lxBm@lvB#ZRlCpefau7(A%w)PtrHiXRg~FCt&y88QmPDF=Q6{ECIUH>2coBZdQ! z5eeJ`7r;`&uos^|C%p73{kj;f)qC_F_eZU$C>k>r#NKf?vBX^wZ1#3`eXz3OVchEg z$h|^ArKNtD`SWKq%IDNNF>7|CY1F=V(RKzMC!^GAzE*y+n+w7{n*|@aVRQvg$Gvlw3bxc96M7_ zNPiGe1&d8+dyDwS3Nc`Hck}_FX}lA~3jq z4)M_1#zuEt6rQ}V++MBJ;fgI_a5-e-BOzEv7>GqkXCGX8&3c(xZuf+19U)#$@t?gfX_E86Cp;L{;XdSCt zxhB5tn7y?T!=YIBdV9|Z`riE_(p&cv5c7ulVjK{#V}=`GUh<|a5K#;~C!`PcyKE*A<5xPg^THTf*6@#dQ zd7IehxPbupIsTk%gcN_!sg504$9WbY#-CixUk;~$a0%9)#G+r>xW3!rXrN{;LgX#{ zL=)uZFnJ}`ezsUDSxfv0_uhIIrn0J6Uw&|4Jc?v|Pw4xGYxw-|py!l=V8vy@lLMdF zvB_??1vRnyy=4#?k<3cII%GN2y}^D$9j`zhPI9}s9Iy#ls)lJzhuOFv>X0B~q*)C7 znnTSQQScw?)NrC7#(Bgf%f5d_xy+UL62;e#xtC0DVTd4S#z>QYvV_GXpE17Vn_+g4 zII^&dNON$0#R*7v+3`Ah+yyHmSpUMH0`vzNjmq}F_lIKN;lQA(q2Sx4%M=DrsRmkv zHV(gA?G0nuI*8{ihvww%x5X4FC=Zn5U6sf934ms(AV;NcIP&5Wo;!V0w{xQoASJkU zR(_>fCC*Olbf2?<-soSdUKV635sjc=GcZ|iMWq}%{r&Y37mh$X%Z*qYSP$ivb2S!9 zjz+2!<;-j46`{JF>m{2MiG!Y&A}a_?AqW(wLC$M98n6D!(#19^`RXTTk#+>+LlWR| zYvrrFxr6c@8@3bS>R4g|iO%7xJyXag*+#HwNIEq}N}|=;ckg1^D-5Rt@53&>gaj?-TGjTPh(=z7rW{Ndk=Y-g*%Xa+}W z^cYY-_9I}0GJD9-n*-bLC@^t^p%1=yIFXYN z883;(V1H7bC4+F}GcnKErLus?PK=!Dv603u1)~wledh-Bvzl6>*kc6{&-6afivNgM z;=qJmzGXGp%w2$-!RV)qet*lmXUTqwB;7>|sbKkNync z5Un*obE^YS+WOF4u&#hR(MD3L54$zxr~G`x7Fe_EuogE=Pb{RvaR-;z6SKOxkX5D* zTkNv@-!?mxxGaZ}w8cuzlA|DZUTB{Qk%bS#mw64C2QZ?Yrs6p4N)6xrP@9{NDCDV< zZf7t*;wi*A#3BsnOx*Yyl(lUwuORli*yRnsa|edJDPsBBg>(bXZX@KR5U+$sb&|xx z<@2-C8s^8FwjHPT#rHXk6n&9ST!f10f@vxWNeEl7EOT71qxaP45PcEjMa(gfgT73v z)`Wk@gO4!NYoj13^31W}W_~w(NOw%HS*`7V-oAN$+gu(Cdr`DGD~(~HOofR-3A#hc zF3;;J_BLv!@O$Mi;A_u#DHJ!)MKNrd;##(rq#QC9jmY%vl0s#2vsNo+8~9p=F*fz`<0Bz4#L zvEDi7CRMgIE_$1{e2M=`(moC#=>C-lT!y6-{p{K(SR^YqGykhK)1qP0+sSK}9|{d=T1H9J>lj99Ej zCYL)$bc2PcTpaAL^%YqgMvCpeNCHret3uFz9Lxn55f<3S$gnwZx@Zcf4p-pj^V%h592GD30cC-MZCb*q@;f%O*;zLR4pah98x2J-BQl8r zMpH2*HLfq*pAPu%1O+eWYZ=c1v3FPKh?d`Kn2zV>9BEmSa(`K@%=y|U79FHB5|&qL z_rc+bIaNHmdC;G8*3oqLGG>mdV+;lA+{oe#5*rvA!($^sp+`lUdF{WvE4VXV(k1I1 z{E3SmkB3J(GG&bjk{;xITpmt*4`X(0=sh2I7Yt=BzN!!7 zm6q6rrN1`*2By+vV4Xn7`s>Q!?K14mw?Lx_6~GD~lfid3KFuyZz2wy`@$6STE%qj2 zW-wkMUj%C-89xwuK{d6jf*XxY?XN)nNrk|7u6o^HOkJzpU0W$?J0jTwUvAO}MD^YL z^4uIf_)+euZH!|V)$B9@Is9F!bcC$B>xU2h*g@&Gq+^Z#!p*_Gg~?>LOU66T71}*y z>1~V|o6nQ_#c)$LAB*8?TOBnN3kT_N$NGaw0HYw95?Q|11u#9AssK&AK(>2ZCM>cCbN=rdfr?HddX=tm#d8P%vXN( zY-btNuvxwbMh>edIgb&g6cK5qKjeR_zFMElmzpQ`q)}es7?QUC{66^n_}t9x?SmS( z6Z0Y0#1R_n_!Cb3=Ii9D;=7JI#WaUbsFV5+LFICXHiwL1IS{gFva;aGz8N_{ITaX=P z>d9w!rF5GXK9lS}Z*sj!#-F=n)Cg4Q(br%qO=1=D;EhVcPK}J1 z;QY27Du0md_3{f^5<6hrfYymCib#a=!7gttr~ri!Z03>0WpkEqf}eYpN^K7r0$RXE z!l?lyIJJ~OM5Lm8Q01X)etqH^M$9T|>fTI67p1v-`K%#_pqt%0d82?dXgA~czKcAn zkALGE?+q*J2`$O2`2=&xi3DgEwkQwM)-)(3-CUh5dhHEe&RZ!B%b9P5hn^w)m`>AW zEbq+p55JiG2wgj`m##-45E*Pr>NMxiI+&Q(`Z#JPVm<7q;k~hF_;HjdsX`MV_i1La z!r237o`?>?@(o=eqei%(z2^AR*Ww|SL&uzM)!IO`v8Ol7I&y@yOWL;6WHr~7=gj&w zc36P4xEUp-OVtmuvl3?eHZ!sWCH(YkQjQ@7QvDvSRJXbws4tFZ`lI8e>V-f=#T`x| zTp|~N-22K+OTJ1agLm*znoE&67lwCS<_bExD}{`x;yer=od!vwE8ee=>Pfn@#gIvK z-n|;ryJ|rsphL(IuS!sehjQ4`;)Kr6=2nHf62pKA;Xc9Yh%5R`psIsPlr#k1G7_y! z&pDdTo7y$z<%}t8{2cwbe22ztDCn#rn#1HR%vZ#RIiLO5aymV zpE-uqBwFX#xb%HFV%=_m@$*vf|KBpiid52bTF zGgW@98YG+qg`8y=t8!A#kP#Iu62k7RB8iBlHXDCyZYTb0JB%V_fWKIN)p@U3xK3)O=%;eeH(ko0Jo(Jd`CR7Z#+@fNDa_ zppBO};EadYi0c=T&viNnwQtE!x_w^YG?<%Zao>Oc+Zi;>058h}oE?M_|<_WP3M{q@u}9_I`k?!4u<34EMxMaCi7S&YPn4hde>Z$!2)09LAE-t%C)t##?bdd2~SH*dDnSko)sKl z6l#>uIN5dGMakmOIi8+N4C@J#(8XBhHdK~vrkSK6#aDMA`Gu~TisejMZJpYo-s}dF zq^_>Km9pFSZAUG6a3MF6@J|3$xstY=g`36rRIlTI{{Bx%AUkZqFkugJD4-`}J@w>C z4KYV<{YT*1qpKFKLG~cUJ_WbM_MRomzNwg?bAdWStWt)O*+2vopT{*A97K< z@L4Tc<#5qSadA2I;1@HEbL2Z+wF!7dr#p)jO4Jpr>{b2}2gLceIDzYPqP;$W{cCRm zOX38@gFBLb9}UcnWG6O0wSl$h#$2%Y6Fb_2(x}+<5yNliw@moeYgGzU>%+M`I8T zKn|(sOSdSah3K_9PE@iTT$&&BedvTSQAyQmLuyqy`GcRyk1_1CGT!bWwSXID>1tgn6Y|fX_DZI+fBx``oZQ@O%Gnd3DJaT-_;E*{}qu zk6&Ah6%3n9EsPLDi?;|_ALFLQzaQPBh^1n_{iLc3VM!hsx*hu>%OwcKS54$1u}vKu zX5fD*RQ}k?fX!nUH|XB}aHai3(KaU+bv%q1VJz?~T~SbLLMYf&>tR~V$HBFj8edw4 z{4ZMka(b&!(-Cnfr*kl3NmQjmK1-V`v1GFt@;HZ>YRtXP$)>F}RRNX`R7eZear0_z@s0;G3KR#ggKi|Kk7 zEQhvmf0p$rCB=uqa$szg8=sW1k4D6R{a)_rgNn3FroH%Wn3lWZswr+L)y&Q^X&oJx z8tq)+%QY!MrC5F0S%iwiuz1$m6bJIHa&fZ+c&{>As2h&L5<{Vtm!fbJBLs*q?fo1% zuq%xIa5f;WFwduwikFgTi~PcOKEelJiBwsqCRVNYPOwyL+OCR zRGHZ&k}&hmnt=ijmq0J^SY1EwTO~icV6aZinS{dTt<&IZc_y{SYc&FLamXQ|s{8^8 z2oG4z+g|4>q^f8864ufW8|$Uq5)p7=e(C~(QP8KT)hE7tX#_5`9jP13xGz;DIy7dw zj~X|+k6Cy1_2gtzt@NJi+0TnDb!w<&u`@o^Q;fLpJtC-Y1K1Xdm zhIJp7{6jM=+b)ez(el*1%rbTxiV?1>ODOnbvTED0Qt5uL<5RTF$OKoISBS|btW*p~HGCTU!4(N7)agzvRkkC`A z@EF@+enqTEb0Ot}f{vEeh|72#Vm*&Gnob9x^>=fcr9wO5^rs;S#E^)}&m=wc$<%?mVaA^adXH+$F+=q#08g#mY z8UNyj;d`kTabCXV+bvbE6Y zlu;+KF+gg4w!`V-mH6#**(`a|pkI<7iSgkep{-$Enx<%-pOi7&oL^(4>i#WXQi;t+ z)^ZBSQW&Wr%lua&=_6G*uP4sk+0}5`Zm$z4VMu=wyR#xbgaU5_i|*?a4~WsjP!c zv^kzMC=1uACqEF`|3QPCB(-=p|1gHUEt1>uO50N%*HPZHm=Imn82#4v>y#PNq~k(q zF1I=ecw-vIl;~^=+L}D)8z~1 zmcdm4FwoN|Q!Orhz^2w1pIA~aAYbZEc;yuarE-ku86#1i&4s%j-Fm#O(9$X_6;IX5 z3A;wvdnZhdgpoPeg?MYnygJgN8?2J(38eHI^%-4=4!D^D@Eo|uG1?5wbrpQd0R~^i zQ@u!nv2ePKMkZOlBL+gNqOf7lKfU3L3T_kR8?ZVDdq!aSKvmK)Z`cujw-`%jW zqlTZc>!ig(*QKJ;PFi7nvHM2FKDUxDM@)V{ohX6CNvraRecr7V7B+fY(5{1@+L<7& z`fM8P}w`R98=qY za1nH3RiRU%uyll!by=J#RK__r%tR9^v4b`Z*0HPuETPMz8VRkms6 zSvek5pkq|$D1UJse$0w9tnl~2X*|8ci($v8nqOnsgWE+1APx&8J~i)0#h&lN1f<*jUpS6HMLy;ZUw+l*7s;D7`0N% zrZy$vX0=L-LH^AXd7l1SL_o$xg2s&Eqgrvw9bBGJ2kTG4N%mxZIg86FDz2bqSO`ej zjWfcdE>=fAX6NIq8yp*q;o{gD@)5e`lgqR|4O*#_iD&4Khkn9`eJ!>I&MD!7-MaY*v7JDrm9nm)S1ZqdWvp!GiBNW8Y&7HZ+fv{*fOL-zKL2s_ckW8!iqH}1|u4G2FJW8ah zL9r>62A$e09Ct4^5f(Q&N?jV^5EI~5PP|CCDFQ3z!n=XugJ>Ls z$_E(CVETwSz4B1%aZ1i5Ol>;h(HGiuY!QNGiad~>c{lz7J!240O&zrUn7vXDDJkm= zuPwg7glz7PEjP`(ahu8)R0^(A*_1I+GAnDNbT&vl{k6BPf?{mDb^_VG(wD(_wR|H8 zGB7(JuwH+&g4Sr5ca(S>?oN8ES2c}Ss-7qpc;H93cLli7DK2VKGtXN?!-pjV4M{+| z<)A=9ip@x^fZl+%IHhiM_4LuRX5YPTJ{;1qN#x?hy3DPC`ogd~05aPvJD4RTIU*U{ zxc&h)aGwe`d#)gGz7aYIL~TZE6?rStj)c~;s+9>@&S5+EO&A&9nTZI5&3C4g3){w*Zc6eTf~prTJQDyfs9A{TegaGEeMtI_WPyRmkEHKG%vo6Yntx&+e=L883v&-(PD- zKWcsvR;3Rqvf-Q7)o;xV$fvAmGY^PLY%8I zH5~p^wICj^t39+qxlL}%aoR;zHC5_F^LHDhZ4(3vh6+H+idQAKAhm;uZNl$$s*D^!x?l z%2+}n4Sw^@V;<>w4H0~U7CaSY;9M9{YiN-c{Gdc>;^W$XSO26@e3g+sx`Yzlh^IAg zmjXA*@2}osMG*6T%Pn~aKZq+QZOWlVpUSWw$UMeeplIV`m&Co2b@pQHC;{{+YN;6q z_t52(&VjJ!k|L$%uBfek`Jn`qt`WZikzAMTO~dK*QWl_TnzTebt7n(4@}#oF=m$e* z#P7-ad(VN@Y{5!zC8f^pBDVEu41tZcH~(HeCNlygeHYl&nyBJ@s{A|UE48ILVbt0_ zK9<)l(mHzxb`UH;(L1Z{;BhhA3ob>E)PB&Xz~p0+BQqKK=ouU+JL^DzvbWdxfSr>_ zW5mXXF^9D{r%3HQ^Sy*BYzrd@>k`EQ<^9$O<*+TcF}wU zFb@%n;bdhZLg3F5K_7tBuEoG&AlDhs8HWxPt^8zN33^d@R?7F$@c2U0gb>*DyL{6I zgDn{h8dY%hxpRAxId#)BO)t5c`e1i&E@nvwq3Wg3ci8dx6cu%A zE?lqqHIHn(Fyi#+Ow&OPz(N+10`8Fve7bT(ve%kT$JmKj!v@-SFG0@8Sg;_gVqRQ7 zt&*&b+SiBh+}R02uRaNCi1nGR1+?60EWU_4?F`678 z7a8J&PLF;OK9`B6EHgNY8k!tezRf{bO2I1S-~87%&%UB2~ton@si$ z`B18os{NXi!&xl3ur$9arX@vlJ9Z#SkXNrCz6=W|7}eXkrD0xI%k;KFK_Siw?rYTv zTrh(ag3aK0DD~t3-s&;5ytvrOe|gH^CBca~NDB8JiP}cFN8$aA8uSpHI0gv33HT4M zTy z1mCq0i8T>5G9TJ*l&wAzpo8bPMfs8iY61IXf+s?h)mkBLNy_jO}Zo?O&&gmg|3y_lfhY zk=%fRET6$pNuR*0arY{FKv|m-?y|>~k0`+_wYmmaa&G79*|0v?6wOK$<;A?(`Dhoe zj?P8=bmVOXlt^NmNRx78^K;3DYba#nVeG78=Hy6n+BA-2>CLCAph#p1N%RO; z+NVJG8EzCc@@OvvT!tUKO~jh7k&ASZKV-VLI;fq!L5lVUZ+*#|$X_jcMZ-ufpy0Vj z=p0h$NnC^3!hxLB;@~ri+0KrNLF$FY)BpDKB8z~!1e`@iX;I>U7=>UyZKiW$NdIDF zVHv%>W{B(V{ij)Js9w?|%{XVozYdhsHkpg^{K{3W^EQb=vT%+X1 z`2ClMHzKfl(%HhZzxWhqCcbq)6w($3k&U7KiB3@iX(GIwYjvd{jP5%TQ`L5}$Om;u zfdJPP2FL|+no*u0%S({4YRP z_O7ZXi2_whPz8GrNy|~1Uz{px_OQ=Mw7d|)FzX_d1zrEl=fC8}F68)P4Npr7>k_81DuWh9K}?I_98t|fk*{Ea zncXvLutrd@Fs)GKHh0;ajqdmUuRyaTgOI6xTv3Y|t?SRVM4x`BgJcL83qWzRaZcJ# zf8!%DdAA_bW%{iE}ml0SyQ1U{ron{`?ao@24YMNRgAM zQ?Cl^>Ti0durr$h=1vNg6vtVWXb|NTRonT+^((eq0U4{nh;ozYB#XZ4{!byU7mUeP zLifHm+>q`S@4*AssEM|gi^-@S8sYEzFE1^s;;U&$H?NU(bF_{$kNo(+2JcG*x@-Zv zVyvYz<6VHCG*=-Xx*T_<`ct^*%m zi%JfoAg$gjAZhe&O-0Ib^M=B=+Xdee+)E^zdJvm%=jN^sRiQ1YmUhIAecIG!cp|ewk4%b@k{IH`y&WWw`d%O^rR?coIFZX5O zK$r>k{hx})BQDA?t}hp#PcVA6(Ou$i6GeZR)caBOx02-S3yS<+kpl22u4Z64>xPtu zlU5$t5pKDPGs7KY`@etcr};SN+Xw;c{uA&J6bRlQgb3b41Oh++0030*??1Md zA%e>%%9@HW44F6U3*YX)0);vRm=8t-hyjKN5I5g@0|o%B07TFn*IFniAdY>Q@n6CE z11%+a(62ry0!WRrH5UgGjz}Nr)1_1z}!+^gO^1mH_%>>}Uuik%h#ThN% zTZm&XfO;VhQvQ;O?^uLE>@z&wI49hHw*66O$oyn`z0&&Q@;`z2?G^}j359i7S@2Ul zFNc`iZUXBPP;e;-krc>Faly)g1b`6d+>K}b^c(&Ej~&>~1OPqJCUj{eGO`fz#Gn>o0!@qFny%)?F{ns#CVHQh!XCdib3zkT z>Tww*cf0swB7cMeat6WE2FbyiiW7&#u)4s~hCos-Mj<-KB4S7ZOl4VzAR*a~ImrQx zjf#{IBc#c-{Ehzz>W|_7hWn3P0T3WtI3(fJ5m7_uIOWATRR$W^tp!=<^fBNXz~}!C z{ZCMWF!8()u)IkyjD>MJOF^u~1F#%nh-M0;yCdN9zXbK)5&!f1f8q+jY!6ofK#3K+ z834lO+5-^~n{F^X2kue-ImRz{{{#ie(bM$~0B_`}90XMF>wyAPpM?ObH$Ptf>rnrr z82^yyPh4>z2#O;Cc;54pi69Qj3j#rqXwrzoBZ%{msN|CW5aQPxe}ap{4UnBrliNb^i9>4$rK~NA-V0|b+a3BzX23CoJstQ(#l3c{vxIn66 zJa{P*bd|LCSA8g8B4N~@fG-whWdV`5pndIymKM2;jA)~w_|cLI7Y?@NEvpUBj|L^sPT1hf`^jyrC}+A8iBvxlYcFTOD<&b|!qNN`7O zBi)0(I6bjCYdX8bQCqM_P)EvSbAZ(ptu)|^q?%Ea26k}PvA8m!Vxbtd7W-Q9^9XL{!Pp2vUN3N|)n7^`cz{1mOpvLPv#mN>qu1 z&nk4fYJ;%Qhv1vWT7{7E;Jf%y0>LWuWDJEJz$&%)f$cgKOR51_U}a268I>Ftsuw-L zR=uTIC2(b9O78#oF^5v;R0Kpbt@XcFdEECE$?;c)Mwqd~d3lJvLlaYNJMC%rIN`p$ z`}RWAzQ(gaq%H|zQYU8{_ps%K$%{Pr(qYfdK)6~=t9dphK-IQcwo~@`F-Y8|z3V3c zdGjpyg#jGvQNrUElMS-F%F7ctbcrJqZ7kP$#k#Bho6ABoV!I*qhthMI zGAk0i9I5cPP|xaa-jF@%lMipnzNc~EsI1awF?eU32H#EIhj6Q5YgK<@T`-xBISA2h zdV(w=p9Zz+T+i5}a~G}(Y+*J)5O(ZjS|cU@6Wn8}T$ToDsQ6mW^s8)xqB*%0vm*eI zz_!_Hq$?ZFpRDfvKwObQ%)q(Mw+{K*LL&i}lh8YdzRtcVDmItRkA@%DS{xfYWm5%n z=|eis8CGrEI=-2BBlS3H*Q_|WoQ~k<$-_H!_4{QGm@v?%Z)9+YbLnamHBH^+)Yb`D z1koRE>%HN+B52-g9k)Ne%oy0_e)hbLBArk@+#Fx!M!x9_u%K4Bwtolzsij``-NvVt z@Lle!PQzx+*753jDE;nf3|az z6T}sVH}%qKaG}gX{da$1zkk2`joTZjU|UQ=7I!r-g7BeuA9fO-dWk+3GohaVw3Y9P z-DuPccfRO!8oM>iJ#!=;pZBR47j$=0*Q2slGJFocC)(a4eE1eKV!$qNav!2(n=Od_ zLM4jGflzAuba#2A1Eq2`+0Xl`WU?mmGutq)9oJj*4017sI7yiqbd4DL$Vg~EuCe_^ zASV>k{GD94TTW;>tzV-JAO{q2>{n*cIV)HT)I15Q>7zorP2ssiwcqaK_=F zN;yz}N!0r$ty8-O-c1n()iTSmapFmyaKAy*u~{o?6Og*~Gy*5ddf_9Kh3LC5>+qzV zN1}SLDt?o_4QkfER{PWg3AN@0%CYd?iZSkzY=6T(j%o(AK7rI#bl z@9}4?M#{jj@W?n_bOQ0y8pMj`!h9KR%h?Q(`@B|2md*QWBQlaMQPHq=Tx}l5{7Na5>QPy-xNDrsOnQ69I3~k zx!ko^|Luc;d99su)Q~^4XG+O#bWKn8kf0 zisp4^4x)+UWMFof=O!fAr`$PQ-mvdTPf}uH+blj`dw&AZ!*^j?OoK|Owz+2gMfek4 z!P8dvmE&fNNcyD!g3&Ka>C40euN_?P90JknK!AE=|K0DPO}or)yZcbZF;rde-p87f zG{E=6gx9c0D$E-_eSG({aZnKMhDHS1M{FU}#n<;6{cO=##fBXI^^6uOuWhd%>l-CZ z1PH>mK%3f`-Mz0vH$WCaEZx&Te5GjN*$qDFuZ_II@QR(A+bDr$>@obIg~Wq7zywCr zy#G}Y&Y&q4?qJEIJ}Q1}Ri74k7OqS7XYt5)5BjsvtnKFgGeZInA^2CbpW_Hc%Z;L( zzZo%Pl`Zbn1qxxhg!0YW-WcHk_)Sh(ZUCo3(=TiLzsVf8lJ>n5{!t%=NYmnXpBjkD zJC-M-m^e?igfZkAnr%Z71P4R^oA0$lWU!M(p6CAF2^xGbbQQuY#Hh$6Tm9z&3@^w# z`a4$!;94USvnyj?JG2J(xHe*3%abs$9f2;1N#~=F8zj)HLZRtmXnNwCcsZ!X-Kh1m9Fb|h#$ zeObzhP-z~c`TY~CYKN{zUBIQ49_qQ*UA^)q_v6tccUCMg0LrmFvqg_7ipltDxX33) zmQ@qV^U6=`(rse*wxcu^N=4fVr{!w7%4kD_{99VVmB|4s1M-7gB6u5Qgi5(|@^G)k zGSwNK(Ehrji}ads)H|v&<9cnTriC)_)xYh%wS1j`8>-&mr%16I%oO8y@aOyA!*@R? z51zQlQo|S4ZL`><(eKY6bbTd>F890qUU!_%bojOAS?un(_Ag1h*mYy&wRc~gW3{}} zc=P&M-r>Qr#WP;9oxbicP!;5*@ve4dCqYoKos1cbZzK7vsdls&}j;fq#()SVhwLx7nBA4!*jg zz7l6g{SKZ-Sc1@;GuOlE%z}`?S$Dj^&c9H^cmdP59Uz3nv=}Oh^rzAWd{^XpzXAKg z2TWY^2kN)J|B7|1$E?TX*U_OBQrUO^JcFlbc=0@JQH-UhX!sLwoj1%=$Ipa4wNSE6 zB>_)YYFQJd-WMx5s!05m+4EaG!T1P+sD_b$fF)viz1NFxP#5);TOQR{S?@2^Pm;xl z#cSQMS?oEzI}e%iQB(1zci!JaNCoC9B&%F&s-Hx(CB~|w)cZF*!5JMfc>INskqOD% zr=Ir{YN>7Hid{Qy8h^%|(v@jznES@c-56hNe4?Uu?@ooPU(6H(p5N#SQC#_#a}xai z4)S0)NehO>uC@?8UN8(0lhz}%Prz&I@9>TpWzg)K-Ji6HogoNNeD6o_#W*v{tfr92 z@D;!p&SiL)tz!b1k_P97UpNQ>tQ#${hG;2R`~B}yB@?|($!yk5TIXs5L8qzp|{wDI}4?y1ZioZfkXKs?G-96t`ijSomTP` z273l&$*4|cRQU$CX$g;V@>EnQ`HX3qZ|I;O)pS|z+SMI%XUX8joxNgFrhGMuAP0wY>fl2(_)Zn798-FOwDQ5i!|(+E(z4=FJR zz(0wFB=?RGc0*%-$ch!S0*tWssMQsx?QDQfxiPoovFNc%(;e<&ODe-r{}+3285~Em ztPReHnVFf<7UPJSS+dx&*kYy;Gg*=)%VK7h#f+9LW@d|-(L3k7=e*~OxOZ>tkKKrk zFTRP1>7JhIuFUN0s+z2<=eZ1N|74*Voj-y%#fc1!8=6_H5p!8^`&j=2MYC6t)tr3;8wf#g~OmR``GW zo*j&9CQUr0&~(bHL1R668Co_?~}2^Z-oB|MM*P)fYZ zkNbAbwf`h6*)w_%LIG2qW8Q4@qJhCQ3HNlLrHL*t2Nl6?%J4#U%dV z8Bc6g+1VS%h&PML`gUhz_)=VSjfn0lQjR4gkq`rBI?D`Zq{_#^$alaFG~_t}UNg$1`swK_a+KIl`V_MW;`-VNKGUk$qJ!6J#tF2x1^+)EhT zR$ZxLc$SBC=FOSNMK2s!2Tbs#LK9C~P8+dS+PvPQ7T&!d7aW=MB^P??NpxS?hhf*f zc?h;>cIaFf`es$z0p~}}u&*Nb±^&LPL0@#dX~ui)D7xeXO-Uk^UL>d#NoKV^eM zx+9Ly+dgj^vyVcX5%~jX`?slEneg&x$}0^lzo{*UbOq7v!VfmsMt}tH7Xu~4pGgUG zaPG&cHAvS-Ub;~YP%fA5gN9k^I(X0Fmaetq3*ZV3Q)BLMY`Qx zrii&`{sGJ}MWSotzleSO*lr|5jNl10&QM@d`>HNxSnR^a&pr(ks(RWc-_$*3P7vJ+ z^#V#LBW<$Qejuzc$5d^^STT}F`lTGD-Q!OSZ=&FF9Zrvxhwz-_SNpYokF8vOT_FxdKTUe>-t#- z{p9|5CaXk3%)gPTX}2#o7p25!B~#qW?XH=$uE;7_teD`oE=5f0-Wjozp@s+ns>jIy zI{OS{y?qqwBNr7S7xvlQR!<;Zs8CO>Yz{p_cKmp_zBfe3bCRd0dQPjAzL29(nY_*^ zzE9AV7VqQJy4?nkk(qHfU@Xv;rabOrR-I`)p}dow##+o$u2THmzQVR1*69QQ4(qI3 zGLChuN1c%7-Dh;5`cu5KVrFQ&xH|vJIlE0rldD&04&KH+Ztc7n#rtJe-??y2`Z)oKi$q!KwfD_AWh8>!hCu*5C%qF72+vD(j9aVY!scVw16^lZ^>$HbTU83)YA&z~+vmWn zRIdH%jEf7eA;n2HMakbUf8MTj+HZ@pkm|kXRQtC*zdb&r3a6eyhX0_XGjTGvDW=|wJcQbUuMfwUS%&stiRk!$3&1{ z6|o$-?_b2F7DdGVn__Q^w#TgpTE7v6Jewv0e<9moS2A4lG)7p#dwhMoxQyS~|Gsux zVg=8-*Xb){5rAiK6*pOo-hjmU(Zh>-kX9Vl!F;7}bJabR&o767T>G{55lms_!1_eM zkOHvU>$1ZUGt6=5;>fg?`PNt=0TOI)nh1{xAQxIcuap4sJFs4F?J$?Sy{oVB`LLW- zO}rTd7dK(?X7)6{ex8j^fPll?$+=~``YuptMP%cfp>jEX7Xc5_-fo{PGp~ntYclQJbxP`x_l=V4r!rM`D-@%uw_SKi)G9slWpZwu4AV%FdcCRx7))?wS{Js}Vp$W%x#LkPjfe>g@V2kCF*QwP8ukq-n9C2=UQ}(7C!Q+~<2JP$l>J zFRpkru)N{kZ(Yr#QN#Du5tx`niUkM}v2^M*Ot^;a0_b7~+ie`(O!02j{mfVy3pc-H zcXUVJ&ISy4FSquFq~5pS@+wB`H5B-afCgojDKIwAvVZ);PP-Jz-PIeYe*PN+J^|fB z*d&Thoc;el+6+NhoC5yBJcmU1fzU9J_dk*qq5q9}egOeJp9-Xa#Q0TB96_}`mo;19 zg#TckLt^~bc=Y+;^UO+4JX9(l3hL`uC`Z@Xvrt8}KA8ze^4pk?{hA}d`r)IeZ|_Ra z!J<}u`45J7W-mi9-i)uCa4*0O@@NW4Wyx3Nn$Ll#Yj;`K$kH*f5b|Fw_ePHh*a^6b z<6bhjLWiTR)&mOU({{K?7~18dAJsiM99hMiD)iEU{SFk_7m7qO%ciTuIvzLQ-hCV; zYJ&{~;ctp?^c46Nk@hrt1jr@PDb|+P=;8qVp?6+c4h4oy`L?*C`HFqt)#vh_SjJ>F z^(%S5XHVujxoRhg)yHN~s1Sh8qw@cnM*LM>xee8%bU3M_nz;r$_UfuIOQozAGgKP9 zaRj%k%1I*8YbFj--JA0C1|~37+UQXg03e;zgTth3Fpe`l{QN*T>L;UZ1hW0|8!UZ9?F(T1Zx+#D|t z=5AS9acFyWf$p-5Y&H0`*g~4fQ@jP@d}~&}*)RFlHs;BFI!EbUR19CkKl4X)c88T| zP>MSjFRAQP2mEgUwz)8hKzN8@W%!w{azitbv()|o(gfbjC_vcmVL`Ldbh za*d$_XQ^T%31}Zg!>GH=bA5t^SA(UBk%Vb+LX@Xq1+DMf3w9x>M7}(AmySM zyq5^ReC(T~ySt8}zdWL=DdH+cxWmZ0T%35r0+`Vpr& zWxiuxB)qzroWW97SS1hcA6ESVS~!PZ+Ai)^qh~s0N_+>MJ4{rAz`6nJF~3c#>`7w zV>^o9cEpR7Oyd$gnkA=wHcJ&t5Xk$YHYh-$phq2B!xn8xvpenJ1I5$>U4%olv$)Pu zFYk<8>{@Xn3xQ?>s)&_^gDr&rf_U}TH%0oc zCxV_4HwTNUL`X6(4)b8gd}O{kTXR8m&F1Y#^}kw&>0W%GF92B!gg*HS6TvA%<><#k z>4@JAXw(GUTxHhQeg9Qzd1B5yg~JYFq}EwHt4ZBs6P6ewCCz{wxB+k*9iUyhuI@f43g+3vN? z>i35#-U+7)#QZ;KG9L( z%ca~?zKmy2(amTmX>O#asRZu})a^x_9otrNdzE%02=3fUCOR_Vr-14jV z6B^udiMG8?$fmwI2Yi3jyis_0uWf$u76ch+fX`o5Uf=@`t};75Jd*tZ6ni%%oZZUP z(7_f5Z~1jVz~$M-jk?QjU=Jhaa`q5J2Z1J+N9eEb_`&5P-ZbwJ2$?h^_|Y}!OyOdY zlDt?@2D+EJk`CCP26&o&b-@QnrUvo3Kl@|8xsR3)Y_9u^c`wA8XhNH2*Aa@ii@YY? z<4?G&{c@e{Cc->p^X zo}hm*<9Ty5c`%`m*>f@aJvks*a(7I-ZjZ+*Ra=l5>&Ff_s*U*peurk)CBLk^K}${!)S**j3V>PQ9wgoRnLOE#TiR5GM}xT_O?Nj3N>fW+}BDgu6&S z(#0=H{c$Y~RxE5$E94J=&oZt^<#ae_^7^={3ED^PW#>bZs?HkdK^LJ#C%g`Q4K2;J z&}c`~UZ?y}Q>Ayytg;yTumbI7nryd&dGM9H3}2V2?{j#((#L_rWF44TgDyi>+QdkV zP!`kk!xI0a-qRa^6u)`45%udgSd4zpahPB`CxXeVtPFLiJiAigC`-+FrW`;SuEfk2 zeq2~It~t66?zlM!i3G;#_nQ}q)rT9Efl29i7&=#~==(JI_A@65_t-1=9~T?!1Re7; zL6isZ7$43cvE?oq;5g}eq}a^Dj3L6uFGVqa*;Cyv;U0OauD3S};)FD%`Q(;2PO?am zb%j@qnd(AiV5kx;bUF6cebNqr={2{Tg%CQGgW<_Vrwjo+YYtp!u(6VtWVj(*;)aBO zsRWmYZZyB@J%&;kDpBm@(Kr7iAA*VxrO!nwD{x+s6t_w*?_5hF$>S1i%11i<5P(f% z`hzPYp$_7!@>EOYLDkrda4fIjt{v3z_qfv-WH)yMjD?l0`7PE)7brI(!DuR<554;( zD-;>Icoa4sc#Y0PNLlDl{{V7BV*3Qz>Fx89O4Ryw;|0M1$8TSMM{(x)L#MbxwZ50e zQD1IYjK*+f@(8-B6FmDYO7U!FmZNGojB|&48W$lx6&Owd5X4XE*^`Rb! zThWHs7T>wH>yPV%cr4C>B}?W)x0(148XW2$w^V1EPQH=dlUhJolel9bv+4uC-sM_7 z_WHV!icWW9SjuWIRfy%lz}5CScEJSOF2T==@4W=E`L#GMHhw9Ctrdf}LBe<&PmQs3 z1RyBXDsVf$f2R#F#t5{gC-DyzG$oNtmoc|){(UHvgdZelb8k+Pjx9ki(6_lcGv=ZQ)VvT}31`1XT`NT^X?@|_i(^J7xc_)@PJy@qj?KyrL zk1pM?KaPqaQ9vXf#EYCBuC^Fdwc zNnHfoSSZ|!CT-Uq@k4=deQ3+!5F~OJa*|6m;9=3|BCLqieQP{x+@#M>FgsEbI6qkN z9gcin?NFe`NG6J`mZ7JI z4kP@$WOd%#YGK~6-TF1KQ!rRAH#RMr=w=%bPO?xEYYsF~X;NnE%_B{dKc($?{-fvg% z8?b4oRnbpV=xPG-PsWm-K_T)w=mqr>CsV_eEiR#*Cs+WDlK5)9F5s(mp8y`Kt6(}? zh9&Hd`sNPX?K_%d*pqlMQ_4CBHZd{Uu4dtuXM%8Wb!4=cYR{*spseADJU%hE2<Nf44x5v0*6alJ#=)}Cud`3~h0a(u)s!kZiA5CZSLLQ(as#(8nGh%&dhMAfGm=I) zp<)bLgb7zs>|R=tZ`(IMc^a*!6sRz`uH=vdsHeKm5oADg3wrOPNAbBp*JZBo%fd;h z$3(?IVG!^%v{8e{x~p*`m_H;c7kX9fCz@O7AAno{90me*&l`6?76vD_SFh67yXbYs z3j)fb&#rByEfAto*Hg8!4y}=C`eXt7Npvvg!KOzDbq=;|#kYaI=yVFNIRWTXvY%nM zu^Qm49Kc=ga_M)jjpg&6x72IW?DDLNg2h7x^pTe*T&al)_(yNFAc=8_O@I%4zJ-3q zkD-se>ptFi-J{vB82gFmH>wr*40ErmSX7A{Q(DKuocb>{uQBrZydN1~aUl9~GIIrx zC5>V;)&@~hAQ4Y_7++|em%PxaEKP1~jw6^fNnAr^-yU`lyLcd;K01i2VBe9S=u8d# zG%qXeF<*O9VNom^La@nQ*4 z;M#B_A6QMf^rQ|0)B(AT(IYo!&9T=$e(Z9k;Nr4Lam82ukz6pZ1nY5Oj{;|cffl9$ zYMEoc?~2)pP{eo%73G5@%~_#n|I$5z_hyUe*qniap|!$Qzv&Iohl?pn%~>ev2G@up zA7hXXlHy_H2~kJ$>>mD_IC=ZvN9IdZdIGic;^pv$&;B#R2{DK~p?Dv1Jyc-SQZy;( zTxtrxoyY_fQVCUNl%{eFno6F&A^HPQM1kC?$^CMF$3)`Ns$xmkD4`wp$WXD?YnRFk zZ}vQXCzGhM1GGmPnD%Jvc?ezh&l@kuSW1O^?TLfviZr{FG7Khg-yN(%Y(>`b?&NMN zZX5@&w+}SD(Ai?`qDEmEMDdmZz7D6$h-SQVgyOe2tunM~%;<%-xSW%`rCzupgq|~K zz&o1j(!6oR(+?6w&fYmV59&g+e7|flTK)_{QrJ`iG##^S%>Bi_6nho{E-ou)Vk~GevQ}`n z-1-qR%HFQW$qqEG$j6Mz`t#*~OLzA6A!n~pHlkK27 zYQmB9p+}PDPZsH{^y{(D$T~27e*hq{D3fMY>Pv8A&=6YO{;1yq(dO!rFY?RI0oEa@ zoMaw?vdkRxBD%7pFu6&ZuJ6}uuNNNT+#aP_i~H`o(TDi1R$A%xmceQyghcd2a*c2{ zgwGO)!^$3ql>)w@642VX3i(ZC@HJ!^o8BTl95s)uBG(Ugyb?j*OGeKFPg5_+16<`} zJ6vLoa4@OJsUU#KUSl7AO-nnXzlN7!k_ivbe}yuYr?{UM9*rB96%d}0SW`?3 zUmX*xaBjVMwd6a*G66OkrS0l63}18y%!CGB)5w!jS(Ctx4DDgLV|U^hN<~D`P>S~k zY}~qjz8Hu?I3XV-(@j_pJI%Nsb03Q+K#j_QJA6fg0h!nbB~$-3j(ESGSoYqzruC0H{AwZ4)8>xO#Czv;yEhuF=*j* zm7}X)pz$Mw}XSuLS?gFQzQzTTTT*CTQ1_ zL-kY&r{su=ejMwE1jr<1N@|g}N>c`1Y*;W7^JEiQ+M!mxw<@das+a*|=VC9wlE6xq zYPt-b<>0U*KvQfcpl&|y^%A?um#A2Q?+<`F$3PWYE;iYArpU$|j0>ZgqXDb45_n!T z`ZEQ?HG{wfm;fqQovF`4}Dc^g+=tF9h_nTAv541W0<#b=#xtyT;3h-ZflA| zGayI|!3_5Jx z1c1n!6@75rv5lx>#?|(g?C{8YN$9Nb!ko8?gk=dQRARNqk6+Kj7vZ*QNk=TmJu}gR z2~&?S+|BGT80_4_(1WWbGwgZ&mIog&$FY*SWL6#3W>;$halcesnu{u70TKA|gR>=n z<}Bkoa`tbD_Yqp3oY6qq~8Qzs7Fc6-jpH+E?px1XSi z3GxiWf&659i;1WSWD@`Bb)esA$8N^)Eru!0ny_wk4(ak|Xb#bKin(qO6fOpGlSv!3 zW8_d5N{D#n2I;#vAhTmsX>Qwsr+YYn;vLwG1L}JdzVZ?Y6JWC+5dK9Gf!%=7{a&&o zbzIngl4ZX8PW8o{$YI*L1O}_sOKJ0d#$tCQLO>aXA3RKh zlv_@l75)j5pra#3;^u%J2RtCnv+my^`#xgrH&Y+7c^Zb611fRn+OL+afoCL$L^n_q-gZ(Gy3x={wkPk#@ErLv5{fFU!53z=si3vj zHNX`R@P2l!@I&x!#62jk+vF+B7(x$okDH9GHFZMsV$l|z5K4m+sHYaEU7$3d-Ka2w zRIWrJ9mTGLC5hSJ$MLb9FPtgI*{v7t3qm4{+S2f^>yEDc-1aK@v2`2$63Aanx{FMW zo=H?2SRdd5(f?qWKpdKFq&v;5pGgktL^P*Fw(Dc61sp`*)E9xf%U{m1-#J2~`t9$2 zQ*u;g8*St#45s6bP|L%XQeZL53dL_IMY4D23AR9&q!#~7ld3RW6C@+3;>M*-_d_`` zr$sXyX2+Av!l{c1+6Lu-I(5+3S?l{UT@LFzMKgk2ei3Ag4!^f?)s-2lO_5Ldojdcz ztmqy*B{%qJwFke{9)L-&O@s701Oivz1+S&Z)Bc@>GUAyrCY19hD}tjBF;qqMaX_-KvNIzvb$2{m0aL}HE*24Dis&>rS-#3`s}CP(W# zZLx_V5jL)G^mFr{VT@SFIG~V7V>qezgn!e-#C%5B=2?`sHjj=pfD458gD$D}?rA;= z#0M3GwBM}@K^QqTPTs`2g^SIYJ=MtiH2gSEr-Uuwr96E+a$x(;H7Q<@9|q9W$Rb_L zeUIH0fr#Ois7vo7CG_V7I#d67w-;Djt5tvrwW{tH{1Pb%N4V za#Hpqd+ZoGYZRrkF@wn-w9yxrPLOA=HNJBn23F;ovsxQ(MLFRb{qH^@@q7Y-dXHE&4*@@HR3yo zw^Ahi$LRYBYmVS3w2Yyc9oY}N*h)XvTighnDOf41Xm; z-UTm_USm{#h;D>GPmU4bLV=fEUnP7Rt9{QOn4}WIZ4-WIYH8_HJ09Hif0oiJ+pU3_ zr*uYthn2ZkTFNtlh3jp*k}g1!YOU=o9ne_(UCMk(X;_^ZgOsk*vjppU}{rRJQt z3&(m(gs95&%V+&&w+dFsOJi>}q2Aujpt5sZG-W?Cc&Sgng+SR~_&}hq5h7zUCD>cE3SL+;Wozkn?R1X4^cD&JI_3^QFa?)U>p$V*Bz}Fk5i7%a1A(5HXLs z9S}H;sjpgeV3427oA}V(_+e&zCybT&YDP~0bg{L*nzO~e=pCpUDq{Wi8J%+)N!V*I4 zDuPi??w3a3ieowWrfX^)dy0fR#arLWz2NMamrZl?4_IFa+&mejzybnd8S3juDwV>wO;4p=Qr6) z6v8pe90!&ro5HaHxp=gap*`eI(JAJn^Ig}vnPfj*okc%jI2%jUS@}?Xs`Y*S1AuvE<)teo+2?9euSG(?|W`6gAXR)r?SxV|Igw7 z9c(h%!IjSA+(z0F>ynN!DPA7p9slyclG)A%*X;cQf$h{>6fYL?QRegVwrzVGCLVXU zj68em**4aum zGobX49fpjrXugUx2u2uWiQA0fEZ_!WO~icY6bvY0nh;8El$BY=77VK(B+YI3CzpR! z#7i|FJ@pHzu6!){<~NNK6@J8mcE*@6{}PhnAV@3HNHKVt+H4nGl(Jo=Av2R%Ds`|8 ztEW?~d9z1jO68hZRn_VqP%O0n$usNxgj%wfh=YbiRZH5gM_v%=i>`dwN-0>R!bt$}Y;?(}ndhhtwUFwBYJno!=vU1f! zU`j?=Ad@SmAyae!Vy)Tn5rld>Vy=)%&SpOI=pLZ)^e&Y_1rRbZH8?eA`U?<0ae(9< zDvyDc&r<(H(ZQT-0yn1%#|w|*BY2-j4DA(k*ktY zA?6RDHxt!pp{W@6sYNE`z(ReoCfP{8zrZWaDWX&#mjZWhs7_lDIz-{I2O$avsuG4R zlS5Yx7AkT5m_#S|_8`HGUH1FGi;M<4_eNw6Ej$M!YD->O*%~?vaur0;^o)h?DkerZ z)qy3eV4SIGC(BW9>&brVo}gi}?~D4XoMheWMt`~heCe|nSs)ZZ!wlh}QscnwAZx}I zFBe{I%jd8H>}i{pb)z(XYEudCl>Y%kjN{Q#df7onQ|rTd&M7LFumZ&pg~=7MRiA@u z&pvsy=bouI2g*>*Or42({g6L$^RCoRSDu~KOkL;~U%zH0QUcP|&Jr_eU@z^b5(R z83jhrvJ(L`;-UX`eEgQQG0c&DAK$8IMiD^yAo7dM-GpLJc{u$P#RO%=R-TMgK1mn5 z&=eeQci)3QFqj;^Z%c9{UO{1;9$)t)yY3VBY@(avkCmN}e25G}(J~C`H;>FSqweKrzOs+Pg-dKJt{UzI zstpVDNN9y}(98D$(U#5=PULc7MaGBr#>$Asl<2n*S{p0-^G; z@tU7U)=uXl9s_wu@h+`!AH5^&J8n_Gk3}$T`Hv}&gC9G7P-FvMK3zEvrt$9E?vn(D z{9B2XN1g$sVA-`998Fo}n5~cl%f_yg_im?cZ9H!rrifH069ecwm=x0@Vc0%J2Q=%5 zs5X~kCo)2WBlx|*X^N!-?5@A>mOZ-%e?QE;im2W-KL_;m_2$4kv5H-Mkf^YJXD$%A z+j>>ycW;d>cQ3pJz2C8?&C#xQ*n6j#cK#h3QsORneh3+ZhByA5hORauTr=pT;Jm{D z)$l{qJdl zR8`C8C;=!DWXFt#-0G&~<%#+PV}tK5w!1%z_7T`+F78P zzn|4kuG_4VRDdQ;eE?=juS|dpCplU8ACU=IAMpK27&Fx<pt@x6kxbfrs4aHr*hW28~|$3kxLVU z6tbFXykW)b!MpEPsl}nk*rzi>(Au6CFu<<-7=fg|@@P>gK{-Z#wnM|Fj-!*W)PIX#Bha6e$ttpi5h()$%4(or z+Wy)1{Q;QA{?)qI4*u1#)W)*y)@A;t-$(CcqQebog*)c4n%aU%V0Irf3yZq>4UpMK z;_vnn2h$X9Ei4yH#rw);N)6cDq2#C%ZIY6$s=O<`q4yf`Z)uJU0iUnT?#RJp<(MZ`Rl1|-k0vDTyant~)@ zQD$|;=H1yM5oz|HfuD@JKLV#GS6@Cznl2xTRVw^CO%nZJK`a~>J;jM5Qc_K9t+Ntd zLXj=!7+)xP=m)h0&`4Cy5e=gXpTpg;XL@$Lg~2{#!@McUWUpxZGUAbzodJQsm%Vv~ z$}>MAtu`q?;TJcU!Q=+b__cW1uM}}0WdhYY-~ZO$YeuP$dilLbDr$o%_8qOtcVx8* z_|?fcW0l{00R|(Z^b=C6nWP#F^y~&Z9}}HO?(!W{XJ0aH2Pd{BD(^~a3ayOla%w)- zn6Vh%?;HUr`ub=Z!lCYA06k~`30y;x!@`LJqeML;>i$@~6kL<;@+p=KLk_CRZk_?M zIM)Qm->J0lW9Gih{>fvT>J z8j{_UZnzZJPr_)GS_^Qg#BQ?|g?v2-p^F&d<`dfnW-4t_A^D zT!ZV+De-;zQh*YZj6PR}51m03Ciz8RnTU%NTz(lvvc3>Gg%EKmOq8xIx9;j6Q2W;) zZZRz9!&<`os<_r|SJf#~qOU%UM`$%UKm2wDN>xKnBT~aL5SVOPQ*pR-igA&;_=I){ ze|$Imp*-Yp0E38YQ?m&dtSbNf-gHm9MtiNAdEBe&4?v;+%dGEjJF937&oMlhO>y*8 zMB0+aUyDZ=&2&^oBp`~?iXR`m$AAYYstxTy=6?Vq%=+A1QRkVzm&fUY2!E2-43}$#1GtES zbe1xPzBY>gIQ25}leR(5G)Y@+vn?V*Vv|4i2Znn`ka7JCw~dvi#Z zzdhH#x`<{FUMp%TBjA(oL;fhtfY198*{z#%kAZ26+K7Y?A@pNUmwkQ9zkW{UhkZeILFrgvS-EbhOCY zW2V2;YbIr6yE2HdH`1wVWv(NubmSIc0C~4&kk}d&Qu?UHP{kw}cmP>pm`?O1uQ6Xk zw(4=+vHrA8gcXt^^g%g_h=p2x!o5RmUV1ig+702f^bpk2Mqz14>ehddo7cfqrlLdH(=A-%q`Jk)%bAvfA2_kSL+XQtTN-D z1|u4IV@#=?szSEKl(Et(@Db1)BqgK8jHkd}gYWHm+Uzppmi9U>zUaqijKyNwI1rYB zNDxe~o`_2&#HbKxx&N+%giA$^3D@88?~(+dvLw~1es@y&|IQO`{+~Sph>1ms%nEsx zxtv&GHA3qVk$6E>SK6I-d8CqRvqk?gq$n@usRvP!bZY~WMZ>P#j@CIq|D${@J6^xS#sQoV*Z@1MR=j*7l z#>lf8aDP#+FZe9n?XE%orlvntTA<^5&NJ^MgfTAr8h%I4zm!Ga_KY!14@e;-;=&LGY;c zqXTn}b>(;(OmaGi7s|=Q-4_12sM0>V#G$gAreGS_j2+!puPeI7ZEI&J;pb4^MRVzC z%T6nNt@W`2JX6T%cZtxdjGG#iKr`9GH*L9=+M8!Ck!jYz;>MD?yt^Zp?QV<|8H?r? zHCa1bXKb6Ih^zV|I&V_1%4L@>v?=TDA=4%Cc_+dfNJMI>IJvJ|iA<_Md)MNZ$otSs zO;b#KFSYKon z(KMM+b%VM=7Jf8!CJ8TLJ^H=!QzDm+8)z-YiEj~>>6K~JEo4VN6eS(nX)py{C(IyhRJlJx?fU#@8r>v8C8;)57}jd?h!iXx_z(?5Z05OcQiRLR4UVS=AmxW zUSp^2XYe(zHh7^-gCqitg6Q{wCki>#-fE+|ZBGN3f=T+K$~gj|4X zRn>@P>ZufYUsnY1%V~X94Fmewc5U+<#1>|ICQz+lE&$2RsAZKR1eiO@tdq}eR5=gt2sk@gJ-*JfIXChp~yGHy@FK>ybtwT zfa?;^!FAgf-td@w)fXNHAK3G|xK&KkMV*xp*T&Hk0}71`^*-02LF>SXdH*D$s|LARI zmeXa{a8vcgrB4k$iNMKa=ikQ#k-4Zx%1b=QT>SIQ;zQ~`{i+0aOmDbKl$1Ev|F%bl zwQ6n>$d0r)53!q8dPth3d1r_Nm+0ssy^zrviP643*Sk)q<3BphNjXgg%{d~^lF*BN zYZ3g?fJ?o)yCI_sP>o7Aj~wh~)H&2PsGuj(WcWBq3~wFxk~FYY(Vl#NpGVTf&3M)@k*Z|G}y+KCqsxq!^ zi$shv4^X~KzHZHwZ#q|k4!kI+aw782c+P!pgm!>qn6PH#(n!Xl)m@NlTzxvIbx&oa z>uRJKtjC2px$=W}3WB`xLvS{0$7<#W2W4rWTJTuI(N*<}I&StVWxN)ZyK^xQk#J5X zb;a)lwiXS@FK5}=4v1WqLPMh0FAw5lEbI^n~&pLzMLn^2`G?hYUBDmw)M zr>AANDI^36KH0AL1K_zHlg~6RujKsg$$U#K%i+*N*T13|CXqupqn5SNr%t6zSyudF zCf~(2_$EI<2U@|ntw2IJq`|#zoE;|#nuTa}^lV&I2#Dpx>^K9FMh#GY`JXFx_gM^* z$0-y(XBuAEt5gsL;6J}td|Hu%PB@o~#_F(wd6r(&`#FwoqF&Q`3relHxn6&0fP$*1 zc{o$Jj1@=@laUDBikYX~wLpNZanTokl-a(JXC-cDanh8_O^@9`^Y$>kf!P@A$k-SB z=8CwfYxKj6E{b`!uiK^?05`Ybt1vu*A%rU5;B!evfW)dDL;Yol2nlpBdn*(^ct`O5>ugW3xSA#$J>={iacMVM~fqX0wV>{r>+0eX9-4 zfH@e%@pLS*SK#M7IgO5Ildl9EfZ*KEq*vUoaFL8UhDoW=xqOS(3|Zq+2=1%psTv_X ztb?11RgPr<;@_qkI8Yp)FiUCX*%R;#J{OiKw@p|p$n0J%pVka26%`43n4x)u1tkb@ z3#(9Gwo5ao(#yNaw6GU?A7h{bz^h)IW-`g}YgqD`=PMYGwK-BrwqG1bBZ12(sy49Q zOKw)}QY-k2CwCR|=;u11Ju3kTeV6|GCtgt@m;EJG;Tc@Xbi$Igi~VAEnjM~;DA`7( z(@;fKgq}3%f+XEeke+?(*Ia>f;X+pOq*2=+eWJ*BkQ~*jx0K@w5Em4*)9yednJT%f z9Fs?voy70KKs==?xpsFNcq>k^5`1Wjxx7n!J7e?uWk|2wVF;a)bBJm^fxYp@{;$v} zv3X?Hb2};L0md*gjy;a6b$V0+We(r&W{26?##DX$;whyxxY3pKFvaCuV1OwCU;B~@ zMaGVF)+c%KQD@k+1s`bfeW~nXV25dc-7QKBgROG^`V~Iweu91DaFnqTy0BHO>xCfQ z8fneNY9`WqY@5MTP&8oW?9r5-h-Pc~0H&I~AWKgl!A2_;o-4tH6I$}t045KCLdXo? z3WSv^Wxm8&uPT@-UG*tp`{;KA6fE(stCB=c+FOlo z0hprnVt>JsymnbU7HD%|KJ&L++mZxvi-INhjI~$Q34P9NP@}x{T3gYBS`+QZIAs)u z1{3>F=!`3eT~aayo@KBaw(pbZANW*3{3QFjoQ2jPdz&@Oth0$&4&FDQ@gQkm@)uZp z-5|rIL%eKn${#SYM&82PTR2joN@U=6MbMP6b`L`k0y<$QaXeLTL&!m#Xayc41p<>V z{{-X8ar^J!Yi9ju3?*q|Ht=950TdM#as9q9WRh-V^Wr#W4|xx8%58NET$%c>CfwvR z!=v*xK(`Ke+@lFJ(56Ur_m(PNkFXEK!q1BLTDq|5V~9%%C!a^gNr1r>A=ytpy_U-@{j-GCV&Y@Re_w)vD@aexj!<=}Vnr(yEy&)5I zq1alj)+atN+^8t5d$E$+A3I>4WqJ`t?Pn!RYx5UGfv=VGq1(UcA`U^VWHsSCPlkok z17{*|q~7Vo&}I#D4@!Rk!{7RL^j2~AbHB^MH`ItR#E8AMkCPLPZ&nMVEXxWP1|dqC zp+7AAv(bDT`fH-)KQW+>#F=@dstj;XCEi+X@w8=leLQqIKg`Fv0U|!V3<&8m#U&ty z2o%0fSPQNz+)Dzmc7whX~P z)x|nv%Iu{S}nJIFl@3=j4R_OwYu2xenrqq zHD|q5t{2c#90n>;ksb#~G#e(vGTl{OgI75$P{BsggQ>8c-TYu1aKA;GZ=|@pDvBJi zT^dg}tXo*_=N^^)f8^QC3uL6MQubiPPNGw4T9NJ(leS*xP6#CyQK90eayHchle4Zv6+ z(czG9X+i;9-is9&flV_r{p>pHrnqk4PLb~P-KzQaJcpDozr>jY9M5&oKTG@N{3Z3T z*}GxRL`OZS1YL+X^orszGDA{g=$bygS38s>d`^vJ4&*CUyI=8_F*2XN-|DZ>wEBLP zid`^_5)wqFKrm*$fApPEXr$mc_$p%nVFEzJ)R=Bz$-gGK1eJE%{o-C;>A~R`K5=|^ z$M`wWJ|OsRxRv0FlSh@X<%W^EHF4}%+pL6u)28n+xe-Og z@eB1x5C`PrJ6Em*uE~qOafg|HWQM|KHufFH*N;ig%BV5Fa*y5f;q6NJ4!n z{-wT{>dMyrV}Qg#gmRs+PpWy&yKt>~6{n0kR)m8F$Z89}fS|ew*w8DFlrbiiJ(NPm7D5X~vYTOwpWRF`*62=k&+p!Q?!D*Szwf#Ke$MBd&v~9dKA-3F zyx!0AU;zmlAA?)@&?PHcm=pFO<;!&WyP6od{X%(zsD`dS#Xt=~r(96#)Ems03#o0n zi)V5Q<-LPphnv9B&&8ZBc*ePX3F~(st`6GQNkcY|+CS1lgD@089x!vZ_)L*izMSTj z$T3AuQpYa%z-=mBt4gR?dG%{VqW9)$ulSE1Ahn5zx(&BA&XWxb(8Gdht3@piY%mq- zh|hg`gjFSS66Mi2nsl&m7x%J zHEd?g6+FxBj1Rt&5ad}FuWv7I$9W0m)KGR>P|lg^MNG=7x1+o5OKQ|+cI`PHjmi1* z=zMO$019RV56G_hdgy90hAGaicYJEiW>?Ey!99k-POG1wj^M3lFiU$Mv+!+Fq3K{@ zH(yEq_22%(g&!gvrHQ_3nUB6YpUewVCq$xF#7Ln8ZP1Go4wnj{c^*IV>D&QjM(uXQ z9~`-D13RmV6|-YCa%cMdE%FYy?UXznAl{d+z)*O7Ldb@>K4oxB{E%7EZ(i+2h)<_Q1RP%+oW7 zGi(0%T2h9Z6Ur|vyZyb4laM^$^T}<=Q42&;{@tRD{eGhr^FEpW6Fe17h4-!dyDpg( z>mUp)_cW(wl>|v0-#dFQb$#cNmjc6Zc(2^6tWl`QUfE@;tzXWI*40jm2ku6uIFhK4 z>6`hwD6qwS_EgHkty^H#%d=}=m;6M$O$)r zORdoK+Z%lLYQ^UAZdNMsO2f{*XR>@#m z|16V>yA2EqsiCQEpF%PXtByHCZnuayeJNsHsle%iR1=1y%gY7+JNNUAgWM4XL2sA; z)esRWZ{uPh(YfcL1d4JE&22a{5!^u@T_3k_&k06ND}Iu<2Nuf=VEMi(6(deB-aON_ z#L5-F*K@exUOEuKUhSlQ2lUl5HN+?rLnx)udww{P&kA82o;#2M@#?WDNWel_& z_ew|cOYX1dSek*N%633vm*1Ni)mnuPpbB{pY$*Hp^V?wgJNe&bKQeR-j>Nh&=BY`= zx30vp-`GiAqZR6<&~-|&iQIaKmZg8#;xZo)dQQV|!LSNg=xE&m5w8xb_kTdjPlE&F zyR3Z5tdNlm0o!X%W6C0`4#ZNsz>0jy7%~hoV!G)sP(d=V+AN9Vc}VWht?^a+YV2vI zq_HTC?f@)`b29XYWxxvgffoA}BCl*jIfbhqK&8uGD3qzG6y56l7-YjJE6dTjr+42; zYoufEr;x6obh`eVSrB>~pT)V+Ljf&WeEnlaxYvtJ5ob*ka7-Q3+;3(!RpPY-@Avm! z`F1pE6o0WbhU%C(ST>!l$T)b}+W9d(H%_rlx9*8u(n0gIn*u`sMBn9^8r4ekB1knE zwZo7s+Z=8V7IvK{l+jwK?n-x!Q$(cBX>!)-3Im~3mgoGx{lsP#I zUR}0sW@nMRw`DZIEZHA4JpbRBvXA^cPx9ih@ZI2$EawR4$uTx! z&j46p!70b^1oOnA`{`(YGATBB{ilFsFoDr;!BcDCf-&9Pwwy;SYA12V;E3PvsI-#n zaCr4J>ZPdqp~&9UEaE`=Q(lqPi!4$Tt=(;9ZGZRZ%5@s*EN+hUQf-4z`>n2X{*rk@wb_Ini`bf|M zL(Y<+N~8hs9xA~!LI`$LF`Tg;Syjr1owhBI*Flvd`~;DUBV$H49;B8mu&-<&+~2;c z|H^D;JY)=dLgop^M)v+HFb8A#)C6slS{_YHD3s)D&pZ~)?+XM#M(sHaIzl3RAmZ&% zx{~CF?8hg9{4ixJGmw%hYWvLKT@@+isJm3P2oKSDJ36g8ViUr1xMP;f{I7fNfKCrN zdabh7g(?zjn)eJuUBCr;5Je-uN-E_YT&Q2ll9irq7~_}OBzH&dm}l1q^D6xADPoMz z3zHJG$MZ}cb);7<(kFXN4JSY|OyuPf>Qh9-sUEY6+3PI+BQLwA0=1^wSeH35N8?`g z7^k+*Y7k9I1;SN=rjOV4Zdv2U6Vpk$V)78UoYBM&*8Z8Gd*~Nymsv7dY?q4aG@Wv& z-}=b22yZhqBzRLte##Jl_RgD1i?{0CV%vVj9&Bb~1nsy4Dea#! zSbzWdMD(IvNh^Y~i z-} command) throws Exception { + Process p = new ProcessBuilder(command).start(); + try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { + Future err = pool.submit(() -> new String(p.getErrorStream().readAllBytes())); + String out = new String(p.getInputStream().readAllBytes()); + return new Result(p.waitFor(), out, err.get()); + } +} +``` + +The calling thread reads stdout, a virtual thread reads stderr, and `waitFor` comes last. On the same ten-minute file, printing the start of the summary FFmpeg writes at the end: + +``` +$ java Drained.java media/long.mp4 +exit 0 after 2.6 s, 740381 bytes of log +Summary: + + Integrated loudness: + I: -21.8 LUFS + Threshold: -31.8 LUFS +``` + +## ffprobe JSON into records + +`ffprobe -print_format json -show_format -show_streams` describes a file as nested JSON. The JDK still has no JSON parser (`java --list-modules` on JDK 27 lists no JSON module), so this is where I take a dependency. Jackson 3 maps ffprobe's output onto records without any annotations, given a snake_case naming strategy: + +```java +///usr/bin/env jbang "$0" "$@" ; exit $? +//JAVA 21+ +//DEPS tools.jackson.core:jackson-databind:3.2.3 + +import java.util.List; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.PropertyNamingStrategies; +import tools.jackson.databind.json.JsonMapper; + +public class Probe { + record MediaInfo(List streams, Format format) {} + + record Format(String formatName, double duration, long bitRate) {} + + record Stream(int index, String codecType, String codecName, + Integer width, Integer height, String avgFrameRate, + Integer sampleRate, Integer channels) { + double fps() { + String[] f = avgFrameRate.split("/"); + return Double.parseDouble(f[0]) / Double.parseDouble(f[1]); + } + } + + static final JsonMapper JSON = JsonMapper.builder() + .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + static MediaInfo probe(String file) throws Exception { + Process p = new ProcessBuilder("ffprobe", "-v", "error", "-print_format", "json", + "-show_format", "-show_streams", file) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); + MediaInfo info = JSON.readValue(p.getInputStream(), MediaInfo.class); + if (p.waitFor() != 0) throw new IllegalStateException("ffprobe failed: " + file); + return info; + } +``` + +The `main` method prints what it found. `media/ntsc.mov` is five seconds of 1080p at 30000/1001 fps with stereo 44.1 kHz audio: + +``` +$ jbang Probe.java media/short.mp4 media/ntsc.mov +media/short.mp4: mov,mp4,m4a,3gp,3g2,mj2, 10.000 s, 2941 kb/s + #0 video h264 1280x720 30.000 fps + #1 audio aac 48000 Hz, 1 ch +media/ntsc.mov: mov,mp4,m4a,3gp,3g2,mj2, 5.000 s, 5812 kb/s + #0 video h264 1920x1080 29.970 fps + #1 audio aac 44100 Hz, 2 ch +``` + +Three things in that JSON are worth knowing before you write the records: + +- Many numbers arrive as strings: `"duration": "10.000000"`, `"sample_rate": "48000"`, `"bit_rate": "2779526"`. Jackson coerces them into the `double`, `Integer` and `long` the records declare. +- Some fields exist only on some streams. Declared as `int width`, an audio stream comes back with width 0 and no error, so the stream-specific fields are `Integer` and read as `null` when they're missing. +- Frame rates are fractions. `avg_frame_rate` is `"30000/1001"` for NTSC material and `"0/0"` for audio, so parse it instead of rounding it into an `int`. + +For a single value, skip JSON altogether: `ffprobe -v error -show_entries format=duration -of csv=p=0 media/minute.mp4` prints `60.000000`, and the next example gets its duration that way. + +## Live progress from `-progress pipe:1` + +`-progress pipe:1` makes FFmpeg write machine-readable progress to stdout: blocks of `key=value` lines, each ending in `progress=continue`, or `progress=end` for the last one. Add `-nostats` and `-loglevel error`, and stderr carries nothing but errors, so it can go straight to the JVM's stderr: + +```java +record Snapshot(long frame, double seconds, String speed, boolean done) { + // out_time_us is "N/A" before the first packet and again while the encoder + // flushes at the end, so N/A keeps the last real value. + static Snapshot of(Map kv, double lastSeconds) { + String us = kv.get("out_time_us"); + return new Snapshot(Long.parseLong(kv.get("frame")), + us.equals("N/A") ? lastSeconds : Long.parseLong(us) / 1e6, + kv.get("speed"), kv.get("progress").equals("end")); + } +} + +static int encode(String in, String out, Consumer listener) throws Exception { + Process p = new ProcessBuilder("ffmpeg", "-hide_banner", "-nostdin", "-y", + "-loglevel", "error", "-i", in, + "-c:v", "libx264", "-preset", "medium", "-c:a", "aac", + "-progress", "pipe:1", "-nostats", out) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); + Map block = new HashMap<>(); + double seconds = 0; + try (BufferedReader stdout = p.inputReader()) { + for (String line; (line = stdout.readLine()) != null; ) { + int eq = line.indexOf('='); + block.put(line.substring(0, eq), line.substring(eq + 1).trim()); + if (line.startsWith("progress=")) { + Snapshot snapshot = Snapshot.of(block, seconds); + seconds = snapshot.seconds(); + listener.accept(snapshot); + block.clear(); + } + } + } + return p.waitFor(); +} +``` + +With the duration from ffprobe, `main` turns each snapshot into a percentage: + +```java +double total = duration(args[0]); +int exit = encode(args[0], args[1], s -> System.out.printf( + "%5.1f%% frame %5d speed %s%s%n", 100 * s.seconds() / total, + s.frame(), s.speed(), s.done() ? " done" : "")); +``` + +``` +$ java Progress.java media/minute.mp4 out/minute-x264.mp4 + 10.0% frame 88 speed 11.9x + 17.1% frame 226 speed 10.2x + 24.7% frame 353 speed 9.77x +... + 98.0% frame 1679 speed 8.33x +100.0% frame 1800 speed 8.1x done +exit 0 +``` + +Reading the raw feed turned up four details: + +- `out_time_ms` is not in milliseconds. It holds the same microsecond value as `out_time_us`: `out_time_us=4501333` and `out_time_ms=4501333` in the same block. Use `out_time_us`. +- The times can be `N/A`. Before the first packet is written, with a slow preset and a short `-stats_period`, the first blocks read `frame=0` and `out_time_us=N/A`. They go `N/A` again near the end: in one run the blocks at frames 1733, 1768 and 1787 of 1800 had no time at all. My first version mapped `N/A` to zero, and the progress line dropped to 0.0% just before finishing. Keeping the last real value fixes that. +- With audio in the output, `out_time` runs ahead of the video. Copying the audio stream, the first block said `frame=34` next to `out_time_us=4501333`: 1.1 seconds of video against 4.5 seconds of output time. The run above shows it too, at 10% after 88 frames. libx264 is still holding frames in its lookahead while audio packets are already being written. That's fine for a percentage, but don't use it to count finished frames. +- A block arrives every half second by default. `-stats_period` changes that. + +## Stopping an encode + +To compare the ways of stopping FFmpeg, I started the one-minute encode, stopped it after three seconds, and asked ffprobe how many video frames it could read back from what was left on disk. The core of the test: + +```java +static void stop(Process p, Stop how) throws Exception { + switch (how) { + case QUIT_KEY -> { // what pressing q in a terminal does + p.getOutputStream().write('q'); + p.getOutputStream().flush(); + } + case DESTROY -> p.destroy(); // SIGTERM on macOS and Linux + case DESTROY_FORCIBLY -> p.destroyForcibly(); // SIGKILL + } +} +``` + +On JDK 21: + +| Stopped with | Exit code | MP4 left behind | MKV left behind | +|---|---|---|---| +| `q` written to stdin | 0 | 749 frames | 426 frames | +| `destroy()` | 255 | 776 frames | 330 frames | +| `destroyForcibly()` | 137 | unreadable: "moov atom not found" | 243 frames | + +JDK 27 gave the same exit codes and the same unreadable MP4. Only the frame counts differ, because they depend on how far each encode got in three seconds. + +In the JDK's Unix implementation, which macOS and Linux share, `destroy()` sends SIGTERM and `destroyForcibly()` sends SIGKILL, and `supportsNormalTermination()` returned `true` on both JDKs. FFmpeg treats SIGTERM as a request to stop: it logs `Exiting normally, received signal 15.`, writes the MP4 index and exits with 255. SIGKILL can't be handled, so the `moov` atom never gets written and none of the video already on disk can be opened. The exit code 137 is 128 + 9, the way the JDK reports a process that died from signal 9. + +Matroska survives the hard kill because it doesn't depend on an index written at the end. ffprobe warns `File ended prematurely` and still reads the frames, and `ffmpeg -i killed.mkv -c copy fixed.mp4` turned that file into a normal MP4 with all 243 frames. If your encodes can be killed without warning, by a container stop timeout or the OOM killer, write MKV and remux once the job has succeeded. + +Writing `q` to stdin is what pressing q in a terminal does. It doesn't involve signals at all, which makes it the option to reach for if `supportsNormalTermination()` returns `false` on your platform. It's also the one reason to leave out the `-nostdin` flag the other examples pass. + +For timeouts I use both signals: ask first, then insist. + +```java +static int waitOrStop(Process p, Duration limit) throws InterruptedException { + if (p.waitFor(limit.toMillis(), TimeUnit.MILLISECONDS)) return p.exitValue(); + p.destroy(); // SIGTERM: ffmpeg still finalizes the file + if (!p.waitFor(10, TimeUnit.SECONDS)) p.destroyForcibly(); + return p.waitFor(); +} +``` + +If you're on JDK 24 or later, `waitFor(Duration)` saves the conversion. + +JDK 26 added something that is easy to misread: `Process` is now `Closeable`. Its `close()` is not a cancel. It closes the three pipes and then waits for the process to exit. I wrapped a running encode in try-with-resources on JDK 27, and leaving the block took 10.1 seconds: FFmpeg kept going without its pipes and wrote the whole 60-second file, exit 0. + +## Several encodes at once + +Virtual threads make one blocking task per file easy to write, and a `Semaphore` decides how many FFmpeg processes actually run: + +```java +static double runAll(List inputs, int limit) throws Exception { + var slots = new Semaphore(limit); + long start = System.nanoTime(); + try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { + List> jobs = new ArrayList<>(); + for (int i = 0; i < inputs.size(); i++) { + String in = inputs.get(i), out = "out/batch-" + i + ".mp4"; + jobs.add(pool.submit(() -> { + slots.acquire(); + try { + return encode(in, out); + } finally { + slots.release(); + } + })); + } + for (var job : jobs) { + if (job.get() != 0) throw new IllegalStateException("an encode failed"); + } + } + return (System.nanoTime() - start) / 1e9; +} +``` + +`encode()` here is a plain libx264 encode with its log redirected to a file and `waitOrStop` around it. `java Batch.java 1,2,4,8 media/clip{1..8}.mp4` runs the same eight 20-second 1080p clips with each limit in turn. The machine was running other jobs that I couldn't stop, so the numbers moved between runs. Three of them: + +| At a time | JDK 21 | JDK 27 | JDK 27, machine busier | +|---|---|---|---| +| 1 | 19.2 s | 15.9 s | 28.0 s | +| 2 | 14.8 s | 15.2 s | 21.7 s | +| 4 | 13.9 s | 15.1 s | 16.3 s | +| 8 | 13.9 s | 14.8 s | 15.9 s | + +The eight clips never took much less than 14 seconds, however many ran at once. On the quieter runs, two at a time was already within a second of that. On the busiest one, four and eight beat two, which I read as more processes grabbing a bigger share of a contended CPU rather than the encodes getting faster. + +The encoder explains it. A single libx264 process already spreads across most of the machine: under `/usr/bin/time`, one of these encodes used 21 seconds of CPU time in 2.7 seconds and peaked at about 1 GB of resident memory. Eight at once means about 8 GB of memory for no extra speed. Size the semaphore for the encoder, not for how many threads you could start. + +The Java side isn't free either. Every running `Process` has a platform thread of its own, named "process reaper", and a virtual thread blocked reading a process pipe keeps its carrier thread while the scheduler adds carriers to compensate. `Carriers.java` starts 100 processes running `sleep 3`, one virtual thread each, and counts threads while they run. The threads only call `waitFor` in the first run and read stdout in the second: + +``` +21 100 jobs (wait): 18 carrier threads, 100 process reaper threads, 124 platform threads total +21 100 jobs (read): 101 carrier threads, 100 process reaper threads, 207 platform threads total +``` + +JDK 27 printed the same counts. At the scale of FFmpeg jobs, a couple of hundred platform threads is nothing to worry about. It just means virtual threads make this code simpler to write, not cheaper to run. + +## Two-pass loudness normalization + +We deliver YouTube masters at -14 LUFS, and FFmpeg's `loudnorm` filter gets there in two passes: measure, then apply the measured values with `linear=true`, so the whole file gets one gain change instead of dynamic compression. This uses the `run()` helper from earlier and the same `JsonMapper` setup as `Probe`: + +```java +// loudnorm prints every number as a JSON string; keeping them as strings means +// they go back into the second pass exactly as ffmpeg wrote them. +record Stats(String inputI, String inputTp, String inputLra, String inputThresh, + String outputI, String outputTp, String normalizationType, String targetOffset) {} + +static final String TARGET = "I=-14:TP=-1:LRA=11"; + +// One ffmpeg run with a loudnorm filter; the stats are the last {...} in its log. +static Stats loudnorm(String in, String options, List output) throws Exception { + var cmd = new ArrayList<>(List.of("ffmpeg", "-hide_banner", "-nostdin", "-y", "-i", in, + "-af", "loudnorm=" + options + ":print_format=json")); + cmd.addAll(output); + Result r = run(cmd); + if (r.exit() != 0) throw new IllegalStateException(r.stderr()); + String log = r.stderr(); + return JSON.readValue(log.substring(log.lastIndexOf('{'), log.lastIndexOf('}') + 1), + Stats.class); +} + +public static void main(String[] args) throws Exception { + String in = args[0], out = args[1]; + Stats first = loudnorm(in, TARGET, List.of("-vn", "-f", "null", "-")); + System.out.printf("pass 1: %s LUFS, true peak %s dBTP, LRA %s LU%n", + first.inputI(), first.inputTp(), first.inputLra()); + + String measured = "%s:measured_I=%s:measured_TP=%s:measured_LRA=%s:measured_thresh=%s" + .formatted(TARGET, first.inputI(), first.inputTp(), first.inputLra(), + first.inputThresh()) + + ":offset=" + first.targetOffset() + ":linear=true"; + Stats second = loudnorm(in, measured, + List.of("-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-ar", "48000", out)); + System.out.printf("pass 2: %s, output %s LUFS, true peak %s dBTP%n", + second.normalizationType(), second.outputI(), second.outputTp()); +} +``` + +The two test files are a 220 Hz tone with a slow tremolo and pink noise with the same tremolo, both generated with `lavfi` like the clips above: + +``` +$ jbang Loudnorm.java media/tone.mp4 out/tone-14.mp4 +pass 1: -30.74 LUFS, true peak -23.63 dBTP, LRA 0.90 LU +pass 2: linear, output -13.97 LUFS, true peak -6.89 dBTP +$ jbang Loudnorm.java media/noise.mp4 out/noise-14.mp4 +pass 1: -38.36 LUFS, true peak -21.41 dBTP, LRA 1.30 LU +pass 2: dynamic, output -14.65 LUFS, true peak -1.00 dBTP +``` + +What to check: + +- `normalization_type` from the second pass. The noise needed 24.4 dB of gain, but its true peak sits 17 dB above its loudness, so one fixed gain would have put the peaks near +3 dBTP. loudnorm switched to dynamic mode without an error. If linear matters to you, check this field rather than trusting the option you passed. +- `-ar 48000`. loudnorm works at 192 kHz, and without `-ar` the output file stays at 192000 Hz. +- The encoder runs after the filter. Measured again with `ebur128=peak=true`, the tone came out at -14.0 LUFS with a true peak of -6.9 dBFS. The noise came out at -14.8 LUFS with a true peak of -0.4 dBFS: the AAC encode pushed the peaks back above the -1 dBTP that loudnorm reported. If a delivery spec's ceiling is strict, leave headroom for the encoder. + +## Summary + +- Never leave a piped stream unread. Redirect what you don't parse, and read the rest on separate threads. +- Put a timeout on every `waitFor`, and stop with `destroy()` or `q` before reaching for `destroyForcibly()`. +- Use `-progress pipe:1` and `out_time_us` for progress, and expect `N/A` at the start and near the end. +- Write MKV when a hard kill is possible, and remux at the end. +- Size the concurrency for the encoder, not for the number of threads Java could start. + +The complete examples: [Deadlock.java](Deadlock.java), [Drained.java](Drained.java), [Probe.java](Probe.java), [Progress.java](Progress.java), [Cancel.java](Cancel.java), [CloseTest.java](CloseTest.java), [Batch.java](Batch.java), [Carriers.java](Carriers.java) and [Loudnorm.java](Loudnorm.java), with [make-media.sh](make-media.sh) to generate the test files. They expect `ffmpeg` and `ffprobe` on the `PATH`, and `CloseTest.java` needs JDK 26 or later. diff --git a/draft/driving-ffmpeg-from-java/make-media.sh b/draft/driving-ffmpeg-from-java/make-media.sh new file mode 100755 index 000000000..e75403efa --- /dev/null +++ b/draft/driving-ffmpeg-from-java/make-media.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Test media for the examples, generated by FFmpeg itself (nothing to download). +set -e +mkdir -p media out +gen() { + ffmpeg -hide_banner -v error -y -f lavfi -i "$1" -f lavfi -i "$2" -t "$3" \ + -c:v libx264 -preset veryfast -pix_fmt yuv420p -c:a aac -b:a 192k -shortest "media/$4" +} +gen testsrc2=size=1280x720:rate=30 sine=frequency=440:sample_rate=48000 10 short.mp4 +gen testsrc2=size=1280x720:rate=30 sine=frequency=440:sample_rate=48000 600 long.mp4 +gen testsrc2=size=1920x1080:rate=30 sine=frequency=440:sample_rate=48000 60 minute.mp4 +gen testsrc2=size=1920x1080:rate=30000/1001 \ + "sine=frequency=1000:sample_rate=44100,aformat=channel_layouts=stereo" 5 ntsc.mov +gen testsrc2=size=1280x720:rate=30 \ + "sine=frequency=220:sample_rate=48000,tremolo=f=0.4:d=0.6,volume=-6dB" 60 tone.mp4 +gen testsrc2=size=1280x720:rate=30 \ + "anoisesrc=color=pink:amplitude=0.1:sample_rate=48000:seed=7,tremolo=f=0.4:d=0.8" 60 noise.mp4 +for i in 1 2 3 4 5 6 7 8; do + gen "testsrc2=size=1920x1080:rate=30,hue=h=$((i * 40))" \ + "sine=frequency=$((300 + i * 50)):sample_rate=48000" 20 "clip$i.mp4" +done From 436bc5a20455af6b8755736423d728f4f1e28899 Mon Sep 17 00:00:00 2001 From: Mark Santos <24554186+markksantos@users.noreply.github.com> Date: Wed, 11 Nov 2026 09:31:03 -0500 Subject: [PATCH 2/2] Address review: audio-only progress, drain ffprobe stderr, reject limits below 1 Progress: an audio-only encode's -progress blocks have no frame key, so Snapshot.of reads it as 0 instead of failing in Long.parseLong; duration() now inherits ffprobe's stderr like Probe does. Batch: runAll rejects a limit below 1, since a Semaphore with no permits blocks every task. Article snippets updated to match; re-run on JDK 21.0.13 and 27.0.1 with FFmpeg 9.0.2. --- draft/driving-ffmpeg-from-java/Batch.java | 1 + draft/driving-ffmpeg-from-java/Progress.java | 9 ++++++--- draft/driving-ffmpeg-from-java/index.md | 9 ++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/draft/driving-ffmpeg-from-java/Batch.java b/draft/driving-ffmpeg-from-java/Batch.java index da0bf649d..7296bd1c1 100644 --- a/draft/driving-ffmpeg-from-java/Batch.java +++ b/draft/driving-ffmpeg-from-java/Batch.java @@ -24,6 +24,7 @@ static int encode(String in, String out) throws Exception { } static double runAll(List inputs, int limit) throws Exception { + if (limit < 1) throw new IllegalArgumentException("limit must be at least 1, got " + limit); var slots = new Semaphore(limit); long start = System.nanoTime(); try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { diff --git a/draft/driving-ffmpeg-from-java/Progress.java b/draft/driving-ffmpeg-from-java/Progress.java index e219292b0..841ce3a80 100644 --- a/draft/driving-ffmpeg-from-java/Progress.java +++ b/draft/driving-ffmpeg-from-java/Progress.java @@ -6,10 +6,11 @@ public class Progress { record Snapshot(long frame, double seconds, String speed, boolean done) { // out_time_us is "N/A" before the first packet and again while the encoder - // flushes at the end, so N/A keeps the last real value. + // flushes at the end, so N/A keeps the last real value. Audio-only + // output has no frame key at all, so it reads as 0. static Snapshot of(Map kv, double lastSeconds) { String us = kv.get("out_time_us"); - return new Snapshot(Long.parseLong(kv.get("frame")), + return new Snapshot(Long.parseLong(kv.getOrDefault("frame", "0")), us.equals("N/A") ? lastSeconds : Long.parseLong(us) / 1e6, kv.get("speed"), kv.get("progress").equals("end")); } @@ -41,7 +42,9 @@ static int encode(String in, String out, Consumer listener) throws Exc static double duration(String file) throws Exception { Process p = new ProcessBuilder("ffprobe", "-v", "error", - "-show_entries", "format=duration", "-of", "csv=p=0", file).start(); + "-show_entries", "format=duration", "-of", "csv=p=0", file) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); String out = new String(p.getInputStream().readAllBytes()).trim(); if (p.waitFor() != 0) throw new IllegalStateException("ffprobe failed: " + file); return Double.parseDouble(out); diff --git a/draft/driving-ffmpeg-from-java/index.md b/draft/driving-ffmpeg-from-java/index.md index 21ee04ecb..a086580b6 100644 --- a/draft/driving-ffmpeg-from-java/index.md +++ b/draft/driving-ffmpeg-from-java/index.md @@ -185,10 +185,11 @@ For a single value, skip JSON altogether: `ffprobe -v error -show_entries format ```java record Snapshot(long frame, double seconds, String speed, boolean done) { // out_time_us is "N/A" before the first packet and again while the encoder - // flushes at the end, so N/A keeps the last real value. + // flushes at the end, so N/A keeps the last real value. Audio-only + // output has no frame key at all, so it reads as 0. static Snapshot of(Map kv, double lastSeconds) { String us = kv.get("out_time_us"); - return new Snapshot(Long.parseLong(kv.get("frame")), + return new Snapshot(Long.parseLong(kv.getOrDefault("frame", "0")), us.equals("N/A") ? lastSeconds : Long.parseLong(us) / 1e6, kv.get("speed"), kv.get("progress").equals("end")); } @@ -239,12 +240,13 @@ $ java Progress.java media/minute.mp4 out/minute-x264.mp4 exit 0 ``` -Reading the raw feed turned up four details: +Reading the raw feed turned up five details: - `out_time_ms` is not in milliseconds. It holds the same microsecond value as `out_time_us`: `out_time_us=4501333` and `out_time_ms=4501333` in the same block. Use `out_time_us`. - The times can be `N/A`. Before the first packet is written, with a slow preset and a short `-stats_period`, the first blocks read `frame=0` and `out_time_us=N/A`. They go `N/A` again near the end: in one run the blocks at frames 1733, 1768 and 1787 of 1800 had no time at all. My first version mapped `N/A` to zero, and the progress line dropped to 0.0% just before finishing. Keeping the last real value fixes that. - With audio in the output, `out_time` runs ahead of the video. Copying the audio stream, the first block said `frame=34` next to `out_time_us=4501333`: 1.1 seconds of video against 4.5 seconds of output time. The run above shows it too, at 10% after 88 frames. libx264 is still holding frames in its lookahead while audio packets are already being written. That's fine for a percentage, but don't use it to count finished frames. - A block arrives every half second by default. `-stats_period` changes that. +- Audio-only output has no `frame` or `fps` line at all. An `.m4a` encode's blocks start at `bitrate=`, which is why `Snapshot.of` falls back to 0 instead of calling `Long.parseLong(null)`. ## Stopping an encode @@ -300,6 +302,7 @@ Virtual threads make one blocking task per file easy to write, and a `Semaphore` ```java static double runAll(List inputs, int limit) throws Exception { + if (limit < 1) throw new IllegalArgumentException("limit must be at least 1, got " + limit); var slots = new Semaphore(limit); long start = System.nanoTime(); try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {