From 4686454a731b1b7d72522b843f0015f806066837 Mon Sep 17 00:00:00 2001 From: Samidh Date: Tue, 25 May 2021 15:08:58 -0400 Subject: [PATCH] Add segment_list support for DASH on-demand profile Configurable under flag --dash_force_segment_list, default to false. Note that DASH live profile is not supported right now. --- AUTHORS | 1 + CONTRIBUTORS | 1 + packager/app/mpd_flags.cc | 6 + packager/app/mpd_flags.h | 1 + packager/app/packager_main.cc | 1 + packager/app/test/packager_test.py | 18 +- .../bear-640x360-audio.mp4 | Bin 0 -> 44576 bytes .../bear-640x360-audio.mp4.media_info | 40 +++ .../output.mpd | 22 ++ .../media/event/mpd_notify_muxer_listener.cc | 1 + .../mpd_notify_muxer_listener_unittest.cc | 230 ++++++++++++++++++ .../media/event/muxer_listener_factory.cc | 12 +- packager/media/event/muxer_listener_factory.h | 7 + .../media/event/muxer_listener_internal.cc | 7 + .../media/event/muxer_listener_internal.h | 1 + .../media/event/muxer_listener_test_helper.h | 27 ++ .../vod_media_info_dump_muxer_listener.cc | 7 +- .../vod_media_info_dump_muxer_listener.h | 6 +- ...media_info_dump_muxer_listener_unittest.cc | 50 +++- packager/mpd/base/media_info.proto | 1 + packager/mpd/base/mpd_notifier.h | 5 + packager/mpd/base/representation.cc | 4 +- packager/mpd/base/xml/xml_node.cc | 107 +++++--- packager/mpd/base/xml/xml_node.h | 9 +- packager/mpd/base/xml/xml_node_unittest.cc | 172 +++++++++++++ packager/mpd/public/mpd_params.h | 4 + packager/packager.cc | 10 +- 27 files changed, 693 insertions(+), 57 deletions(-) create mode 100644 packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4 create mode 100644 packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4.media_info create mode 100644 packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/output.mpd diff --git a/AUTHORS b/AUTHORS index 30ed4f3c72..fa5959d1e7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ 3Q GmbH <*@3qsdn.com> Alen Vrecko Anders Hasselqvist +Audible <*@audible.com> Chun-da Chen Daniel CantarĂ­n Dolby Laboratories <*@dolby.com> diff --git a/CONTRIBUTORS b/CONTRIBUTORS index e5010bdb0b..bc24fa1476 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -43,6 +43,7 @@ Piotr Srebrny Qingquan Wang Richard Eklycke Rintaro Kuroiwa +Samidh Talsania Sanil Raut Sergio Ammirata Thomas Inskip diff --git a/packager/app/mpd_flags.cc b/packager/app/mpd_flags.cc index 99e3b4e38a..5a881a0e6d 100644 --- a/packager/app/mpd_flags.cc +++ b/packager/app/mpd_flags.cc @@ -69,3 +69,9 @@ DEFINE_bool(include_mspr_pro_for_playready, "If enabled, PlayReady Object will be inserted into " " element alongside with " "when using PlayReady protection system."); +DEFINE_bool(dash_force_segment_list, + false, + "Uses SegmentList instead of SegmentBase. Use this if the " + "content is huge and the total number of (sub)segment references " + "is greater than what the sidx atom allows (65535). Currently " + "this flag is only supported in DASH ondemand profile."); diff --git a/packager/app/mpd_flags.h b/packager/app/mpd_flags.h index 337f4452fb..ccb4192947 100644 --- a/packager/app/mpd_flags.h +++ b/packager/app/mpd_flags.h @@ -23,5 +23,6 @@ DECLARE_bool(generate_dash_if_iop_compliant_mpd); DECLARE_bool(allow_approximate_segment_timeline); DECLARE_bool(allow_codec_switching); DECLARE_bool(include_mspr_pro_for_playready); +DECLARE_bool(dash_force_segment_list); #endif // APP_MPD_FLAGS_H_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index c8653ce7bc..179ecfd926 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -451,6 +451,7 @@ base::Optional GetPackagingParams() { mpd_params.time_shift_buffer_depth = FLAGS_time_shift_buffer_depth; mpd_params.preserved_segments_outside_live_window = FLAGS_preserved_segments_outside_live_window; + mpd_params.use_segment_list = FLAGS_dash_force_segment_list; if (!FLAGS_utc_timings.empty()) { base::StringPairs pairs; diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index 4605bbd658..0399ed1aa5 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -478,7 +478,8 @@ class PackagerAppTest(unittest.TestCase): default_language=None, segment_duration=1.0, use_fake_clock=True, - allow_codec_switching=False): + allow_codec_switching=False, + dash_force_segment_list=False): flags = ['--single_threaded'] if not strip_parameter_set_nalus: @@ -568,6 +569,10 @@ class PackagerAppTest(unittest.TestCase): if default_language: flags += ['--default_language', default_language] + if dash_force_segment_list: + flags += ['--dash_force_segment_list'] + flags += ['--generate_sidx_in_media_segments=false'] + flags.append('--segment_duration={0}'.format(segment_duration)) # Use fake clock, so output can be compared. @@ -1518,7 +1523,7 @@ class PackagerFunctionalTest(PackagerAppTest): def testEncryptionAndOutputMediaInfoAndMpdFromMediaInfo(self): self.assertPackageSuccess( - # The order is not determinstic if there are more than one + # The order is not deterministic if there are more than one # AdaptationSets, so only one is included here. self._GetStreams(['video']), self._GetFlags(encryption=True, output_media_info=True)) @@ -1526,6 +1531,15 @@ class PackagerFunctionalTest(PackagerAppTest): self._CheckTestResults( 'encryption-and-output-media-info-and-mpd-from-media-info') + def testEncryptionAndOutputMediaInfoAndMpdFromMediaInfoSegmentList(self): + self.assertPackageSuccess( + # The order is not deterministic if there are more than one + # AdaptationSets, so only one is included here. + self._GetStreams(['audio']), + self._GetFlags(encryption=True, output_media_info=True, dash_force_segment_list=True, output_dash=True)) + self._CheckTestResults( + 'encryption-and-output-media-info-and-mpd-from-media-info-segmentlist') + def testHlsSingleSegmentMp4Encrypted(self): self.assertPackageSuccess( self._GetStreams(['audio', 'video'], hls=True), diff --git a/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4 b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..81d1a3dc5662634aad430ea4941ba329ce8d49de GIT binary patch literal 44576 zcmdqIWn5jowm!OWDemqr#T|;fyHniVy|}wWad#;0?(P()xVsd;i+AsR&iTvzbidvF zc$gU@naMLoWM$4|0ssIqQ)dr*8+%qJ05DiM*>M74^C=q}IGG#Sm>K~9VB&rdCh^A124`dPR`!|08j&GM*}NhNjlC} z|5y+8-`0QH{eOmkl>d9m|0WCkFSmiL*ce+F098ab#{YK@s}B5g1O5N>gp-|%?f)!B z87Lb|7}y$H18o5|NRW+%ttl`A<--S`+;%4kF>?Y-kOPzdAWP z{nNz2_-8X@o7ftC68jAQZ2~9&=~MCV#-}lHGIsiu!SI0pz|jQIKLwCGAl5D+AW)Fh z_5qL}pQR}{{fh~lsiTbnu#sv-3Z8{8J$S0F<5nUHd;8G@$JH@3jA1fO(AU{v!ad zNKQ8YYQTT%$V`m?DHG4c+R6Ffx_>hT_S1jGIBi@_+&}NNG5PnsAnTu5iJTq((SOE2 za{}?NzWu2H23C6~C-ct)B)7}6o;q33%3WRprJWaPpbF&w;;m0L8gM9o_5mat)Boc6 zS;Q-_lYi>*oImxT0GiLtKmc3qzv@JCHZ}f_4ZtM8h_mBo2L%1hM-R*nY<&=*FF=Zc z5C*~?h#Vk*!w^IO2w?dj?LYwg83=Gdfj9u+1B5&fAwc{9;!_7~c@UtVK}LWO2SN{s z??AKxAqj*V5V}B=0bvG&J`lk`eAe?>&S%-5Iu9T|^Q8h21O(8>|CFl@ym|ovwB@rt zL38U%4{#C)pmhKMdi#kR)IDK6VU(-c2Ohsg z^RBXXolcb6UX#6gm*hgkS0M`t1eLSW4GFNzY9xesif$w^qJ_F2`(g&aT!Wr$viY10*ZhjB@jRmMKZ>o zchhpMHsh~JRTVV9xcslV`a|%thd*eM*I1jC)xAIhE>A=wX*v%nu&@L`!_Sh%7|P+r z;7Sy_RB9D2vVAG~ z>CtPh2zw+yZ}e1p_`zgfC8C=CS-$S8mmexsCN z!%U7xlxd;{%^wm0U+hX&JFjIQigsuxLiXgdh=gbrisf)qFUwSMf_+a*b>_B)lP{-! z!R0T%q<^?e(tj2faOc zqLMrYxtqfo6|$LlAr*8ii3}1$*hqJ1fOhrLiOWe3;qluU*wf61qRBj(Y(@LUPaSV? zv(%AEj_lbKHxN;?B8xe&nc%cGZnrm~wFmQqBbSixOJE{(`uGNd`Z$arRIZEEkn=)5 z2TU2V`V~RpAb(A3XFD%-?=sGaC%jkH>u99d6n^TFr=OSW;&~+>m+6e1%$G7$gJM)% z4AQ^-q5CDII_FN;{vCv8JPjoGAH0Nxcg=#kO>E^qNFoNt6Q^C)TC4*L{ezL#Q1S|pH8z( zYUr5>D*wLDRC!&)sMc`{G$_2XdB4v8F9da|y94~Rq3vjQXlfk01HJH*9>m+2iC5UZ z#vS=Yp2XDQNUcG~5`fqX%gb8m=lAUx>_BXa}eNy>0dr%NiPDM2>gUGKKM$ zP7iQ_#V3a(o(NXuO2g(LrWWyuuFDthT6^=V*is&^#31BQge$%yI;-Pv z=JPBZ*CUktc)-wgR&bLhIn4K3ZVixXV)pKjfiv_RQ5x~u_nf;DJWs>GS<*tQD!%i# ziVwg8@tCA3XC@+B@Q-J63`hU!=Rs8m;ZAPAQ@W%I)?m@~u9|GZJ60n^S37mSK&+HX z_F}f!BPII-X}TdFo|#e!eQ?&fMpX;P8-sQ%d%Q-h{K!D4!I+=~i66r`X889S4H?gQ z_i|h3J+Bp;>S0@JT0T0m!L19@n4J*9IO&Via=A?7pSzq71CwP@DCfGbo|K(lX=D{f zloOoWLzI=lbfI19RSe(+EB*#mCV5h;b`lt^Sj0V;nRDd@B%rbq*r@7M@k&Qf36VtV z0cxmi(IyH!Vm~kUHPw_S%I`?V;Vw3W(5W5en|yS@3zx6qUZjZTNnD6^bTOswRr5-E z4Li!t+U_i?L(pi&1Wz2muFWJaiDiCEC>jSPpioJI=x+*0K3<30)#(? z-v9(#N2E}D%Xg;Cu(2t^FocGSfeBwt*uBBv<1RH>z|ij^xNatA1>koa-=jmkxcDdN2%()bWuZ%(?$S9M11 z1NBjY?uO<|bsw%i{3Vvl4?OQy7+AWv1mG-5#Hx`;7MF>(uj5<^{F(OUF9o~&`;iN_ z_6Dy1n(-brB&c{ZVnBMPRqH4^Q9SKuP=|j?p>Nm~?+Cs1SYGI!%LS$p7_vaE55NLI zwZOwB%PP<65u*xd@BbLGlk)Iqu4{f&d`HoHesmKn?GhSMQz@OfVex$C+T}Lo4ub^1 z0oZy%%k+?8;QLpNHVD_#u)hD@6R9RTR+!_mcxlYYZ5`lEDs*J*w+n^H{)jmE;yw06 zy8Mvxc5+$5Dcm;X^RXM7O58*28Pthg9fSgi0yYT%@a((O`DC-_F?pQnl^IWy@9{&T zgGXSjw3S?pXWF=`*xED`yo|BIoD#|E%#B#v0tPnIjM&+nT}EpxV)x3BU4nwNI8#9M zH;%b!pU7ZT*2bJg+ZuKWhnkqRgH=p3FAWBb=QVqm8(EvlI*Fc?xlL~9{!ZHhrc?$E zn~%*${&uNB(@;sHLbTUo2qR|xAAMRzsYsEZ@(bA9P(-N(Y%>7@$Wfm}3FX(-yXiY( zxjI>5of61ZOG094YJ4Y>_P$0#jz(%K z>iEKmje-w2&xb*`gUQ1EmPq@R*xn|~p>4!^a56F(L+`M37%Vg>uSc;wwU;W>9wa>~ z3*UhB+M%u$aI7pUR#Vk|r-Kg1{(Iq}9J$8#Y6$dBhYrFNM$tUc;yI8~U=NQN8$230 zGnTk(pxl6nVFBmpbW`2dF6ANl)nn|Ctro+EEs>3NnFY0wtH)+vzMe^a5bljl_ zQDPQ*Yk=$^5$ZW97X0K(^I))(rHAisDl&U&bX_97 z7k8_iPKhcuYHjBj_E79 zW<0C;8A&OoMz#B!oqk1$VC%Ce{<#YR^W9IB5sG7Q1%0B$Jp%AkAWwtW)*WD$kb zA?20KW6tBlE@9ic&@aRVuZ>RnM1mCLH!uS;q7G7-oo6$#VT!r**1x-BY_nXZxMZ%g zTcW|gE&cA?0d(^QN1|a_>EC-YC@LvdsZLFUKr#yambLnb^YhTZ_p_XYcl8Eya zKc6v@b#%{rI3tcWMVKh-gA*KKq!J<|+#s)~D!}*S`a3dzoyrILrnJaX#ALq0!z{

xlWy}{5{WEcC75whHAGV`-TmSh-j_T=W7Z!j)z;w9WCLA-oEgy7 z)XJ%y8OYbLtPoW{F|Q}*t6PVuvssprb3<3xFgpY=hr1Yxt(uU8_`&W4EFR;y<8+8| zf|844#)RVItSR+@V+x^0t6P8iyMcNv(t~#!xo9z8%?4URM1}-q?|y|C=3B>$WbDo zgqP7NwGpeb!T!rUQj$g3Y8}jTP>g{%3mT$zyRP0=+!artyh1gdPlB`i`!eZU^*Lj1 zv9xHW03`5+)v1F`W@sV`Ud`EP&}Z~N6d8GJv9I9m{Z>6jK6`SOij?vC-bPK1&Kf5l zIIb22QSfu5g;kgd5`80k(6@-6=jD~jIBScO+V12Ginh!(3SqWPNK4lc_!x0oJ|+x? z4e(nhSA%3E#R>!G-@>IgKyUc@1+3aNDN%~K%H5<8!2pu2v^LUY=L! zo4|tcv2>PhGybGY0yU}$01=EuW7cGcF_M6cdwl^8#2sy)ESr!O;a?ax(2d-0bM4z& z+TxnE6g32rx6yHa%|(uc>B>dJ)&xebI=}&<8$vWH%3tE^a(m75^3vG#_u;~0qI6PG zsiqu1FD_o%@_gd=^vdx_#L|<;pw-{`>u)ZtKFwIV5Z3vb=bTDie|9vQG%?-eM%?Z7 zoy&C{eB$n)RO_@3!1d!?X{Ll8N(DsgGQZtXDh1XVq!g1>MP!OS^5#J+QUjTTtvPwc5Z!fq{szR|-wx z3S(d%-Kk`3lHz;o@cQ1}-@Sac^s>7<_+rL}T~4F=ly`^C<=j-gIvMra$%U-dr(4_j zcJ~k<=3#r06?e7WbF9+oVDibk`#jH9@~@s2DCAN zdJP?qCZuz^*?@h$c_pdmC(ChnpT#FH_oA+kpgO;w3eT4Ms5Y_w-GDsVcCcW?K7M$u0}xvCkdE$YXXYJNv%6ZYVQME~_F7$+=Cn z_FSrOJ)|fvM+ugM!hS-a=GA<*KIB<%H1^094DziX3yXwlJCWN_eKD%SdE@#Xwm~DvYLve0Tucfwe z97keq*J@|Mxi+1!@j`f{xB_MBK8bHH^HK;p%dGk#>R!CFAVW@TC?M{ zJ!qdhrdFi-f2YlPM_cuR*u$I>VxH;c3dVbHKfd@La~~Npf7Nim#kV zPcb3|tF9236=s@PAKqCzATme;yC-nTDrXKuq)?YeLlcLU)}d>$0E$YAn@XnWXCD;t zW4NdJ6tw*K1KryIe)#4Qm-^(rHaiHl^Ho5@1nh;4IQ+{3V+B<&pNAm_f%_HE;YA@R zzlmA4E_WllPG;LRJ%nhCB_XdqL}}$l5O#i5hC8VHy57Gf>8g6V-W>39D?2-!Zn0xL zpVe!-6WZTw$iuKg)QdLT)R72ksZMw-4+TV+v+jA|&)my;PX$|{i~1&%Qs!wbwc^my z0Q#fhth}R&mqGk_2Mi(1kRSn8`wRZyWFQz7$RugwoKbx4TqLj%Y85?T0!m0y?4vBd zdV?q(@@sK3wZ^Wb`ci|*1YUgIe01rW=X!i=RHZZwCYe$B-hqqSwgzKO52biz5($pb zXKF210XOuR0P;cIr?uq=MHh*I_)w4<8A+zCBD!Mxqu+T|9f&O+wk1Ispey-nO)JPm zRWt{@r@MlptT>LmF-M-o-J<4dzf{zZX#zaatE}Sl>vG{ZXNH{;FZ+wN+O8O zhi;2=<34myC->^WiNz%ZWm@#x`cnnVwDC|m_qDnN4H?Qz%aOvzPYg$i>QRzS*edPo zx)wPpcB3vmj}D^^eu*Ma@7m(%=al#IQ>raQr9s7@QgT`LJWsnO+Q^thI&v+o26QzT zgP%_9==5;%)}=yk8`DZf5U39`N>D+Y`(85vs_<`x)1FJFf1eoy4!@-Q6yVldyLPR` z!@!>0o5Qj&txdbfB;D~|BUVM3R1dLei!iY2n$`RQN1hh#eS45^mOa{99@C*Zhb9Td z)TVvrJ`D3LO8Ry6xWKol+r%}=F0{9jbpw6H=9IwSwJZizMlz?1@TlzV9lmz#ThjF3G*rhlL?VebO^Ejc_WY z|NeP5rmf&;KUWt33yc9GJO4}vazs_y{s$t(2`zc`^Vnx!t_s1M6Kt)r(ol3!I@(Q- zr%@&IH(tw~`dJs7_-nL6svmBZL*x87p|eqAQdz0gMA`EgDc6(dH)YobK`cf=e*P)f zIDjVQdL7vey^R9A58%Cz!p$V8^)je|{oY6Gc4GV1U>P|(;qe`%F1D*os13DDax z(B#|jr2}(m`*mi89(EQ}T+kPII%Bi-{>@&)wzAb>kr`U|5UOP^J--M$#b92P5?}FA znL=u}{H>$z6@)MBE1`Uu*_9M+-_-gM&2!f#tyD!N%4pao^3f%UG}({1ez#LS@!(}N zR5I?&whh}QC7+iM-OEblCCi=`3i)5S)r$9qw9oD=IIHNIB|a22GH|>GX%5AF0sZo;XD73D7>PTA(vCv{s zDZ-Xo3TcenrSLYy`BOlGX0WhW(T`SF!77^)z|?k8;FjY6#k22!iiD5`@6##oO~G3x zAfvQ!?7b7JJXy6ghW$4eNnF~EL?D9u-*b zz7!F>L7p)DOZYFIja|&RB@;O3fXW0VU@H)# zPqnCwW~kX0n(Q3W^kji7dwNxMjk8c3h^ok*bu_dlojFoZWwg)l#L$z?C0mF%Bn&uG zQ?k0a8>Xn5hS$xAabSpDV#GPI{TF-L(O4P%l+h9?ivW)0-h=Z{sDZmkz)=&Xv?k!kLOk?<-|4gP{b^5KGqgtnV z*JA>fxkA71Lv3@L!CtS1*tKRTa84--!J86h@mwgj&~Z{%SQ=HxC%Tu@oi(*#a%I4|_}Xc!+Q^mM#=-YkZx*r4~5eT?29 z3j2?dK9{b+mH4sxmjkBI^*Q~%>Zt1mn;^$~})wKp@T+R5edKXV}*O0eMkMqO{@j77RJuOmYkcGJ5k``)F? z5RqznAmHF{9ya*0Un#cDL@C+>bogKh%8W?ScdSX3Dnl9Kcyy{bjt=gFWrQU;K5X%o zCWmC-g!{AGN0*yrL(`92M+x{GO*og+y8+wK`MYYz zu)$rEG^_=XL}{BGi#>WMgwUKWE+0Q&8kw`8~LZe7z=k79DjbEAR$sAihUtq=VJh{$ipOGNr1HUQuBNFZ9XVlN<&`>_N z#2VY5h%ECi#<2vg%$ZgbTYmQ7iVhXd!9rW6$vBFENHTV~C)*4#h(UPVZ6Do1CA)hJ zp)feGRyPLsz3nz2^S2Iv99GH^ZtW2gSDJtDzuQ7@WS(>{SMK9|_!AE$ya^W<3o%ZWB`tM28T~c_wOBJslL5sE$YxwuaoulG%KUhGOnA%xH0(6rd_)d&Y@KWDB8XFLm=dKfPk-h$S`2nX+yV4SjjxeAlC0bXf`wIaAZXBb|> z7pfw`S#OXd5GTPP`#vLQ600H&N6B6$2`F@8GCzZDVwnkv6+>AV0VN~0{6RQ&D$Trr z71B7sZcUso|Mypbd~EC);N^XX1;(5DskAM&2QK`2d2-SeTO_eSkB=D^;5Mfk>^m`& zwM<$TSJ?vxH$4V}!-N=)Ru-}*UEWtP68b$Rwuy*%yTr zm#Yhh_xR2M7VaN(ycrm%F8b75O?IN-YMc`*c6lX~1ZM7CFJT2D^~nnIHzg`_c6+_u;``25KdF>9p}e(V{2fV(-{M6o4ue8- zMh#Da?^ocG3hu*Rn_XAygea)SZuPymgnr{io0w|gRzV+69M%kW+Z?4m-{6-gG$Xm4 zJCqkXZwP*zsRrD~Z7Lf(&$;w&XQ^~iM?+QsMkfOZmE`YTgbE0d;^ zY=D4UhxYCx4(FFWIqKmFZ`l92&h!aEnG)l-+9~q(e*dPW_tqujC#qT$>HYD$=sQAi zvJv*xuA!S4by_-x!CzECEt0|R+BtTQxOf|}Ek(i-^s zTHBQ3aR~|LBlzj1@7Xc$?C43$>(;PLTiJ)5gChb7(O0O55cxEY68+}u<_7J9oagTn zMc3CI2rI|)ziIs*yc^&Ef%YcZ!-xfi^iv0!_u+@xmDGF6ay3r>&{Y~!r2R%-um&?!mA3M-;HoUc zM5Qgu3R|L}<}|0Y1f_w|0yDcuR+zyIWopS^QqUb8Y{K@43mWBi*m~*Uh(uQ44c^~t z9+8=3P~Nf&|64g`YcE3cI$z4u`2lvDFNYosK(vl+_N)KsnC>z!+~Ge|MMdm+{LE*({ic_yZ+*$hY|Q$ zg<<4!9sb<&gk*4KA7rAmn--x|=AqTzR5%!XVBp|&rsouEMl8cF(G1?p(V+B8OcWl2 zS|X2Z007|Yp8o)##&+KWynGAB4aZb*U^EhB)YBUHKh}FfTFti^m#W5+ZkVFu$n>k_ zlkd-J6-Chso*BA;$AxHGabD2)H@Uw@zs>Jazr_V7o+t!34P;({4IQ1l0+44!zm=4p z>FstU^Q6kL9{zax&b*jPOH1G?2uB&zKeeL!5t<^On{ts0ani;+kPtM>uOKS#I7a-F zm95nGrxEs0hb*T8$qdS{_j|8Sa=)L!=;>Uai~eOVReWuFc#4c40MJP;0DvAc1_0u1 zVFK*TiwvV8aOjlcg(SlO0IweNc>Qt?$yjrS}7OCBaPtFHY6kuFD&ena5PqO(Uh zIZVE9ZbbHek}$iumBJhQ5KFXshJCJRE_uw;TR=rP(Zwq(i<$yKaVgxPaRLTr6M`38`3qt3RNd7VBv2w zKU@VdmJT_(z7aUMtd3TJBsPAv`-V`#cVdnb`<|%c?z$!`C92|{hvfuM`;k?r~Ep9E3Ertez3nv&B`vXtfGUvN3%UX8x)Za|tQ3%v9c1h%jp^&_#ofIVv@)F5c zHlzD&585tcVvFZb@>~zXG&rMCfs|p0s6A51W;C5O~gzO=|jE zQi*A*AfephzHN0O^%Y=7{ll@3*-F6pki6^mkI5Ily(5%ZggA47AeN+>EYDlC94r{R z7?9KQ9LUOgvZ=w>izoJFA+y1=TkT0Z;EFvt1Y=Q<@4j5uibhh$4HY(>SdmU_ppPH) zt8U|*7BwH~-!GOm%H(mcmSdI`FB+qU$oo!<7f#T z%Rd`_5q^7|igKiI7`BCKp1~`iEfXaPet^y>073os-j)bQj=r@pnZra;)amJB$CA{nv-7^kmEc>Q=t z-BDB~1HVlM3(pvvGLSE;X{uZjq;^}<(MqMrLm%7_&E@_f&rN!XPwRQX5GbKrZz*HG z!;uMjp*{S=FtmF8=D}D1YKuobf&7Mpi{LK)OW0xvC=FQLA5qRIM$NW(%@a&xn4x+r zZM&$%%bfEo59-#8Y%kS;sagdPYBt_vAu3R?Jhl)Hh#Fl)rLZdfAV_ipt&_5lI3`FY zK3;IJzqtKEH|xE3dO~3yxqGdN6cbFnUuI#k9oVt;wu!012dQ%-R8vI;Gfh+qOMIkg zq+zyJx!m7!YX;e>)& zX|)&yQ8t?*A!5O#?B{PVlc27&+v3NG>r`YK(( zFQmDlYUe;4B8DIc738+@1fpQ2W5aC_rYuE7I${JWFz)S`I?zC28ZN2o4rU^w++6A} z#!ktJZlOLSmS?tq6Ts{?+nwD$m*_$wqTy&Ff=aeWeKL}87$X}1JV!52#G0B5!0!Ef zX`Qgy%-KL7%h1?2o)|hk&D!`ZMTHTd|l(WBj)Wic{H8?6> z$2wVa(36H5X%7wcr##8B6Ceo2sCfw_x1&63gTya(SNt2mW%VijKy>15Ki0IHAX@l2 zV?ad4CSZ}gAmEi9`rDl;3BjY~l;@X3g2+TP;zdNzDK4VO2I6|Hqh~;dDVuYjpWn9; z_`e@@?8ZHO2$&uJTEPsHDM=@`w)eF|M-ETh!5h01MnZft=|{wT-$l36w;H1t%b@q# z%a}0;g9a-ow4~(~W_M&yRtT=*b@zVfv225vf~^&nI8F-i1GKC`Y@}1$P6zKK;0f&q zdUj7#%u8Nt%2+9Nxl+I#=1U$EDW2sg_U>gv4_82o_+Id(;R=W>fw}|GM=>tn8r$`? z18>8&(PS!n@}O!J7CG>EnuQ#$Bo_Ah_>6&T@PXhEq9BT}8ToUkYTFO*_s03lPVd|M zQ{#gZmD1Mp>QD@oGBSB+VlI*NP*$%W4A(PNeOe1(nThGLWcw4z5qAf<)0L*?jJiY| zJzw^J=B;|^p5$i-h+(!JtWZ#9xXAO1r-4a^`F4xTbEO5$LH2t4%OFA} z-V?S3i`b`Ah8-QKD}?mw*&{%D&d+9~G zQnb!dTj{d);WYyQ73O1}zHd%LO$EtZHfs`^?#bKcbgr8STK$@mk+*WyWzW28Qmm`O z_s_ljVrwE=0#ERrd&Tre1h>zr7kRZMD3ZM5yA#z#$zMQFk&rX)^W}X9bjb+r#`m8o z-$+KfI65lIN@#3Ba`gpTh_OE(b>fM;!`YN~=Uooe<$XDnQIQ3mMq)?e63jvS;UuKM z=f_S1hGbnpIwFdTLOC^+Nem^2;%hu6$+F7(uKlD-`2Oe@^ip5gc)$F!T(TpKk^;PkF#-H zSz5{mb{g@LzmD8o=pn(|CAss*1Z2kRZTS;+JvO}kU+MacdynGm#p@$^xQARqlskEJ%Tgn&L3$L-7A=O~3i zlP+XlTpDj@AW)%eFD$>ab4S({s{QvaG#%!E_rRZj8PeP;{XDvnzi=7l{yayvftf(N z2$aDm>@UiX3$l*T=Iz#9_s6Z}Z_J$QgqugJX>yNa(+Qn&2f;ieFMQ->UZQiInU&nK zF_^50X!kIOlJu5xYKKG|D){GdwF5X*9LMw8&CR1myPGsO%%D}C__JAGwsMSwxk&6$v5(#n~3 z0g$N2CUnwgFp(rHj68E~gPcf(SmIY0B4A9bE)RM_)Z<{Opviv!agt<`SIBlwSSsAN zbp&$}+}!+p)t;Og13YD5>`LIO5B%eQkvor9KE_q%bY}E*mT;v&apFdB?f8 zy7t9E23}L$dQDy3y3S<^I%Uy370J#9;qsXrWtLRh67k{>pG3SwsbIpgy15;7*6Koh zaXizEN}g<8)zuzmuP@YTw({wL!E~zVDI5Pnw~;rQ!OLS`<0PNH~)DX`FFIK6eL8;Ds z9x}tTmx`CF{cv#EzqERJYuHU^-5+{OyWv8BmcnGN3d&28tek7CbEBfKT|(>;5F=nt zN5W7_YgU$j>??JUybu^Sz%X|d*eJj+LEd+M==_Mr@)x13d(h;i3!w?LsfOr-7%B93 z{cY-5&qk)$ywp;jR>~5p!mgE+p@>umJyUJ}iUkKnd;UC24>2}yhNepRfd@QeSxKwO zOumzcPNO=_NDlJ5uqFNQ!8a?3{ig`I+?+kXsopo(wH5cvX&Eds$(TYi>s<>P(86J% zq_(sowRsAH}4Bp9~BZFjW8?JBRYRZqtipeWKx(7Ho&DiD$}!PG7$%-Uh@HZh=BbdCOp zf4Gf^=pQ7!-*DGMFGJop0kxg$ay5_WD1P#KGEVD%yuGBtp?X);{2E|;Bib{u^t!E4 zkmuRa+)2Ps=m$5qPxyM=P#E*vjq6ZUBm(QK#SoZwQqo_{COWRL+z_&BMx#-f&u8>f8YD+r=WzeVS7?9XSgC z5}(RQbn&Z??u<8vKMclrg26k(l%u+IeCDf%fBzKic*ZJzg zPLk|=3)B_jneY4!-Dn@*G|ZReb<4V zXJ~I;zjq+RXygZ+Mv{ZeBZ<4TQX7<2-3vRC%BV$qR7d~3UAGVOQ{bB9$lGxNU+OIp zuWbD2m~9$MufLer=om`-6*_xKfVqMx70V@~f!3H8^A(_I8pOpNYrtaW5fVA;6*~C3 zQC+QK{j6>e^J?kSc=e2jlR(swscW@psf+tu&Ez|&KjnPHX#49qrkAxXS&DGC{D1PaP6T1jC~3$`WbX-kNamSm z#uOSYQ7AC$II^iLwtm~p>bup;HdT4%ySMKeX;VuiNx!1D62{X3F!z#wFl4dAuFeGsyet@VO-i{H>hxY5Z!O z<zrzjo~)P9#ttg&*NGo zOE>GpDzW2@r(X66djejD+(jDBjfE}6bqo#0U1~W75(9BcD)={NJex-GNRZ-#wi6u*EN4#8cfU`JlR##y!b)23Nb(#K0>M2d6I62(N-cnMK>C{z?~Tq_k1c*;rBv{Va> z>JuXm!eKQhTQja-_8oTI$XLC}O*;xR-K?fhKIkQig}};A_kUC-jjy#m;{cR3oHQgBMoL@?I$>x`E4s!g!^1iadyGP6mP#*!ZIR3w0#x$_)4 zeP5_L(QQ!bC-N~DUHMKc7`b1NI2xnjrfO}!Q}>tb+fk;Pdmg?~xZ!b2GmaHcSsW~C z4t~V(CWZ zPD%6u{#20^kWw2SedHD@IoanJ2{?;S&s2D*AmxbY0&i647n(fw zAp?bw$atX=jY^(D0iDuqAwy>Qv=N;HEw4JoH?q{edAaO z>q9t`bBcv|;0dKgm0f69s371xP!gh*dH{eo#)>&W$;hn~qoSy*BIC(G%t^@eXICmIm5ueT+_i!sL>Zc?0zG0qoO|Tx zq1SIdU*JwP@bKk~AhiW^+lJPIjw*_ue^PbrJ?j$Qxhp9SM|ZLbA3LR$%~W=+}a&+q?LAXa2N8khb7~Bh>TeuNS79B>=rbhCF!!S7` z>%#zfeH9=OH1IXXM11{SwE#ivofZ0n7Rf7xAIVE!#gBamakzNKH4NdwQOb87_1x68 zR_HbSl@j2JTtdr=#{3QwRy7mV8dZFBQhI%J5kpCu6 zd5n_3v;(A{SaDc4WG8EK4+S#Yixcok3smTN)m`&5Tz+L6P0eTpagny5h6Pz$}SX3s_?xdktcV_Rd&;t%wmQb`^!i6KEaK%@lw@K-Yk#R&c^42HAF70zi}U zq6Mi#SSZ-H4UW7q6zqyq`T{>Sl2jCu1b)B>{Ekr-PW?}U^Bw>o&mVhn`inMpx#k(u zJGA*Pwh&8ob3MU(|KnzbD=8V>IlC8OsiTdAeXizg)i<-YAoAh>rotXZ05|HUCT>%o z&FRz8+RUKE@9J0(t!vi~-slK(RsCMZG*_KQ`6?nhKcaI;36wZrs7fc)`^yRr48m+d z7{75ML94IjBNR+&BB5aF^lc21d36;I^UZK4 zg{669L8UZQl6f>Cv{$3tz)!|0bA}&XekTu~F28;05?*_S%fnvQUWxr-@N%izj+}tu z`janqfs4J6Q-SViDTif42xVRnYfc{vfDdy?7rkb{e*b!Bb7a_XrY`PR<=(DGAU4QK zQ>&jQ!}C?EWGN0q0Le9w3`T+6xU`El`QB;qttP>)F*QDdsAF)G#siLie~$_8fP z%!+Eu@mg=fVD(Ep*l(Ys*w?q7T6~(1zl5(-{JNuD>%<;Gz`cnHaexHFTE)YChDnL$ zRUR{ojis42h9biAGtUD7 zZCX>Ne4=~_Z)8&)J7z%|-EZSl9~X-8K1XzOo{-8`#KYhfL2L(;!JM2+)Ip~E4#wBq z{$$Dx2VYhGz;?5~e)|R02+yxyNYobkrNHJQ(c3Z!#sp=Pgz8YnRvlr$jY7|%x%Wzs zp}KF~I|s!TBS0Vogs9t=?fleRRg!z^t-Y;CAUU`N=9L{X>l^CHSO#N#Q>Eppk6K4x z60@DGAlo&?^Ybch^j+Y?`BFO_p&)Eocj|mzJ0bR*I&GQFz4v7zO_komJBlZy8ECcR zGM3e3Kde2@5HWTquV&?>Kr*Gpgia}A#riAX+C!dhKwJh$-_i0#qIE;AG-jsZT{+xY z3CjgDvX$Bo4C8N&7`uCaxBuB~&YcqKwk7attBI9#lr!0hXp;#F6f$#|YmWV?+-i`-MPI)t3l|pcp__11KLyfr$(OSel`!Cb;$eYgbeJj3Vfsp zuiVDekIydeg%Nz-@@n5E>qhB4T52__j55Y^ca97#b z6$?NTjx_{z{fV6mxWJQ8Onw|719wWhU6Prme$Ac&zT~nAAsQ3qH*bv}j+VJuE=Ln3 zN!Sk~kKkp$p`m8M$0C943qrlLMhTz>PXg;h8JD~POyjXaU_a+2oVyE2slZoM9e1sq zLx{rMP@hZo&9zB4)k?T!6IE}Y`6+>rvPw{(0Hiev=B(ikdHKKHc3nhL=%^{=YkMic z2xNif-XNZLugpHFs6g71@GtuU;!{u7%fyfOpEpGzG71I@4ZulH{Jz(yJ=}{-+E<3R zpiG(wa~46idmo?<{WgRvWI}!sorvMJG)w7J$fu@CI1{8({L>t-D4BNDi=>7|l;fqJ zge*apOMsN3B_9Xr{wJ4edJ!(wst-je*GAV)_F>wbhDttO-ck-Dc}}-6K)B-?!VSt8 zG!6zkUyv~lRBMnRQQwT~&(mYpqq8RSPF*m{+Nz%v_z^x-+z*@gso1f6g|U2wLx9Bi zy{$^Y-OZaFNx-Dy&fHqr=J2;AmAzY|vC4@%!d-LYF<*wnB`e(y;Ia~UAbm-&L#~-Z z-p(t1plXmRNlFCI<7)zc_wB`)k%I|yX9UZc?RcVYK?|?(T4aLs-Z2Cl^1H!Gc})y| z)e|ZLa+JdS7^HA$_4Z&knV}{sn5`U^=nc+_9&Gw@eLsT zyRizAKDRLMZ1w+8_D(^zb79+V+qSz`+qP}nwrz8@&DB_K+qP}nwzd2HYJI!@gMT0F zRNZyWj7mmk4rY?d7|%7?n^MbkBe0NCFr+lVZaFv7c{;DmkeX5Q6etiJ4%)=OzNgzM zq6d@4p2(+b+i7K7V|H#6ZD}Rw5(`ik#9qTp%evUVnh&JeMHz(T$Yn-DsR#EwJ+9NP zkp-Z>hl9}Ee!oIT3@mHY+~yYFTWW+tU1Gh4B@rd#SsZvNwh z080PAI4FRA+5`SS927r(36P(Xs{dL34+llVe+@nVhl7IZr;ps9G3@_!O8gXV0-FCM z;3xmNAAEoEEEL(f&#N z&-X`kGO+j`rwGK)%FhY>=uiwS?0)#)@^j*V0zWZ#`hPVj095}gLHS39fBauL3e&%J z=6_`QN7jF2`$zVF3@{@N7;Xr`$zeIRQN~5e^mNM<$qN9N7a8+`$zSE)c8kD007?j|3n~K zjWDhVu`E;0;rQQz4EPj(-!YO|REQIzdWQPnH_c~Sbp1(9&9l&!%Wd{1Rq1~&*ZlK@ z=TzbFmjH$P9a5!7vD4@rF!Fk|DU>(>Z*CEp?-E`3&8v+NIXh8U+JdmbId#1#omVYI z3{k(YvRJZQ2pGG7W9+ES*CeUc$;sOHBWjtR;_{dgQCNZf(gA%eo>g00c$J}N5wk^3 z=D+D(WsE?Q1l)$FzY?Iz0`Dbm3}SU3M`rqnvCN2MuJJPyMyO0g@BUcza6wx@Io1v4 zm{@ebe+2@8a|^_$Crxyc05$1tTlPCmz@95m6`jSv-OMnUWh#8W$aQArtBp}<9Z6I zvL5Ni?6$GK3_1b~9REhB_xGk8SrJ`+3e!ke$?TOdWfRvKZ8US@OGHbRbzPC8WNuhf z>!~c$;JWeiTDWLFjzYz`-e>$qqks*Y3USBK=Q955IWSrX zbJ-v7>wLocv`ieaOSwTwjUk10%+V2c%C8a|rhd1{7!Hx&dy1W*{VhKdwjX*{Z zT;RGZZCi$`Gg~>?8By+1?AGw=!8oFc#phMs`7~>k+w;Rf^_%{`1(Ex-KJ4B^d4)YlH(vCdo{r!Z3v5H zCzq;99(_YFk#AZ>H)p`J9tAC5jYExVZ0xlFsZp(7R z`$Yq`WjLqiICP!ItDov1S?nEGOO5=U$(UG6Ob96&={K)l2_yl`8n?giiA~@!o6T3l ze{+i-h1?r6Bw#VY4^;)ZeP?igeKy*@KfGimKm-w0yn`aZsU{32Q|v}(qG#sRW%S3i zW=(=#+y6C|F2qt8h(CkFpRt|(#%m`Ny`F&*P$sYSz7`s=*&`4+_0{O2YUDV}-W_oe zd_O<t89Q-3Kx;%z;>p#wOf@+G7|#MQJa_(e;kxJo@g&c#{Y?|HNd zpB&dxb`$h1%@^VABgQVn`7M7t#^y!JOMcu0&{)c8KDqQoRhqAJ!^bmXv~gKYpS9DA zkd$vP35(xNQkW9q&X~y%zyX=6-jD;j1u-ivNtG#3S=~T)`BHJp-Nqv1Dygw6p;2!_ z#=u2r`kRYhj)AuPoqy*CX~x>C3ua2+8NscLrO6y63{!03$3xb5`)I{1F{;dIvu3%( zw`Yar+c79;GEQhJbv#8wDa^2k^nfUK*lZZO4B}HcmUyEhB%5u}P>bFO2;>E+i0RKH zvqMV@8izN!P40TOdgI!@&XmiV5EH`msvYFHEMbfcRb*FpW;_?I)UD>59~JWEVA zif6WQLKZP?LhWY6u)kpe<>>{r`Gk009KA6ssx%uTH=_>zl>4N`F+gzJdM@wa(Dc|B zh2$|V(9*YhR*_Xb?Egm3WGURVO3{?!-SjH{|53l)vlGiwA33=gm2OcRB_GL^Xj6WS%_U`xIoKuXO2nLq(1Q{>Z;F zTbFjb-8G&3T{xH`JG)I(F8pO%$QE{+8N6R12vD#_bs#HTv*-?C?Pap0C^f4qti1n6 zP1f3C)>RAqd)+-$M+Y(4R_U=k%ImxJcIvn@HT4l3C=sGU>Fl_Tu5@mDw5<@KZZ_dQ-J~8c|DDZWnB=2F5R!Be!E15CXj6?qw{%|a5feX6;<+z zYQYu7_*WP|s1?g3<1!PL+Hps{x)ZLHZlVILj*Ka@QPbhMuHakRTM>@ED)!xHi8t+p zHDG{`L*ol3oOOo=7&E3oPl=xSUi^Bd8x%7~X{w$rJ9?hjT&}gtA<{k6MzBu2J1p|p zwb$)61yn+7{~MLnP|NG1jU1lsaU44y%Q~)H1r{20()E3!`llIOiEUN|1pb!b8GJ8D zHaZhniS1({ZWo3(cRYSH{nuwoPyQ+{ZBHC|{g?C$hu>y@q@v|<@BR|NYH(_@k{LF3 ziOvx?`9`}u8t69>>$|ftJ~#**ieH)3E{V7aV-ie~--~z>qlXI0J{gcbV;9+?7@zfw z6*Jik80!zogX{yr+#xOsI5LCnAhR9(KD^y@bA9$axjng$h{uBF0S($m;{LdmWh zLS$D-_iks+d`I72i@fVoeSj+y|6^#c_fR`U=R~sU$>$My-0YwB@j^_u(R0v5Sj}QZ z8guK>rIV~~<}Ri+PqWEH5SUYa9s=LYY#rr{{gR^VQxAxIsesy9O#~g2HiB&?({yX- z>o@`JD>yGrlxwW8@qF=J<{?23g>mnm74LB!48IX$bp^f9WF2FyYoeb32C zB>D7Y^lpj53?#fEgW?PWw*v%?Gko&|DlmaBz`r=ofg?n;$uB?io3NdimJYqJi4G<9 zTZTzV4}`@N(`OfQSc|Ck@oDdbA1^oJEnJI5)$U2nmzv3* z-%GbY*rCK3rrE1j*ct6g_baIvl;1i@1*9l&t z)q@bWR)^nqan7yE>+^fG^q@9#;81eC)&nL^^8uK;k5EP(m$bBj>;L!&)H>%Kuuh$J zmZ#n?gp+6%@t}Va66EGE`yuqz=yZT>Bq<#pg(0?wz`~}_T?5nu(@SIRh@l<9D{(nc z68Jp-`IY+=4zin6F-LKr5(k;t%X46pipLT9P&if<+y$#Y=A zXEuhzGc~D^lkMR3Xg~g;h%AZ;WWC9$aw4G+usl|js=P3nUbCZeBnTFL8ZFHY$!0B8 zlH=(#^Y;kpNLz6PW>%hJF9`likV|?I^yK&C1e*>c(F+Xgt_XI&SbdMeUIkwD zjWI6x-RDF2kZ^1~b>rce;a@2H$eL$J@m8P&=_a3Zw+P>+JIG@4uMz4pS^m5b!In^_ zS7lD|*JFR|WiB5PE#wmOknzwb8^~1>s~Smmh8_SVnMiAWWXA{?Xt>u4>8uzLqXE6H z5Z_-41mOld<)2bJO6{H^u}FZ!<`hSZA_U3aywd+vZ>aI)d_+H=DH z0W*}XF@vUqhM5+d?7mXo<%sER@Du_Ha!8*`h(XA!BG*3wy!+67gkE!3-;g^%mOj+$ z*9<9~XKuqT$Y3MK$1IbE}`%Kk>xhHw# zPz8r#H(XGk^m(QQ;*7}Eg=;*qE*W;g(O2e+aR_7m{oOFly#HXP4Yqg0KUL34(aoy3 z(#o&QzHH0m0%FZ$jxjy#k2>Sg0tl2w9{IPwmh4r3f0dGZy5(VRbN;afyA1~+i^Q=% z=Jc0Ak0SZTgC4;UM`xgkrj@l-f{AOE^kr@sNhxaRrBb~FCEf4*9*vxj`n!HQ=P(>X z+}NRR3{B=ReGuOhKs&_#6j9#m6f|hod9ACvmBipo}B z6;)Wr$Z|DEP7CoIho!TDmZ1f=akKMIYm{xh(M*3gqPxiOxU2idVJ}hRZiLdY z41t|OhRIk4={DXa#qv%D0K@SoiR-lu+nXVhR&BRNJ#2h2{~}5;$fQ5k3n6~jq9*d_ z_GGCp$<%xbAvbE2N&!tekxIzX&^(eB0wel6*S$C;Dq}3mWbc}y9GQ8n2hXU!F`11S zjNUxf$+VFOkoh~8Fuq9QSq%t!^pP3jSF2ul%^S6mIc%#`XdI?OhBk7whWN$n2wacMr$V?WboOu*wESV z^n#dtPuv!=!u#^v9!3lC?#;pVvyLCxb$)2ip~3nnENcRKCb8tKV<`se`H(-oRmsp% zOQ04PeT{O^aL)K`M7pHN1p|_y@t)zva zCa|ydfB-|;?5F8##Y*H?xQ3}EC+mbKcnq|YL(^N9x8@Bcp=2l4RUgtntscv!#07{E zxMxC+n2WCYmyA3dS;pCx=j$_j8P3QFx9es;bh?_gtUBT5JULNy%|^Z{;c(Nxa>}d2 z_#xS`Q&XqDr8t8YGVG?ulqoAMSwmo67*F90^C5T;@Sc0G@+^^l&)y~I`tFyrnN3!t zXFN0=MrraY@aDSQb+rZIoh&9?_M3yairaI>3pU%=%N=Db8KTb5k?XH%zzGbBbf9PZ z*${*Km38{Gcir7=+~J(heLqpI1~vML5A?^Ho~Muv6ETo!U=ZD;Z0Kt1$7|Q}D`5KQ=GH0MNzrC^3y9zvnnO-06EF?J$Ze}Py zP<16-AQQ{wwdK)6W2~jvA2bD&}JZ|4}0xxaj8Xz* zwNCr(r|aRydQv@&hSV1FGt4S$e2V-rmCb~NP2^7`tc?eU%i>hb?js@y_2<-D+>TYz z0^Q~XKiowo9E09CDR7^9luV}yyT5Ac#pnbh)OKwkKBub=Ni^PDL+~)QE z(jZwX`X%GT^q@;P>zXN+WblE4709_HkR78r%`6-VZ)y%@_UOs2%%IHzbf`Aiyk6qx zH4sG(+J;-o2chp>?V2X1G6z`KT7wf^Xi}3F?Mg)mCGJgd1_&bU3Os*2Gx6+@JiQ%O zxNUd_LvYfV68$&7B10NK*$S@0DJ4L$h&iT1@J<~u{K4Q{t-f89hRs}63XPI~ z$3Vxf67NEbxNDPHdzxwf$srm=GJM`Y|G|9Atu$y{_w zce^jNScr_RA3a?KROJo4RzhIyp4l@I?x2wH+^3f3R<+hP!hu!8PcbmALvG{#@Q#R# zsHdpOK%qW|m^CvDB_XhK8B6AtJ4KJ1Grs5u<6bT-W03092TImtnIj@uU5DP z1`v<_QP?Q4rv&H4uR!=-HOVRqY}oS#3wZ}T)INSEBT9g^d5%AoTzOZvIkSLMmiC`r zqM886l>ht0FgA-^GdXtPwt^%#Qv8xiUy`KaOL{ zil|hTF|0acCgb^#OsJx6Nxs z9WUUTr3h_>e6p%}JVTaGzK-Z`5NOCbPHrVVwOZW02pR7g;)1)sBsJi8Tl1}7ftMrJ z!;9Atx)_sj440!h6x7^`5G{_ z%>OPqUwPV4#-PCh!1S=RDrb>kXob~9XsX#f%uAF_^lR=9wQSypO}`P-yR|ReL5^NFUZC7uYhj6Uk+r<4zTN1lY4SIP-Xp^b$ggW z@1C9P%WeH!4hK26rY+t61DwU3pay;tXk`=;-%{+~Fxo!uIy|4qgbyc!@Vk6EU}1Dm zSX659*Tgee4`wczD(!(NN|3%D4YK#e#_Nb$x4RCeR(A z=wsX>M7-&7Z-$Dh(zO?HOLdI|W7u-YTQlxg^R5Gj0cF@tVyvO~=V}5ZbrG^5=XS@A zvS*V6svWU@0LtxIcu~SmjpGB_43#(s==_OZ1ez!)*dXrS<0?suIFA5v8g^8s!c+SB zMHUSRvC*EA^ZS@eiKE0*qQ@<6p6j<-Fx*7$D?(H~m*$blIoyxxJ}Ufgw_dF+v`%u7 z=38eqlR?`Q2Dhz#)LUY{^RGiqAB!Dmxbh--fkG4~J3DF$YcQzXawPg|Imxw@x4I?X zjZL@mtbZcERjI^SKzno|1Tim+cJrA*bl58K2$b5k5+JqkuR#E#;AuS9?^Rp?I;;eZ z99bn(C)^_MtBb?~hJb4e>XaNfOgh%+$!TuNI|cGEZ+q59tQ|pbOFFxVydNG<7d%bS zW`f5_)`Q~=7|H12-kh&j9tz(i3c$zk$O1}B|o26o2oH2h^q!qDDEqQ4M_ zI9P43Ea1UOz%BHG$r6!gbuaKY=Vu^Y{r|u~Bh{?0#v=hMK3DL=wO+|M zl9bl0Pj~mXBXq8L;oDf6EgY+PC#CUisx>_Dxq!PrPc zehQ)Gbc*jhfWx&-hzXhq{|0*5)@@>%R>1A>-n*-aqKv(by1m%Uf;)z)j}Baydre?^ z$bd)v1uZ29CKO6eFr~I31CzzgJ^58_GAvqE%*A$47d*Nl-sq3e_ec8xzPnb>L(`LV z7u^$rIK+!jtI-VxsJg^4Ly|&e3_X9nQKS&fQpmm4PtvOtJ0k`JbLdobfMw!h?)v^3 zY8T(Kqkq+DKSg7Y((Mubv5#P;MK4QVE?9o;GM?)WI>d~mN;a5&5#x_`iq$EgSiQTo z^2Va$0xZ+##0%RfFfao%={aR}XfuQ7#zR#KF%{+uA zGYE8 z6I*aK9i>wRZjo$U80TNXrvx9PmZUyZ?2u&vD~?g7cGVmiCQ%ZvkHj-pK_Ok2eVI0~ zi<84qj)KH)g@mH)8S8C$*|Tzq;vOe#n`DO zGYNwjgQn;bEvZ}JN{ZK3-tI;a4bjjLUVX{gPl`9iATbujSoA{raHM8NZlpM2sXF zS;tUG+=~80Bvt7WM|we(pM{K>$}@JW;@KiL`SYZK#`I@vIxJ)V`gcXcmcJ7l#0*SLU!~2uOZv1Ou8?oZrYo zcSQVl*_Fgob{xUeDBHuj8*mlu${}uS2;tlbQpRECOYbNAru+=&j2vY=tv;_IK)l=d zp4H#)!55kmD6Z6CQ`oTaRhwm_?72;4ql^1?>ckrpAA@c(^l3 zFz!Ga+>N~4Nq-#I-uqcPwwMLff`u-Ol)+t7EvQ!5id=Qjo3Y7--Q1}j0N@VBefbI} z6qNQQqiSeA&}8$NBYl`sPRsy$0H!zySfkAJ2_5?o=Vi!-Tedo3sjA#{jxO)T=L5c# z=#)}ECtI*n`?v)n~^w8ej|ZhC;geL6Y@{wQ-*_i%`C4o`&)YVm7RYqw;biD+?P51anZFX)wqgk8avCIjxAp6;1@FvJF~&%!Hs9t4t2_Spn-D~SBj2vuEQ48jw z)|@dH3jCP#32q-QNo!!g@i1a7YjAmU!xnhZKXb*Bkd;VJbv4)08zl>ZOG-4-P|h`p z3fWc=a_L>Pgsr0CN}+R8g+&F49T~MxOybl2*s?2#nk1U>ZU-Y{hG(LY$IDKev|Gsh zy;29VA`eyF{MUqUnqykzEdle zjo_VHjwM;TCyV;QWrIg7e<)Z}`q_&=?_wPfvw3A_6;{Zi896zDtP!XXm@FaXZJ+_X zBo%Aa9SA}q(~boMX8S_0q=lIp4xK~}zd5Kn_gxKBrA&*SiOfTnH({u9i?aPC1NN8x zXoUF$b^wEKDTm_D)c39r726j0c^GBY`mKj`uLBDksY&d2NGrR~h30Q`MtT5msj9LV zB5oEEs^C_j3hv-%%X01$nH^p^`)tF;H6D-8=Q)bE%^$}hAN}#sX$O6!q6HVpyThJA zf}cz;^tx&Klft^!(+F`;38i3VI2+u*h<#%|Q4ehIR%9*#2hP0b*d#ftt{i>5B6-C{ zU<3pyoo^@+`zrO-s?+p+GwFKL2S7jrGwsRvgK4`VbKburC2N}I7 zaYHfDk#VT@_Ub*tfU^u=SR0Ll3VXz0B_+*7&%3XH3;a);K>XUwNB>e)YIhMj%Ij7A&=@apGJkZ`hOgeABr8VwuLSL@OtWvzPI5gLz)Pxk=})vFJ(&H zmF}RD)?PL2KKPb}^Xc1&5SYw^4W!H(&ZVx((GhzBTz`czQXMZHFBPAM+H`vBVNPR| z(;Ol|7~}PoOpKv$_&Cm*@NXCD_1l>Ev>)WEUvJHvhkdJ`lysZsLhC_(ym~|o0mnz4 zehY3jP0MIdNB$;YU_ct(0tj}%7ls^_R1gQ*rS)8yK8siz8lqb+6~93GT~nQfwZxQ) zO0mYBVD2j~OCL27fZH3DeU{YLP&GlCxnVxvXwgmV8PkBZE6O83^bne&D<}$B zFuA5{#;?@*oVxy7MU6pc=A#^=v_wQ1jXKhnC_LeySZNyomsiLM?O5c#w%f0+)|94K zvHI*pF;UED#l)txomirW|5?RTh9fR%V%{;**1*a?aYz5Uhh~utJ8ZVeaUt2KWIt%Id#C_M1VHz>OC?fHW&j^DE`^T z>xX+6?fSVZe82j}3g}uj>u4@%>2AhopGc8I0GIA@3Vb|bR@AEkqdesa%0!r1>6%;& zI@=S1OXmfQ4o`Y#3#m-joG(FjlGC{7N-j4}L3QW{en|r^cR*=&1-xTZ;(QFRgdZE$ zGowHqN%bXG>&!$L*mPYu;=`|lH;VU&57D20%J?aM_H&pXLlWsCImb<`iT$zt&MU~5PK1;D{HEt-gOcU$izg8N?VD0*YJCeJ5!j&2e6%233?Cq|OoVktC{9Jz84 z{6@wlvn$*YJ?IhY?2`-rlf=oWs?-T(BOcauQ$M&m&}6Tqk5>B{J6hzgGo<%S6*=jp zm%}I4M{F~@bo~cjX{Z>HCAf$)twF?)k{;oVGc^&jnJ8zjup02d8YU?u)v9G70$>+T z-05tAh#^gfTaUbN4h;VQ4l|qR3B5Oxt9>7i3KSp-__>t7LPYF@Ui7J&Qa$mC5-|&G zX66E7OGu!^SP9IUro$C;4Njx%EExI$aI~hl)9RM268xnzc8A(?FGhIEvu3le;)w=r zpCu@hjoaeo4!4z-JS%gPqaLa2oC0?R&k04!(#1(fg+>RfPy%frt(cNU^Zjpy7%OtU zM7*Ql(1QgX^SMc#8lqNA+*q>jlUaZBNw({dQ260^y$a6D>VW?lRlp|<8?PIESXe`*-k*zK-9IzUE&Jv=V5`WW^@!lpB7o1) zNtwTkp2Oc|D@JsqK03m02!5W3N8jujLK~RHDHaL@<8yg@d+|Ov1AxUK_3zAM$*zIj z3pp|Q67w_o9p1c8d(>S{XA;{3c{pKx;m87$)Y6m}aq<_SxnUG(%W(vV!Xq|od+fsR zeGXVcFn`CX?F##haPISJS~8bSdNHVn8c8oqFVNOY5r~zhigsmJVd~>u{8Am`dt=~d zi%pTy{VLFBg56Xd)VK?qpSJ1|c(|#B?`;9#CHr17pCaHL_?z><*&5Lll_a`9qz4~e z|8m#Z6voDrbkBABtSo%NS0s`N4FeayJmQw`u3gS+cpQsBEF=C!Hbs;F>K$==CO*uN zY3m-;0Ad!NY5I#O3BF;wTRo($bq5N;P?gi=th;*}wMSURK(TfON(vF@c2C9@UE6ue z>P&WCTiHy;rW~pqD+DTf1FIctIr%%5nuN5t$OM`&`tz4_;Tpfi!9)i&i2ly9H=q=( z?S|ArTup{-${(jcUDcW)s8k#1chS8s5F+9w+LCfCp+U>JwR}Y| zL~2LVrU_0Ht1#0X5IT-3a%g~*n(8TPvt6iX%`DNk!q=y3(5%i;$IdT2)m!7m`EX2c zt5vG0V?{f4v8sQ&Rm$zUVRhzdKgA5TCAGSmGdRaKl(T32?LzN;OGTazxR5IDQhP{v zdp##a0mKBqo<78|?<|%@v7we5LMdL8{f!ESCg1@VPJJ)=>;ycTP37q=H&E-(`0~jM zk#SNjziK%~yUy1NGke?EMOual#fEdvfP?8pa6QR(-0h6Ne-CI8`Z|7t;OG*BV0B%# zX~awO$*LvH%OiU^j4V+&`nGP*LuAW*DlCw$RmyR*VoBVaQey#hbW5!Xa6a6qD#%e1 zLtbF+#9ELRrvKr-shxwf`-^YOxOL^0X$g!gs3FqSV34-IMQ`GP z>M1#Ww@aemRMps)SoXa$i_9@v%GOdLP{y509U`OE6EcB$p^ycgBI1WIGVM@ zLAyLrZrfSg*O6SB)MUWec^#$V72GTw5mZ2CSpUGF#MGrXG0(&CMP&ksHTV$`z|J?&joylC3NnoY<>#NSK9MaEg(Ta1WvW7U+ z=!^vV`0(1#A0Wvy_;=gUsi~wSZf~ZOd60sU=jnLcHlf~nPv+{&1G_jUzeu75`9Xw z!PcOZ4{5P)gNr~nSXAJvD#-ek!(|JHSN;S#==QD`EfZg_Bvclo=y5L{J6fZc-FgeP z5AzERp4U;`1NOHHIND#RnqyiencnDWpbvmayAyPEM>H~P3&~{P!2LPWp!y#tyU&qk zzHY8io+!>N@!mkkb*+~h!ZNPDGIt` zXpSCk)fos15YJ0SO#Bt?*pL!Nvq0tJvy8&CiFQ48sZbSv*{O*XUavbwx!=T7lEqL= zYaV(*-@{g80IKe`nNdk9SY^-gZFV5khcM>%3~j)c&Ugy%s;x?#&R0QZ>}dVuDe5(U z$v^m0%E7pYDnE+`!i{G2Nd=jtUS`i2CfSzPjR0r@R|)T$@xJFHNt#XN|()a>#F6di*Z8uRN8|1z$fZC%7A zZ}U1UPn-7Hs1fE2$;|Q#)q$|bcn>_$gmUHCt4@cb2`VGy#Waiq*^x^97?PUu_W?gi zjOnzMV0&?%(=XPBs7xk1Nq;dAJFVNH#llS==T9`ce;I`{54SuJWoUtWpXrnwzz?4bwv4AvQCvw;1b`U3fsa z#(^ZV=2MUspN7tpM{~yZefn-EWahr?hQa{rW7F<&rO$0lwV!}$Ivl6(_pwQ_d+OGK zKNim*&Gp2>ni+&ZFn+#WwN?1n(%u2P?}wKJhar|RDW)8c^_sM|=>6*E;j4!|Zf?UW>G5WWmO5-uA1D5ckgnCt z%J;kM>FF-zSGb*7%%ng>>eqqD{^JQnK8XZqZyq#B5Mv!+MzlK^;p9FKIVFo0L2s56 z9bs0{3aurusifTF8U3&Y5;0i$0xbbTirY~rOisO+-H4Vrnqy{DJxzXPem@e$tSO#+ zoaDOeBC0$8klcYAR406ypZ4`rA&_`iaGLY<4 znbh2AZib6%z>{>)s?R%|ohRJ+xUe)q)-o(&!BR%PgkmGno(5Jd6sv+Z-mpeQN-eC_ ziDY^<@(oJ*Ky@;d8R#oG-sA+_4aKJnm3jxJuCj{k+1+B-?hI{|svq7Alb)v^LFuB$ zznRm--;wb%<&&(8t%%zHT=hlj3?N~Y5&U%9YvZ#O3`#H_*^;Xk7pOM;55AFTAw>L^ zI#i$hayg<&Fw_9#J_Ln%@T+|CS2<({2{9eTYBpXV!*1=kEx7`DA)mmw8~?K~MWrk1cI-Z5kt4oDLg zJbfAH%X0d3=`<893V$zgwSVOV(8A%Q3)%cU7zya{;RzKNOECWiNte>SL<}=VuuTT@ z^NltKMFu^q7?{qCNH(!9v4j%nA;`sU(qY_b(Is23j7^(xLhM=x>UM_p;dI{;cS}#* z{q3n2c9Pgq%~+hgC|rKnY4Pe4y$ViNyK-{vLHe)n9g44l z1wTGnx=zzMWwdZE4A24$G!{kc%5JTGPtvAZagK~&A|MO>=P2+%C7HqO9$nnS6#!tr zGw5nMVle8p(rz}6%)sI8P}?NwA={8%F;f!~6IHbRpAJiKl0xf{*pdF@fGUxX3I%;3 zFxWCBjoOj!+UTxbfba;Dsc3L;9r<64}^h-!^ErroCBx;JK)79g-Qm-18Ie zNWAT)D(Fw5J?!a%$*U<3K&#a3GG!ujxMIL4hU?z-=95eA*TTMf4GoYrAXbHS0yHnqFES9<)4)W1iH-_7#LAl_w z124Mybf=QlLR-gTp(;7pbE`oyNB*#nF-8}9#dOK|!oiEVf=3a!MStg5HO98pgvrO( zRZG1YMuAnz;RjqFq@*bGP+^t`!r(KMaD_S=jS6+Y{_hHyG(fP{M>08FHMep<;Rg zG>!x>j!gFsVmIWI;p~G{{Wg7rTbls8#Ox90Rw|<#Q_5VvdI*mj;#RfkI4r ztAuNhji+@3o}7NoLJsr>!eUgIBvC3+C(~~_cSl)T^AON!wmyqFr?Ue2S#tt5ocol4 zGdRN(dQNI9@m;)@;J)c52N?vn@a{q^Ww!EX6NgED39C+g_Js$7g+6uIGQDOCFw?XX z_JuMFls-DwhifpMDzn&9THU}(v3ZnW{6zVbouzJ2F@pJM(4lb4p)81VwrJeqx-IHw zl%$(GfCz)Rgn*kOHsW$rtI$!oFrSr1H znQk#)A%nSvi&}YIyUEWo#LbWkFT~9RqFn|qL$$WIQ#ZJ_kS%9sfhslaIVz_(AyZTD z;G6<{#6)woH(WjvTf+D{lk-E|a6Q8nOgoPPW!qmsP}p@k!e#dB5R(~PcsU4jz8IT{ z06ez8DsFFrRr*Qs^Mg#d9V_M$l6ot*NC2oJlS*8sBIomTA5AJa((DzOk4dOt)&_Z_ zB(`uHy3Xmev&mtO=(#Hg@hnb>$&gc`Q|}^DdL^TixmG?ingKPw6d6DLu}VU@Q()s| zQ({Vj11bp|y2&?I$;WVB{nC~3#?V=0qwfZ5fs`YPi^=w%2zXNKy!i6J9_c#Fk8Gm4WTDq zYAV2iUsX94>&4}wpOkPSh*K5ud%C#T0Z@HTP~O|$pI_3U5094^nN4>b(J1E`;(elu zivcjq9HyX5#yfkkPqqsSmIS->`d@8-?S6~}zoc^KGnl51$-!Y!HGh(-U75Z8`T$EY z%DJV3Y?M7iE4!ZAL3ld_iT8PDtzW>HFiaB+xEQB@sJ`-+5f9H2cc;B3RJgx@>dOcZ zuVEv2+J-~Dm1(4K1%){@uaNr2=S|j&o%7;Xrgy(TLw(+wX2oL`{Vqc82Gwj{<+}9I zAI1XaVpWRf64?y-Qkwpvl|wc@+P8u7O*A^iOVe?Mv;1B*)BYPNOKO0#MkY1lpRae9 zt5jFX1Da+EfUoDmW#=z}{EKHk?}Fg;B1u`JR;5FE^D!bD-^5qQN?Vyb9#KjgGd0wE z2>+0Fh10HX#j4vl6WAwoWG4sKco}PW)PFv(Bll)MG4R#2(r=XWTR=%}6e| z*T9{|XyfSu<4}GAaFBC`iWZ`I%(CPS60REIg%NEZp$Y8)WwfO^nJs%hToSm;N|BRj z)z2DAfZYZ5?SrIA!!^8g^+H26JEGLYvixvi<^d)1nB8iB)(0NY_D$3yJpR1Ro?Y;n zqk_RA6x^<145PYsA8Q2>5@SaO5zE$OK?Gx-{JivHPWCHEh##L1e4V>!lg)2?OGmj= zxoIkHzwg!U)tmB{QF+<4_QP=9PIsd$bHMW2)Q(szjOyIhHmQ@2z9e}7I0y->b+OdV ziaMZmSAyToherSNC1)pg2e|>nt?(w#*a(E8lZ>6Sj`@{to^-OGjst5t@d;+_3=9RA zC^dcJ5WB|7L!6}pj?X6^FdChT=soZ>qMnnEI;B6O8Pu;(g21yJ*+{panY?Q|p!87> zTH9X>J6A-<*M`Kb;JhB7gr8^o+u6=>##G~ofYPus(Rk&qijoGk;~5${;v|E5Ot3)W z+Ner_1T+#K%?1IZ9!BkCo7-MpgPdZ(5rmTDyxmTh^ur%JywlQ*556m!2-+4F&!@#3 zzy4;U_rK7Tv|_e}&Oo?9KG^DW6-|y1k5Pl4>Ke3PG;gF1*LB*tP^01znoZkv;b<*2 zDucuF$J|rOp7bUWR!s-gvUGzZ`vF>3zJtMKIN*lGAEZ<7n`9(L78=AiMzbgM&4Hfm zVB@tG4@I#Gl7=kn31&KRn;sc!KFH8I&g$YX(VJKsDOpHB%=W@tJwJ)oZEO?|=n}#T z+s5j`FYuXMx|-{DpAJR1Nv}$8?bYxXk-NyE!Y|C^o!~^UBohhq;5bV)UWd0&(?Zyf zAcgRU*t*e&LMn6G9533g*Z~0^j5pv286HqxZSSd+x`X^zZ5 zYrFrBQq5N7wQ}^Yk*@o(89@GDObNh0LV(-&zVG6x-}Kbf zbYJyN&-|aS`L9;+Un$|gLcxC(1>>&?`I`Ug2mc-a{+j1hGVlr32XOs4DPqf-gWsvG{BHLV8O>g*GxO#%llsc-Zzi;FBrM zPSvCaRGc^Z6!Ak~$k!{RDN6%Z#o@H-P=OWKWGcvl(?dMttfNAsx5Hu6PynfoA#zia z%~v6%-E$TL?iM$SkVETJqFz@U8Gh>;(RC5P&Blj5X)LAbY0wLLPRyF=qagAe7e*(u z(_FDnMLI>pNwX7F{*>o$ZJ7A@@lgcn1OENtkF`S-xRSvwH&Jg`ZQB`WI11BzoZqZ7 zpZPe`5mqpr1MWo}#tx#TtvyxrefrE24bisIr6}pGk%wZ1%{B43JX}9r&xytCvIJe0 zT$vA-q!OY)Cwp{)h_0eNIbTCJh_zsnD&oL8Lv5KIUHy*^pXJl9c>}Mmoy7hf%-N&i zplY-=!UXZWf}$kDT~k1nd4AmLa-&s01~Gne%pg5^*@%GZ5CQeEqxGEhIlQEKam10h zyYLbl5U!nfr8QqS&}av~VFppPW<3%9Rws2p5Z&E+q7RdwEH=`vaitY7xfGZR^3mEB zU(8IQe5MO(?64wyR(1Tk(cIv4kv)ZpP7r}#<(=l<{4{qcUb>J!5(zI1i0YBzm+jVx zN$a#+i*Sprq?nFjckXqdt!A&q+(Y@>1?Q!Ye8XA&&_ATGak6tHUZZfb>Pu_3XxP2M zg$IUNK%6J^gUCAAzs_6nioyaW0hfMtqroE{o5pGx_O$VBI<2{Q%#~+il+(%Q3Z*dW zjcRo@r>Kpz$|)Fp973WKlzO52Ly+xoel(9{P?4eV4AZ74Y~0r2D07AdfRb6th_1%U z{L?UOYZg%D?Q~^Z((v9Bdi8~}#>#X^>0ypS;$b;5+Ck7RJgi{X+ z+(UdpWyr@o70xz%u*?UyNYg_(YsXn{nL8LlEk9<3&u5 z2moF`=k5n4p~XQNu+~RlOl2uTVrk3V$<+;)WekS!78rMzb?f`^hVKefCoS=7MhijH zPp5V$zYHt)cQL!*(G;%93;~KYQA&m^3Q2_G5u};J0mF97qpg}+}TfRiBUm4QeT);4h)ZFu}{(~&G-e|HP);F6O zr55WHGNKj1-VS>1Y>;odA#kr+1w82zAu)G1O5c`gA*+dGUNuy@+wBwRkOVcpJ=dNE z&P3U_bmF7oXZ~OO4Y$P|bN#Q9dj1d1XNH;qf6X129;=FLFB`(5dY4FQ1o1c?vSXi# z^)}r;!)341X!*0g3w*}NC)OjTapW@u+Yg6G)+hF*E)Yx_v_-M?($=w}i#p}R=1 zAnmgS*VMS;a$4ukcoXvFtpoX3qt%eZJ6aqE9jaaE(fsFN{LyF1$waAfZ0ic(>UfV< zy%9ZpL%sWPP>Z<`r2T1axxbZg9*8C?2)^WU7cnA*tAp{pDSj>nYfoILiRji;vo{5| zpX$omDEvNYvfGR0x!r%dVX5pQRtH8i?3Blce+?*eh_tZ-A&ypFNsMbip4VA+sd|Ij zAE6~3Vbv@{D4(kvDL1(b{1RzI`uT~Pj@i9{dEm(&3*&ko-l0v%JM2%@Brg;AWbz2U z>fh^U1HBcBmyyM&74ZVBj^fbUuyd9Ov;YLy9NVO~#q@PH`lnlPli&)%+x$b0Y~-=h zyBeN_ll&MEF`l=Zu$E582R)@(s_u(7SG*0e5+&Qhf<2GO@U?+fo{5-*vkigqv?p6X zYe@KrYB1g+z_Eeo*|Hz(Z?7#~-0mY|1nnWe&xrGThLoepMlqx~*q{=xESAOnp(%QF z0e_aFgSl~yLH9}-#YT>L`O^;KaZ$P--JX@@PTuxg=WS5UIF&=0Gba9 zi)PWplL%hi6ZOR{paBej)0YRj)@y~WqYfn)v5q&?9WVarCccza#wlK(Ne_Q zUO3#mm zdAFtv6-F(X+dR1s9vJD3R*RHM%CspDFcM%14|h-oMO-qHBzU*dw}ZG_{7q|&c)?_7 zRzq9}vH@-JkC3u*6EIPq6qwvQkQCm~8>)e%o(qNBe0Ac#hx56@nG2OLOO%zuqe7qS zF~`Dicl8GtQE3!in1PQNhaSEciPTC&dv56TQdnIEk;;S-VW1`?jvDR>kDI@FRv2-SCCt*f zuN8*;hqAW!O=^R&OcQ=+yg{Ec@UbgZD{qL+T*xRFLO?DRWuaB|!-~pwqep(j=|ZYN zrEdP77sD;jiwYcska99sxNm5`pVzS!XDn)DBYg^yTl2BCbzf$$Qc!_UgHk(`z7#BV zLY-V>vZVc zZ5+~{yGqGlR>`!`25hu)_aX!pR>5Fsa7o9pWs?_q0C;mBnW>SiwOdz?15$U>&DKDk zv7Z>wk;>fx^2((6$l(?}!F<2QM)h6drT!GR;xnqW;D}JIpA(CW>ruXl=EkntI96{F zDN6%^qpN$`TWf65OY=E;Mos(>4S?-JX8h}w%JzIOT zOAo49gw|wOesV=6A$XdK^c%T0yJh~3dRbhqw~JJvriKTl_?4FdyJGb*-WT}A<#n3y zHX0v!LM7;9YYHl~`$HmjVLWhD4??go>{Ky=Q>~$${iHhR{l)(CubDHeLD#TmI`9vU zt}NJK<$Y~&t(muJ+%jK%t8G!arH^$yG3njV>nRuW69w-2o#uhvKX^E*`z2cZa+l0H zd4?kUEAJ8rYW0a9f@mE_x%md*C}tl_C~amMqNXRHbaN3o)0%xJu1P5`-LWB>ahP&E zteW=L^(anApLX5n<<8@Tj$V3AX+SP}vml-RV6&`RC*ItzEC~mS!VtOsKRM<6ce-10-ta?RztV};#bmg z`rpWqsQ>t+jvZtBg+g40WxW>p5jr`Cf|ewPZlLj(I+JJAJhvCybyw%oRtR%```n71{kW`dum*j+Md_2uDxxbN_y%K znXPhR%mFpYF&7^(1cfRXY@_R%a6pUEGN|rhpgU~p0jSd`H~ra!2fqmEMeACGeTlvz z(ePk?32qulwRoCpm`Xodn0{n>12?k6U(QW-sM(tj?<~@&*F*Ne5>+`v7O%LrM!)9E zu7?xKf@{5+{%PSQCZ!iTF!YpPEM3|h(2B9_7g9reotGxH!qREckvgsMvi3>}R_=x* z(KGi?g4ai>TIySeSsSGR$9F?*i`4!h)Vk5A#b;VE|BBkr_RJ454*6#8DeDph2!_it zlT$IubxQ(9cEc`z%<1 zxXfWZWb0o;y4&k=l*kN(^LZDhb{gF?Hi%M1vf5r0WSd8@YX0y#Ct#3;wPU)aF+H#f z_>H{KL34S?3hP>ci8bNCKjWfGYH81|KqgI3+GBufneU6&=DD(I? zH*kt=BL)%)ZomFO4Von{`6bs23s3ovkq1~KD8airsp2iw?6*av3d)s+D&RbZr!6pY zkG7$Nzl@!BEH?KH)g*9g%Rn_}Djtue#|YN5MT&LLo~spZKA$~4TN31xcMZqLx7dtt zRvcGy`Uko%YQY>ft_-6aAC916wPU?HS>uEKvivFHD)$OD-G#0MKn`y@>zuSg_E104 z|0#TJ9{{1I4_*72bS)wtb$hZ|8R}!n>nm+r8slny6CADO85afe+PH>(K~+dB!~;=N z7F?YT(Sb^>ukJvyw4F#j--h=&n2W+PnY9WlDp?a$Ps_gVW*mQEoJyj;xuGbJj8tKM zY|0GcLLA<2kT&>x$>CD>s!Pq}mBIsIp%fi+K%lydl*J{u&Gyr>JNnb7kKx_s7ok4K z8O>?tIDlFqWs&cghe8C1o2L;}PxL|b*h}~Fj2}7k_TiNCjzmfY9s?-ZfIH~e;hnmad)o6E}`>M)UoWQfOhl>$ORs&~EJWB*P{tj8I+C^uL(i=+2T!@FkTNKUE94u4zrUr zgDrAetbDo`(V)p~8Ln-ewstmt#~{Ym(M!;((A)HVZ=u67*%i1of1Ad=WYSLq`5|Q( zq8JzF;Fw`*4E)dp2;HLow1XG5+m0rz7O^;ECjw70R?OyJk#Jn;A z)L+%8Xa9{r@z;FRMnqAA3|tnt_79Ha*Y0tX?G)ws^7=i z-5)b@pU@7`Mx|nuvs37AO2w&8>3Rpv{J_x>NQPQyoziPEQ|!aAWX@D%$HW4Hr6sze8xLPg+>$ymVa!a3l0r|=Pjk;v42UR?ooheW{ZfRzu=KCIqibcDH>98^ z%4BN*tq6FhC^bL3{`o!zClq2#Hq|Gz)#z&&R;$6Fn2J>&{P`{skjpiZ|gpJRb;}qMe3GSLPD9+O|kN992!4nN%W84Mi4%g&x_2~@QkG4eqMKoHJ~Pg_=9=g1fI8LR==}qVc=R6cLt&K~hg; ztKE^Kg!5P9t9NQKwt{NP!(=#)3znvrm|nW68LG&@`O0%%irtn~$iH9$F%6^KBpB{W z740*Do5+nS+?6F}hRv8kP_A0oO-rH2d$5`xLvFDVi7(h%TYN_D%(hpgeEe%}a6zQ( zs>h4Gmi2d>R)v%aA!n_kAz4p{Xl&U5VFF(si6zA!S?m4jiX!aG1|_W#tXd~vw}fpd z!qAP4E=A217q20#1)O`X6T_#zAUN$zcZW0?0x94FX!9bZBJSfdH^&)=b{!sh(t+#abuPIUWzj2Mj`1H3(G_8cUS z_VMN(?#8PuUAA(IF7zI-=j=_m2>y9)hFM9LXjU{~YY4A~r=3-?$;qrPIhflUd$d9T z5b6Y3e;1mTqe_bF+rBXpxCaujAA)d;&h^7Q$j~YKKzF~A!$K+**ZwW%hs zJ4BurJll!+Xs=mX*W9+2hY=Y6(+H-9th(2Q5}n)|Dp-nbF!mt^DAzXAWoDXwEq7m0 z5b0c$>MVF^U!eR#8YSRd$(*f&b&)bc1SMx&RnVDLI|UADS{N7K z%YAJ~^}#-4S(-x`x9nrW$03#C5p#VAMstqY+h@Jheh9Z%io2*zOK9-1I9TD@5^Gs@}Dr?~b9h{<1xfpVXo-{b@apT|GdeW4fN4Q|u3!&Suk z%AQ3X&Cr*ilO#Bxkzvp=Va%O`yRX-hYox>%lv zHr>-QoYg&Jh35vmijcLcc=ojtr0LGVusHA{a-^ApS0n@oC;?MvZ<$5>vqHA=0aSz3 zvpZM|4+6(v%5p`lsZ^N~aB~L> zo{Ra&`FB>KZKLx1=e1Fg9>RXrXy)JThGbDc8xO|}r_i(9o@>Vdfe3iJi0tc#_3j~iHv;*@!EvS& z;ET;r`W->E$VV2Y3%c*Ho#DpNX;d-bxjRvFkk>_yZ^g8$fqG_jHJJ;H?O3VBe~gN@ zlefp(1_}Di29A2@#)dRVS#9vxQJT_oAgEa9BwpUAx;3{$W$O-2jg7Aena9DCfySn- z@@xQIcRbg!+@uVhfG|X#`In~w+_a&tJP^uIu|#2Y1A@+Q`=wIuoxcjc?8Iq}YtjIT zwMcu93pULxO6ZvJLB(Hk_qJrpnNg{pb`74srF)1Q8H>{EZ+p>@@Ha#KaBx$r)j!%7%9WN zI8DEHShR?s3E`@$4mA=}w5BBua%y1+2LqkCAZB(+-h3&--+=WIM@B<%?3+#9IAE0v ztDhE$k@R33lx`s#1+hXT{^rkkIA)@j+@J``84OCsK|O>lKBsyqIMlxzZD`Ra(mo06 zwF`eD7RgJtG+%O^ouowv!?LolRgQ0^n`E#Pqi^ZFULzWTfKD@-a5ujs@>+YG;f&gy zk7#I42)1&DT7pYfoawI!z9QKNOFPo@FJ?#+=Rt-SH@ym!dfwuVa{K6=7I?)&^ zHJK{O%4CBPnn{)ZBnIPSU@3Ps#y`wY$PWaBBpB2b{V3ptW*A=!?WX1~eO!Vr}S% zQ^JXNt!=NE?x*f4SS}?tRr=D}buh)qrUwWYF&zZ{yW_$!!%R+4+q)!Ke_UB@GV@fA zS*fwTXX>V`s$#;eU{Tkh$~DqzfIx6MGMd!04R7CWu`Qo?%xHN95#=*OZ)r&;NLYf@ znM3DNcPT(C>F%id04X;k;AAOHv-vYV1p~oO;BmBDJy!FZBvNM7`jexpR>Vh3M~Qye zv>Xl#m6!#N;9;%K<5^EPR|G1~lQ&)v9Q~aTn!Y8Hs@CD-IRoRgTa8?xHZgUmvK;O} zB*y=Rs546g%1BX-CmDt`mA4oNt*nh<(~Q>6h*LEw_wT+b-6);;($@Zzf(K%ZWWT9T zWs_UqHMU&z#m-%QeFJ*<_)l!X)j7#5hrzLsP#+SK`)Jry^q+>>(8A#bZFOXX=Q(pPttA30d z125tNsJ9 zmdex;btlH*=}8cwjTAjDE*qg62uTH6-5SK+5UoVMVW&Wzd>Q`KrZ;19wvh;kQ(c_n zL=Nl1_e~es{COy~fBI_$&ez(xP*@LEx#|YGX z{BcU1WY>b%bjUQU-w2N~X_;21cdfICp#pxj9@C-n)QjMF z1@ft+#gq<*1<2ACvz144t10YY7U0(Ut1}C*z$y>O5kAk$xJs}9NZ<|K)Zk}U8LdD> zA2_d?D8^l_MR)S21J@C#g?35I;?>DRJph_A*#pHxD&(K{H5a2Fc<)u!*h%uY=%+r*aUccg})EzcCpyx)A(nVKa8S+BUwCW)lP!ZpPSzjM0z?z7;=we}B|j;o!% zss5pP1VMYTRrbc0)Q%^9{44p-IF@i1Rm%CNw<|Tj#zpgIW0L<+mUjrY(?E|Y%YvQ~ zGA{(lymT>NFEqnJqe#6lDFnCxb=WG^TS|2BeNe=m_e~jt zlZp~NW6bvMT?4{2dK{UV>wPTOoW=Gz$hE|DOk$2e;z2<(DgDhw8MVRQ=ol03aiy`a zen}!{=X^MiFhMBKitMLq9b{VL3+>}ptf#A$$o$mG!MhS4<%0vb+tDXi-;e(T6>DfP literal 0 HcmV?d00001 diff --git a/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4.media_info b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4.media_info new file mode 100644 index 0000000000..1aa25b4e6f --- /dev/null +++ b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4.media_info @@ -0,0 +1,40 @@ +bandwidth: 133663 +audio_info { + codec: "mp4a.40.2" + sampling_frequency: 44100 + time_scale: 44100 + num_channels: 2 + decoder_config: "\022\020V\345\000" +} +init_range { + begin: 0 + end: 1006 +} +index_range { + begin: 1007 + end: 1006 +} +media_file_name: "bear-640x360-audio.mp4" +media_duration_seconds: 2.76317453 +reference_time_scale: 44100 +container_type: CONTAINER_MP4 +protected_content { + default_key_id: "1234567890123456" + content_protection_entry { + uuid: "1077efec-c0b2-4d02-ace3-3c1e52e2fb4b" + pssh: "\000\000\0004pssh\001\000\000\000\020w\357\354\300\262M\002\254\343<\036R\342\373K\000\000\000\0011234567890123456\000\000\000\000" + } + protection_scheme: "cenc" +} +subsegment_ranges { + begin: 1007 + end: 18034 +} +subsegment_ranges { + begin: 18035 + end: 34716 +} +subsegment_ranges { + begin: 34717 + end: 44575 +} diff --git a/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/output.mpd b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/output.mpd new file mode 100644 index 0000000000..866f0cb8b8 --- /dev/null +++ b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/output.mpd @@ -0,0 +1,22 @@ + + + + + + + + AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== + + + + bear-640x360-audio.mp4 + + + + + + + + + + diff --git a/packager/media/event/mpd_notify_muxer_listener.cc b/packager/media/event/mpd_notify_muxer_listener.cc index 421f0454dd..40d1b08ccb 100644 --- a/packager/media/event/mpd_notify_muxer_listener.cc +++ b/packager/media/event/mpd_notify_muxer_listener.cc @@ -142,6 +142,7 @@ void MpdNotifyMuxerListener::OnMediaEnd(const MediaRanges& media_ranges, DCHECK(media_info_); if (!internal::SetVodInformation(media_ranges, duration_seconds, + mpd_notifier_->use_segment_list(), media_info_.get())) { LOG(ERROR) << "Failed to generate VOD information from input."; return; diff --git a/packager/media/event/mpd_notify_muxer_listener_unittest.cc b/packager/media/event/mpd_notify_muxer_listener_unittest.cc index 219fac1826..5ef272e02b 100644 --- a/packager/media/event/mpd_notify_muxer_listener_unittest.cc +++ b/packager/media/event/mpd_notify_muxer_listener_unittest.cc @@ -68,6 +68,19 @@ class MpdNotifyMuxerListenerTest : public ::testing::TestWithParam { mpd_options.dash_profile = DashProfile::kOnDemand; // On-demand profile should be static. mpd_options.mpd_type = MpdType::kStatic; + mpd_options.mpd_params.use_segment_list = false; + notifier_.reset(new MockMpdNotifier(mpd_options)); + listener_.reset( + new MpdNotifyMuxerListener(notifier_.get())); + } + + + void SetupForVodSegmentList() { + MpdOptions mpd_options; + mpd_options.dash_profile = DashProfile::kOnDemand; + // On-demand profile should be static. + mpd_options.mpd_type = MpdType::kStatic; + mpd_options.mpd_params.use_segment_list = true; notifier_.reset(new MockMpdNotifier(mpd_options)); listener_.reset( new MpdNotifyMuxerListener(notifier_.get())); @@ -78,6 +91,7 @@ class MpdNotifyMuxerListenerTest : public ::testing::TestWithParam { mpd_options.dash_profile = DashProfile::kLive; // Live profile can be static or dynamic. mpd_options.mpd_type = GetParam(); + mpd_options.mpd_params.use_segment_list = false; notifier_.reset(new MockMpdNotifier(mpd_options)); listener_.reset(new MpdNotifyMuxerListener(notifier_.get())); } @@ -123,6 +137,28 @@ TEST_F(MpdNotifyMuxerListenerTest, VodClearContent) { FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); } + +TEST_F(MpdNotifyMuxerListenerTest, VodClearContentSegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options; + SetDefaultMuxerOptions(&muxer_options); + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + listener_->OnMediaStart(muxer_options, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + EXPECT_CALL(*notifier_, NotifyNewContainer( + ExpectMediaInfoEq(kExpectedDefaultMediaInfoSubsegmentRange), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + // default_key_id and pssh are converted to string because when std::equal // compares a negative char and uint8_t > 127, it considers them not equal. MATCHER_P4(ProtectedContentEq, uuid, name, default_key_id, pssh, "") { @@ -190,6 +226,49 @@ TEST_F(MpdNotifyMuxerListenerTest, VodEncryptedContent) { FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); } + +TEST_F(MpdNotifyMuxerListenerTest, VodEncryptedContentSegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options; + SetDefaultMuxerOptions(&muxer_options); + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + + const std::vector default_key_id( + kDefaultKeyId, kDefaultKeyId + arraysize(kDefaultKeyId) - 1); + + const std::string kExpectedMediaInfo = + std::string(kExpectedDefaultMediaInfoSubsegmentRange) + + "protected_content {\n" + " protection_scheme: 'cenc'\n" + " content_protection_entry {\n" + " uuid: '00010203-0405-0607-0809-0a0b0c0d0e0f'\n" + " pssh: '" + std::string(kExpectedDefaultPsshBox) + "'\n" + " }\n" + " default_key_id: 'defaultkeyid'\n" + " include_mspr_pro: 1\n" + "}\n"; + + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + + std::vector iv(kBogusIv, kBogusIv + arraysize(kBogusIv)); + listener_->OnEncryptionInfoReady(kInitialEncryptionInfo, FOURCC_cenc, + default_key_id, iv, + GetDefaultKeySystemInfo()); + + listener_->OnMediaStart(muxer_options, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + EXPECT_CALL(*notifier_, + NotifyNewContainer(ExpectMediaInfoEq(kExpectedMediaInfo), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + // Verify that calling OnSampleDurationReady() sets the frame duration in the // media info, and the media info gets passed to NotifyNewContainer() with // frame_duration == sample_duration. @@ -223,6 +302,57 @@ TEST_F(MpdNotifyMuxerListenerTest, VodOnSampleDurationReady) { "container_type: 1\n" "media_file_name: 'test_output_file_name.mp4'\n" "media_duration_seconds: 10.5\n"; + + const uint32_t kReferenceTimeScale = 1111u; // Should match the protobuf. + + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + listener_->OnMediaStart(muxer_options, *video_stream_info, + kReferenceTimeScale, MuxerListener::kContainerMp4); + listener_->OnSampleDurationReady(kSampleDuration); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + EXPECT_CALL(*notifier_, + NotifyNewContainer(ExpectMediaInfoEq(kExpectedMediaInfo), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + + +TEST_F(MpdNotifyMuxerListenerTest, VodOnSampleDurationReadySegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options; + SetDefaultMuxerOptions(&muxer_options); + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + const uint32_t kSampleDuration = 1234u; + const char kExpectedMediaInfo[] = + "video_info {\n" + " frame_duration: 1234\n" // Should match the constant above. + " codec: 'avc1.010101'\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 120\n" + "}\n" + "index_range {\n" + " begin: 121\n" + " end: 221\n" + "}\n" + "reference_time_scale: 1111\n" + "container_type: 1\n" + "media_file_name: 'test_output_file_name.mp4'\n" + "media_duration_seconds: 10.5\n" + "subsegment_ranges {\n" + " begin: 222\n" + " end: 9999\n" + "}\n"; const uint32_t kReferenceTimeScale = 1111u; // Should match the protobuf. EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); @@ -278,6 +408,44 @@ TEST_F(MpdNotifyMuxerListenerTest, VodOnNewSegment) { FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); } +TEST_F(MpdNotifyMuxerListenerTest, VodOnNewSegmentSegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options; + SetDefaultMuxerOptions(&muxer_options); + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + + const uint64_t kStartTime1 = 0u; + const uint64_t kDuration1 = 1000u; + const uint64_t kSegmentFileSize1 = 29812u; + const uint64_t kStartTime2 = 1001u; + const uint64_t kDuration2 = 3787u; + const uint64_t kSegmentFileSize2 = 83743u; + + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + EXPECT_CALL(*notifier_, NotifyNewSegment(_, _, _, _)).Times(0); + listener_->OnMediaStart(muxer_options, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + listener_->OnNewSegment("", kStartTime1, kDuration1, kSegmentFileSize1); + listener_->OnCueEvent(kStartTime2, "dummy cue data"); + listener_->OnNewSegment("", kStartTime2, kDuration2, kSegmentFileSize2); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + InSequence s; + EXPECT_CALL(*notifier_, NotifyNewContainer( + ExpectMediaInfoEq(kExpectedDefaultMediaInfoSubsegmentRange), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, + NotifyNewSegment(_, kStartTime1, kDuration1, kSegmentFileSize1)); + EXPECT_CALL(*notifier_, NotifyCueEvent(_, kStartTime2)); + EXPECT_CALL(*notifier_, + NotifyNewSegment(_, kStartTime2, kDuration2, kSegmentFileSize2)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + // Verify the event handling with multiple files, i.e. multiple OnMediaStart and // OnMediaEnd calls. TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFiles) { @@ -341,6 +509,68 @@ TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFiles) { FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); } + +TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFilesSegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options1; + SetDefaultMuxerOptions(&muxer_options1); + muxer_options1.output_file_name = "test_output1.mp4"; + MuxerOptions muxer_options2 = muxer_options1; + muxer_options2.output_file_name = "test_output2.mp4"; + + MediaInfo expected_media_info1 = + ConvertToMediaInfo(kExpectedDefaultMediaInfoSubsegmentRange); + expected_media_info1.set_media_file_name("test_output1.mp4"); + MediaInfo expected_media_info2 = expected_media_info1; + expected_media_info2.set_media_file_name("test_output2.mp4"); + + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + + const uint64_t kStartTime1 = 0u; + const uint64_t kDuration1 = 1000u; + const uint64_t kSegmentFileSize1 = 29812u; + const uint64_t kStartTime2 = 1001u; + const uint64_t kDuration2 = 3787u; + const uint64_t kSegmentFileSize2 = 83743u; + + // Expectation for first file before OnMediaEnd. + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + EXPECT_CALL(*notifier_, NotifyNewSegment(_, _, _, _)).Times(0); + listener_->OnMediaStart(muxer_options1, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + listener_->OnNewSegment("", kStartTime1, kDuration1, kSegmentFileSize1); + listener_->OnCueEvent(kStartTime2, "dummy cue data"); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + // Expectation for first file OnMediaEnd. + InSequence s; + EXPECT_CALL(*notifier_, + NotifyNewContainer(EqualsProto(expected_media_info1), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, + NotifyNewSegment(_, kStartTime1, kDuration1, kSegmentFileSize1)); + EXPECT_CALL(*notifier_, NotifyCueEvent(_, kStartTime2)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); + + // Expectation for second file before OnMediaEnd. + listener_->OnMediaStart(muxer_options2, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + listener_->OnNewSegment("", kStartTime2, kDuration2, kSegmentFileSize2); + + // Expectation for second file OnMediaEnd. + EXPECT_CALL(*notifier_, + NotifyMediaInfoUpdate(_, EqualsProto(expected_media_info2))); + EXPECT_CALL(*notifier_, + NotifyNewSegment(_, kStartTime2, kDuration2, kSegmentFileSize2)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + // Live without key rotation. Note that OnEncryptionInfoReady() is called before // OnMediaStart() but no more calls. TEST_P(MpdNotifyMuxerListenerTest, LiveNoKeyRotation) { diff --git a/packager/media/event/muxer_listener_factory.cc b/packager/media/event/muxer_listener_factory.cc index 820c540858..287a936f96 100644 --- a/packager/media/event/muxer_listener_factory.cc +++ b/packager/media/event/muxer_listener_factory.cc @@ -25,11 +25,12 @@ namespace { const char kMediaInfoSuffix[] = ".media_info"; std::unique_ptr CreateMediaInfoDumpListenerInternal( - const std::string& output) { + const std::string& output, + bool use_segment_list) { DCHECK(!output.empty()); std::unique_ptr listener( - new VodMediaInfoDumpMuxerListener(output + kMediaInfoSuffix)); + new VodMediaInfoDumpMuxerListener(output + kMediaInfoSuffix, use_segment_list)); return listener; } @@ -80,11 +81,13 @@ std::list> CreateHlsListenersInternal( } // namespace MuxerListenerFactory::MuxerListenerFactory(bool output_media_info, + bool use_segment_list, MpdNotifier* mpd_notifier, hls::HlsNotifier* hls_notifier) : output_media_info_(output_media_info), mpd_notifier_(mpd_notifier), - hls_notifier_(hls_notifier) {} + hls_notifier_(hls_notifier), + use_segment_list_(use_segment_list) {} std::unique_ptr MuxerListenerFactory::CreateListener( const StreamData& stream) { @@ -103,7 +106,8 @@ std::unique_ptr MuxerListenerFactory::CreateListener( new CombinedMuxerListener); if (output_media_info_) { combined_listener->AddListener( - CreateMediaInfoDumpListenerInternal(stream.media_info_output)); + CreateMediaInfoDumpListenerInternal(stream.media_info_output, + use_segment_list_)); } if (mpd_notifier_ && !stream.hls_only) { diff --git a/packager/media/event/muxer_listener_factory.h b/packager/media/event/muxer_listener_factory.h index 841d42e995..fd7e384fdc 100644 --- a/packager/media/event/muxer_listener_factory.h +++ b/packager/media/event/muxer_listener_factory.h @@ -58,11 +58,15 @@ class MuxerListenerFactory { /// Create a new muxer listener. /// @param output_media_info must be true for the combined listener to include /// a media info dump listener. + /// @param use_segment_list is set when mpd_notifier_ is null and + /// --output_media_info is set. If mpd_notifer is non-null, this value + /// is the same as mpd_notifier->use_segment_list(). /// @param mpd_notifer must be non-null for the combined listener to include a /// mpd listener. /// @param hls_notifier must be non-null for the combined listener to include /// an HLS listener. MuxerListenerFactory(bool output_media_info, + bool use_segment_list, MpdNotifier* mpd_notifier, hls::HlsNotifier* hls_notifier); @@ -81,6 +85,9 @@ class MuxerListenerFactory { MpdNotifier* mpd_notifier_; hls::HlsNotifier* hls_notifier_; + /// This is set when mpd_notifier_ is NULL and --output_media_info is set. + bool use_segment_list_; + // A counter to track which stream we are on. int stream_index_ = 0; }; diff --git a/packager/media/event/muxer_listener_internal.cc b/packager/media/event/muxer_listener_internal.cc index 0ef27cff27..336737c1c3 100644 --- a/packager/media/event/muxer_listener_internal.cc +++ b/packager/media/event/muxer_listener_internal.cc @@ -251,6 +251,7 @@ bool IsMediaInfoCompatible(const MediaInfo& media_info1, bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges, float duration_seconds, + bool use_segment_lists, MediaInfo* media_info) { DCHECK(media_info); @@ -270,6 +271,12 @@ bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges, media_info->mutable_index_range()); } + if (use_segment_lists) { + for (const auto& range : media_ranges.subsegment_ranges) { + SetRange(range.start, range.end, media_info->add_subsegment_ranges()); + } + } + media_info->set_media_duration_seconds(duration_seconds); return true; diff --git a/packager/media/event/muxer_listener_internal.h b/packager/media/event/muxer_listener_internal.h index a803928b3e..ab250904a9 100644 --- a/packager/media/event/muxer_listener_internal.h +++ b/packager/media/event/muxer_listener_internal.h @@ -42,6 +42,7 @@ bool IsMediaInfoCompatible(const MediaInfo& media_info1, /// @return true on success, false otherwise. bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges, float duration_seconds, + bool use_segment_list, MediaInfo* media_info); /// @param protection_scheme specifies the protection scheme: 'cenc', 'cens', diff --git a/packager/media/event/muxer_listener_test_helper.h b/packager/media/event/muxer_listener_test_helper.h index ca3a15d872..d1c49a4d16 100644 --- a/packager/media/event/muxer_listener_test_helper.h +++ b/packager/media/event/muxer_listener_test_helper.h @@ -43,6 +43,33 @@ const char kExpectedDefaultMediaInfo[] = "container_type: 1\n" "media_file_name: 'test_output_file_name.mp4'\n" "media_duration_seconds: 10.5\n"; + +const char kExpectedDefaultMediaInfoSubsegmentRange[] = + "video_info {\n" + " codec: 'avc1.010101'\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 120\n" + "}\n" + "index_range {\n" + " begin: 121\n" + " end: 221\n" + "}\n" + "reference_time_scale: 1000\n" + "container_type: 1\n" + "media_file_name: 'test_output_file_name.mp4'\n" + "media_duration_seconds: 10.5\n" + "subsegment_ranges {\n" + " begin: 222\n" + " end: 9999\n" + "}\n"; + const uint32_t kDefaultReferenceTimeScale = 1000u; // Struct that gets passed for to CreateVideoStreamInfo() to create a diff --git a/packager/media/event/vod_media_info_dump_muxer_listener.cc b/packager/media/event/vod_media_info_dump_muxer_listener.cc index e1f59c4948..2259408f7b 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener.cc +++ b/packager/media/event/vod_media_info_dump_muxer_listener.cc @@ -22,8 +22,9 @@ namespace shaka { namespace media { VodMediaInfoDumpMuxerListener::VodMediaInfoDumpMuxerListener( - const std::string& output_file_path) - : output_file_name_(output_file_path) {} + const std::string& output_file_path, bool use_segment_list) + : output_file_name_(output_file_path), + use_segment_list_(use_segment_list) {} VodMediaInfoDumpMuxerListener::~VodMediaInfoDumpMuxerListener() {} @@ -78,7 +79,7 @@ void VodMediaInfoDumpMuxerListener::OnMediaEnd(const MediaRanges& media_ranges, float duration_seconds) { DCHECK(media_info_); if (!internal::SetVodInformation(media_ranges, duration_seconds, - media_info_.get())) { + use_segment_list_, media_info_.get())) { LOG(ERROR) << "Failed to generate VOD information from input."; return; } diff --git a/packager/media/event/vod_media_info_dump_muxer_listener.h b/packager/media/event/vod_media_info_dump_muxer_listener.h index 19044d472b..eea91e483c 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener.h +++ b/packager/media/event/vod_media_info_dump_muxer_listener.h @@ -27,7 +27,7 @@ namespace media { class VodMediaInfoDumpMuxerListener : public MuxerListener { public: - VodMediaInfoDumpMuxerListener(const std::string& output_file_name); + VodMediaInfoDumpMuxerListener(const std::string& output_file_name, bool use_segment_list); ~VodMediaInfoDumpMuxerListener() override; /// @name MuxerListener implementation overrides. @@ -63,6 +63,8 @@ class VodMediaInfoDumpMuxerListener : public MuxerListener { static bool WriteMediaInfoToFile(const MediaInfo& media_info, const std::string& output_file_path); + void set_use_segment_list(bool value) {use_segment_list_ = value;} + private: std::string output_file_name_; std::unique_ptr media_info_; @@ -74,6 +76,8 @@ class VodMediaInfoDumpMuxerListener : public MuxerListener { std::vector default_key_id_; std::vector key_system_info_; + bool use_segment_list_ = false; + DISALLOW_COPY_AND_ASSIGN(VodMediaInfoDumpMuxerListener); }; diff --git a/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc b/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc index 9e74b19113..a284dd1786 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc +++ b/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc @@ -71,7 +71,12 @@ class VodMediaInfoDumpMuxerListenerTest : public ::testing::Test { DLOG(INFO) << "Created temp file: " << temp_file_path_.value(); listener_.reset(new VodMediaInfoDumpMuxerListener(temp_file_path_ - .AsUTF8Unsafe())); + .AsUTF8Unsafe(),false)); + + } + + void SetSegmentListFlag() { + listener_->set_use_segment_list(true); } void TearDown() override { @@ -228,6 +233,7 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, CheckPixelWidthAndHeightSet) { "container_type: 1\n" "media_file_name: 'test_output_file_name.mp4'\n" "media_duration_seconds: 10.5\n"; + EXPECT_THAT(temp_file_path_.AsUTF8Unsafe(), FileContentEqualsProto(kExpectedProtobufOutput)); } @@ -274,5 +280,47 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, CheckBandwidth) { FileContentEqualsProto(kExpectedProtobufOutput)); } +// Equivalent tests with segment list flag on which writes subsegment ranges +// to media info files + +TEST_F(VodMediaInfoDumpMuxerListenerTest, UnencryptedStream_Normal_SegmentList) { + SetSegmentListFlag(); + std::shared_ptr stream_info = + CreateVideoStreamInfo(GetDefaultVideoStreamInfoParams()); + + FireOnMediaStartWithDefaultMuxerOptions(*stream_info, !kEnableEncryption); + OnMediaEndParameters media_end_param = GetDefaultOnMediaEndParams(); + FireOnMediaEndWithParams(media_end_param); + + const char kExpectedProtobufOutput[] = + "bandwidth: 0\n" + "video_info {\n" + " codec: 'avc1.010101'\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 120\n" + "}\n" + "index_range {\n" + " begin: 121\n" + " end: 221\n" + "}\n" + "reference_time_scale: 1000\n" + "container_type: 1\n" + "media_file_name: 'test_output_file_name.mp4'\n" + "media_duration_seconds: 10.5\n" + "subsegment_ranges {\n" + " begin: 222\n" + " end: 9999\n" + "}\n"; + EXPECT_THAT(temp_file_path_.AsUTF8Unsafe(), + FileContentEqualsProto(kExpectedProtobufOutput)); +} + } // namespace media } // namespace shaka diff --git a/packager/mpd/base/media_info.proto b/packager/mpd/base/media_info.proto index 5e2f8619ff..d449461b2a 100644 --- a/packager/mpd/base/media_info.proto +++ b/packager/mpd/base/media_info.proto @@ -172,6 +172,7 @@ message MediaInfo { optional Range init_range = 6; optional Range index_range = 7; optional string media_file_name = 8; + repeated Range subsegment_ranges = 23; // END VOD only. // VOD and static LIVE. diff --git a/packager/mpd/base/mpd_notifier.h b/packager/mpd/base/mpd_notifier.h index 6576b7c625..22f3c80b9f 100644 --- a/packager/mpd/base/mpd_notifier.h +++ b/packager/mpd/base/mpd_notifier.h @@ -113,6 +113,11 @@ class MpdNotifier { /// @return The mpd type for this object. MpdType mpd_type() const { return mpd_options_.mpd_type; } + /// @return The value of dash_force_segment_list flag + bool use_segment_list() const { + return mpd_options_.mpd_params.use_segment_list; + } + private: const MpdOptions mpd_options_; diff --git a/packager/mpd/base/representation.cc b/packager/mpd/base/representation.cc index 3d17472fcb..0616540530 100644 --- a/packager/mpd/base/representation.cc +++ b/packager/mpd/base/representation.cc @@ -265,7 +265,9 @@ base::Optional Representation::GetXml() { } if (HasVODOnlyFields(media_info_) && - !representation.AddVODOnlyInfo(media_info_)) { + !representation.AddVODOnlyInfo( + media_info_, mpd_options_.mpd_params.use_segment_list, + mpd_options_.mpd_params.target_segment_duration)) { LOG(ERROR) << "Failed to add VOD info."; return base::nullopt; } diff --git a/packager/mpd/base/xml/xml_node.cc b/packager/mpd/base/xml/xml_node.cc index ff504f0d2c..c8ebb1a74b 100644 --- a/packager/mpd/base/xml/xml_node.cc +++ b/packager/mpd/base/xml/xml_node.cc @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -376,57 +377,83 @@ bool RepresentationXmlNode::AddAudioInfo(const AudioInfo& audio_info) { AddAudioSamplingRateInfo(audio_info); } -bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) { - const bool use_segment_list_text = +bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info, + bool use_segment_list, + double target_segment_duration) { + const bool use_single_segment_url_with_media = media_info.has_text_info() && media_info.has_presentation_time_offset(); - if (media_info.has_media_file_url() && !use_segment_list_text) { + if (media_info.has_media_file_url() && !use_single_segment_url_with_media) { XmlNode base_url("BaseURL"); base_url.SetContent(media_info.media_file_url()); RCHECK(AddChild(std::move(base_url))); } - const bool need_segment_base = - media_info.has_index_range() || media_info.has_init_range() || - (media_info.has_reference_time_scale() && !media_info.has_text_info()); - DCHECK(!need_segment_base || !use_segment_list_text); + const bool need_segment_base_or_list = + use_segment_list || media_info.has_index_range() || + media_info.has_init_range() || + (media_info.has_reference_time_scale() && !media_info.has_text_info()) || + use_single_segment_url_with_media; - if (need_segment_base || use_segment_list_text) { - XmlNode child(need_segment_base ? "SegmentBase" : "SegmentList"); - if (media_info.has_index_range()) { - RCHECK(child.SetStringAttribute("indexRange", - RangeToString(media_info.index_range()))); - } - - if (media_info.has_reference_time_scale()) { - RCHECK(child.SetIntegerAttribute("timescale", - media_info.reference_time_scale())); - } - - if (media_info.has_presentation_time_offset()) { - RCHECK(child.SetIntegerAttribute("presentationTimeOffset", - media_info.presentation_time_offset())); - } - - if (media_info.has_init_range()) { - XmlNode initialization("Initialization"); - RCHECK(initialization.SetStringAttribute( - "range", RangeToString(media_info.init_range()))); - - RCHECK(child.AddChild(std::move(initialization))); - } - - if (use_segment_list_text) { - XmlNode media_url("SegmentURL"); - RCHECK( - media_url.SetStringAttribute("media", media_info.media_file_url())); - RCHECK(child.AddChild(std::move(media_url))); - } - - RCHECK(AddChild(std::move(child))); + if (!need_segment_base_or_list) { + return true; } + XmlNode child(use_segment_list || use_single_segment_url_with_media + ? "SegmentList" + : "SegmentBase"); + + // Forcing SegmentList for longer audio causes sidx atom to not be + // generated, therefore indexRange is not added to MPD if flag is set. + if (media_info.has_index_range() && !use_segment_list) { + RCHECK(child.SetStringAttribute("indexRange", + RangeToString(media_info.index_range()))); + } + + if (media_info.has_reference_time_scale()) { + RCHECK(child.SetIntegerAttribute("timescale", + media_info.reference_time_scale())); + + if (use_segment_list && !use_single_segment_url_with_media) { + const uint64_t duration_seconds = static_cast( + floor(target_segment_duration * media_info.reference_time_scale())); + RCHECK(child.SetIntegerAttribute("duration", duration_seconds)); + } + } + + if (media_info.has_presentation_time_offset()) { + RCHECK(child.SetIntegerAttribute("presentationTimeOffset", + media_info.presentation_time_offset())); + } + + if (media_info.has_init_range()) { + XmlNode initialization("Initialization"); + RCHECK(initialization.SetStringAttribute( + "range", RangeToString(media_info.init_range()))); + + RCHECK(child.AddChild(std::move(initialization))); + } + + if (use_single_segment_url_with_media) { + XmlNode media_url("SegmentURL"); + RCHECK(media_url.SetStringAttribute("media", media_info.media_file_url())); + RCHECK(child.AddChild(std::move(media_url))); + } + + // Since the SegmentURLs here do not have a @media element, + // BaseURL element is mapped to the @media attribute. + if (use_segment_list) { + for (const Range& subsegment_range : media_info.subsegment_ranges()) { + XmlNode subsegment("SegmentURL"); + RCHECK(subsegment.SetStringAttribute("mediaRange", + RangeToString(subsegment_range))); + + RCHECK(child.AddChild(std::move(subsegment))); + } + } + + RCHECK(AddChild(std::move(child))); return true; } diff --git a/packager/mpd/base/xml/xml_node.h b/packager/mpd/base/xml/xml_node.h index a306fdfed7..b048d39627 100644 --- a/packager/mpd/base/xml/xml_node.h +++ b/packager/mpd/base/xml/xml_node.h @@ -205,8 +205,15 @@ class RepresentationXmlNode : public RepresentationBaseXmlNode { /// Adds fields that are specific to VOD. This ignores @a media_info fields /// for Live. /// @param media_info is a MediaInfo with VOD information. + /// @param use_segment_list is a param that instructs the xml writer to + /// use SegmentList instead of SegmentBase. + /// @param target_segment_duration is a param that specifies the target + // duration of media segments. This is only used when use_segment_list + // is true. /// @return true on success, false otherwise. - bool AddVODOnlyInfo(const MediaInfo& media_info) WARN_UNUSED_RESULT; + bool AddVODOnlyInfo(const MediaInfo& media_info, + bool use_segment_list, + double target_segment_duration) WARN_UNUSED_RESULT; /// @param segment_infos is a set of SegmentInfos. This method assumes that /// SegmentInfos are sorted by its start time. diff --git a/packager/mpd/base/xml/xml_node_unittest.cc b/packager/mpd/base/xml/xml_node_unittest.cc index c24aac35b4..b323ec5434 100644 --- a/packager/mpd/base/xml/xml_node_unittest.cc +++ b/packager/mpd/base/xml/xml_node_unittest.cc @@ -15,11 +15,13 @@ #include "packager/base/strings/string_util.h" #include "packager/mpd/base/segment_info.h" #include "packager/mpd/base/xml/xml_node.h" +#include "packager/mpd/test/mpd_builder_test_helper.h" #include "packager/mpd/test/xml_compare.h" DECLARE_bool(segment_template_constant_duration); DECLARE_bool(dash_add_last_segment_number_when_needed); + using ::testing::ElementsAre; namespace shaka { @@ -543,5 +545,175 @@ TEST_F(LiveSegmentTimelineTest, LastSegmentNumberSupplementalProperty) { FLAGS_dash_add_last_segment_number_when_needed = false; } +// Creating a separate Test Suite for RepresentationXmlNode::AddVODOnlyInfo +class OnDemandVODSegmentTest : public ::testing::Test { +}; + +TEST_F(OnDemandVODSegmentTest, SegmentBase) { + const char kTestMediaInfo[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 44100\n" + " num_channels: 2\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 863\n" + "}\n" + "index_range {\n" + " begin: 864\n" + " end: 931\n" + "}\n" + "media_file_url: 'encrypted_audio.mp4'\n" + "media_duration_seconds: 24.009434\n" + "reference_time_scale: 44100\n" + "presentation_time_offset: 100\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTestMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, false, 100)); + EXPECT_THAT(representation, + XmlNodeEqual("" + "encrypted_audio.mp4" + "" + "" + "" + "")); +} + +TEST_F(OnDemandVODSegmentTest, TextInfoBaseUrl) { + const char kTextMediaInfo[] = + "text_info {\n" + " codec: 'ttml'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}\n" + "media_duration_seconds: 35\n" + "bandwidth: 1000\n" + "media_file_url: 'subtitle.xml'\n" + "container_type: CONTAINER_TEXT\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTextMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, false, 100)); + EXPECT_THAT(representation, XmlNodeEqual("" + "subtitle.xml" + "")); +} + +TEST_F(OnDemandVODSegmentTest, TextInfoWithPresentationOffset) { + const char kTextMediaInfo[] = + "text_info {\n" + " codec: 'ttml'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}\n" + "media_duration_seconds: 35\n" + "bandwidth: 1000\n" + "media_file_url: 'subtitle.xml'\n" + "container_type: CONTAINER_TEXT\n" + "presentation_time_offset: 100\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTextMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, false, 100)); + + EXPECT_THAT(representation, + XmlNodeEqual("" + "" + "" + "" + "")); +} + +TEST_F(OnDemandVODSegmentTest, SegmentListWithoutUrls) { + const char kTestMediaInfo[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 44100\n" + " num_channels: 2\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 863\n" + "}\n" + "index_range {\n" + " begin: 864\n" + " end: 931\n" + "}\n" + "media_file_url: 'encrypted_audio.mp4'\n" + "media_duration_seconds: 24.009434\n" + "reference_time_scale: 44100\n" + "presentation_time_offset: 100\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTestMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, true, 100)); + + EXPECT_THAT( + representation, + XmlNodeEqual("" + "encrypted_audio.mp4" + "" + "" + "" + "")); +} + +TEST_F(OnDemandVODSegmentTest, SegmentUrlWithMediaRanges) { + const char kTextMediaInfo[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 44100\n" + " num_channels: 2\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 863\n" + "}\n" + "index_range {\n" + " begin: 864\n" + " end: 931\n" + "}\n" + "media_file_url: 'encrypted_audio.mp4'\n" + "media_duration_seconds: 24.009434\n" + "reference_time_scale: 44100\n" + "presentation_time_offset: 100\n" + "subsegment_ranges {\n" + " begin: 932\n" + " end: 9999\n" + "}\n" + "subsegment_ranges {\n" + " begin: 10000\n" + " end: 11000\n" + "}\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTextMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, true, 100)); + + EXPECT_THAT( + representation, + XmlNodeEqual("" + "encrypted_audio.mp4" + "" + "" + "" + "" + "" + "")); +} + } // namespace xml } // namespace shaka diff --git a/packager/mpd/public/mpd_params.h b/packager/mpd/public/mpd_params.h index 002e0fcc7e..7f2faff290 100644 --- a/packager/mpd/public/mpd_params.h +++ b/packager/mpd/public/mpd_params.h @@ -87,6 +87,10 @@ struct MpdParams { /// element alongside with /// when using PlayReady protection system. bool include_mspr_pro = true; + /// Uses SegmentList instead of SegmentBase. Use this if the + /// content is huge and the total number of (sub)segment references + /// is greater than what the sidx atom allows (65535). + bool use_segment_list = false; }; } // namespace shaka diff --git a/packager/packager.cc b/packager/packager.cc index 16c7d855a9..a81624c592 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -367,10 +367,11 @@ Status ValidateParams(const PackagingParams& packaging_params, if (on_demand_dash_profile && !packaging_params.mpd_params.mpd_output.empty() && - !packaging_params.mp4_output_params.generate_sidx_in_media_segments) { + !packaging_params.mp4_output_params.generate_sidx_in_media_segments && + !packaging_params.mpd_params.use_segment_list) { return Status(error::UNIMPLEMENTED, "--generate_sidx_in_media_segments is required for DASH " - "on-demand profile (not using segment_template)."); + "on-demand profile (not using segment_template or segment list)."); } return Status::OK; @@ -937,8 +938,9 @@ Status Packager::Initialize( } media::MuxerListenerFactory muxer_listener_factory( - packaging_params.output_media_info, internal->mpd_notifier.get(), - internal->hls_notifier.get()); + packaging_params.output_media_info, + packaging_params.mpd_params.use_segment_list, + internal->mpd_notifier.get(), internal->hls_notifier.get()); RETURN_IF_ERROR(media::CreateAllJobs( streams_for_jobs, packaging_params, internal->mpd_notifier.get(),