From 3674f9598e428ff2f0201d43479a141d439d1b48 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 10 May 2016 06:24:59 -0700 Subject: [PATCH] ogg't'opus reader Support for opus content in ogg container. TODO: Sample duration and seeking. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=121940392 --- .../exoplayer/demo/SampleChooserActivity.java | 3 +- .../android/exoplayer/demo/Samples.java | 5 +- library/src/androidTest/assets/ogg/bear.opus | Bin 0 -> 25995 bytes .../extractor/ogg/OggExtractorTest.java | 14 +- .../extractor/ogg/OpusReaderTest.java | 107 +++++++++++++++ .../exoplayer/extractor/ogg/FlacReader.java | 4 +- .../exoplayer/extractor/ogg/OggExtractor.java | 30 +++-- .../exoplayer/extractor/ogg/OggUtil.java | 2 +- .../exoplayer/extractor/ogg/OpusReader.java | 125 ++++++++++++++++++ .../exoplayer/extractor/ogg/VorbisReader.java | 2 +- .../exoplayer/extractor/ogg/VorbisUtil.java | 16 ++- 11 files changed, 277 insertions(+), 31 deletions(-) create mode 100644 library/src/androidTest/assets/ogg/bear.opus create mode 100644 library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OpusReaderTest.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ogg/OpusReader.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java index 4af33e9326..b0df73f94a 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/SampleChooserActivity.java @@ -76,8 +76,7 @@ public class SampleChooserActivity extends Activity { group.addAll(Samples.MISC); sampleGroups.add(group); group = new SampleGroup("Extensions"); - group.addAll(Samples.VP9_EXTENSION_SAMPLES); - group.addAll(Samples.VP9_OPUS_EXTENSION_SAMPLES); + group.addAll(Samples.EXTENSION); sampleGroups.add(group); ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 1537bea5ed..c60beb7465 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -262,16 +262,13 @@ import java.util.Locale; "http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0", Util.TYPE_OTHER), }; - public static final Sample[] VP9_EXTENSION_SAMPLES = new Sample[] { + public static final Sample[] EXTENSION = new Sample[] { new Sample("Google Glass DASH - VP9 Only", "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9.mpd", Util.TYPE_DASH, true), new Sample("Google Glass DASH - VP9 and Vorbis", "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_vorbis.mpd", Util.TYPE_DASH, true), - }; - - public static final Sample[] VP9_OPUS_EXTENSION_SAMPLES = new Sample[] { new Sample("Google Glass DASH - VP9 and Opus", "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd", Util.TYPE_DASH, true), diff --git a/library/src/androidTest/assets/ogg/bear.opus b/library/src/androidTest/assets/ogg/bear.opus new file mode 100644 index 0000000000000000000000000000000000000000..4808533b3d15b77fa2f7fefa52a2c9c2633a811c GIT binary patch literal 25995 zcmYhi1ymeO6E3_!a9IfMF2UU$g1fsr!6CT2yGw9)cMZYaU4uJIaQOHA{(J7ZbI$Je z?9@)zc6W8v^HhzZrKKtW67YYH=%UW||C)~tWN#4YijHnBQs%~H5Rlvu0bTzSQT$K* z{~rH|Lx9ix!y3CGSpO4OGq!X=1b-uE{L_MiospH9k^MgbbnwgE-qgX&+*#oNx%B_v zO=*!BSdUn%Y`gT3`uH zOYCa*j+#2!n%bH=dfFPg>Y5tb8WuY0n(FGh+FCJsI@&r~np(P= zI$CO)I^f@dyREjh2RFe=>gsA~>S^if>VrE1AL`oR{=lD_8tOWl|7-YbY8rqW`WjkV z8d~7a4E40kv^BJJ|2x$H_pGI%2Ubc~AFL`^L>v6?K>oA)j|f|6ajx)D@ACZiyNQK5 zUj|ioyWWHuH6X__?j1GtU=O&OgyG|L+^r z&E(DfeYZmDeNp*$B32JGJhfgGh|y`8)JUeye`S)81Oh3hrPz|{|5VADcHT;qc$5sE zVH3H?yR+h*7*Jsh7`tc0C>QI-z+(>={;IkieRVu4irc$&YHcnwtg|S8{8M1~8czidP zI2tv4rJNaX7W2`KXBT4nStiUsJv5~NVU6l|n1gb@7G_e|zs0TY*a2LZQ#QgIJ$^S^ z^;x_WV4a>rCmd{#m)!H!$`YMwLxrn9C;{SGmHqlh{j3w*E=w%A<`inXk!>RU5%5rg z0PHsek=>~4@t~8R4!h32`=2K2*UHdq@k(Qysaff?PAkm%#b3TGRM;7;h}315skIk- z2~G7j|0D0lORG*$8S=lx6lnoTVQiE?28|W#~3jS`%#YlGIJu^qyHRYu^JL$SraJ^ z-=^Je$`vvmtn3y7B7q3wglNxNoshWuA81}T0yo2?78D$KBOVzJ@1di_ejr~4PLh%5 zlg8oopP;$GVgL4^mHBw-C1!R7x|T^9*hgQl8;YVCJNEmWJQRw5dJXts|EDUMPiap# zdS3!A0XF+IV|hU!Us)xV{&xz~cM%6DI|%xE=X;HVoUdi!rq2yELG#gu7MmXgqU2t3 z60n**$u`3pZNYuKKYbgfNM@{$m4KXr>5D7TJLi8=sciSxQ+pE3sIVwJhs>+~9f*!H9|hG@6U8HcIdxL*@YZlBFkP3sc!w5GA^ z@P;!@t@Q_yM+*GJpsU8dQ&_Hzq!sNtK1tmT0{q69KU3boK9FB82pB&o%a;9=BL=Fn9WZ6RWas05lf7shbvw+OS^TH?YL6ZPnS^0LW6Wt>iT$iW~x zCgRq^CJdiqu)UCWgjoyCPz8mQ$J~cd8@oAGvlUIKKF!vB-rNAd>y^+I?wij?mS^<+ zt?!85hoa4w!!M7q7aJqH&-QL!eul%hKY}kCw)~GAxBKp8>vQhL_hK*8guB{*HpkPc zY*z|dY%@CXa0*S!{_w#o8v5H&n7$m}4+RAxE%xQZdQ?HHf#Z11gr;RW2T?1tD;^yi zIV6-6Zf;3oMB%>1hR|~QGe*EU<8>rWbDoyg)%W7-BYUWzemtEjsGSCr8rzMoW+GCh z+#8qlLw%ml@Phyr=7BY?`N`vG%sXMwW1DR!jPpf21za{eHZy3y?WUUm9o)~d1uvK0 zf$JYfdoz-vDeNU7x=VFqfEi zhDDj=AXI;RAf+=7k=Nf1x)zL94Q<{js-p+MR`%}|>jq~2D+NM?LS&;z7Cfv{{ zTOX!nm?hQ&r$?i+Fd_QDk@R^3Wz^aA4^sPGJA#tx8{1?usP7}-&ow>;jTSndtnJHh z2F8?C0XH3+X<#<3q8m8^tNc*P+;6q&9@v@9{L2J3J?5Ky4ptR4n<45r3`F&OxP(5= z5ERbV_H1-~Ojrb4b35LAg&xtM?oVW4(4d!qRZ9kqzoAQ>Gu3AuLRn!JylIW*MP_S?r%jv{oJ~e$$3`9s8+f6G+%Cg0Od&+@$z9LdI98-UC_N;s|ilj!mU+1+jG(E>dR z0HEXzsecwUIJvQQeY$Oia5#Mki|oL)N@6eW82M`RCoSXSJ;z=cX|pRAO$jHtcs~;X z*1?*pfCqzF%->1(b3x|j!9C5siO{aF?8k&7^>H0iL~NhvbXuUV%OvwU2pZ5L7Z*Pt z2yOUQ9%8aCH`)t-)7H-w}EiPEg;8DUdYb_U6{J@P4_@__koM$ZWzF6&fBNs#MR@5B&&N0nt`UxHh5zu|DR*%` zDq08p`J24^VEU)z(;;O6i=S1ENTs%2D$+b#lq5c?@T%1(se7=u3To)TNQJ|;FdN7h z0YWczQsI{3S+KUOy(4PQ;*;~8CRg=z!Ch(IIWr2Dr0zh<+HD(#sRje zYjp1~&izP?=#P6YmlNW!G)=ftng>_#FYrIfStpBl0U-~FeNvVrJm(xhY z=c!y}H*3di_k0O7?#geWGy)QZPmZ|p!amdSnINXKHi4vRuzLGx1Nv|JL69L%*&QIh zyEWnVM@5z2P(#3fI|i(CyJ~W6Tje(Q^jF`XId_kKZ~Jon_IvHVnmfGv8=TK?ia~j) zB2-xVHBLW|lax8|xn`0r9rss3#sjdf2nYp&taDR0!r>t!Ef!RP5|qve?NcM2{#M#s zu90Ql|9n~0jMwCsG$rpw>exX6AfW09`KBxwQBACGK2l~6o0K%m4YEi6y>rrVmqPjR z^s8F_-L_xo69Br5WU)o))K39mc??-wsM)YV#h{&SiKeaRKhbDWrUCd#AFl3I4wsef zs`m5^r8cQHBo_wZyj2}f-Lmqn0*{Zx#O#TG7*gdZGft;^okJ;8EzT&te9qh#7J&#! z|1D72j!PN4P*C-L7qKX{GxVG)KC@kzzI9%z`${~h~Nab~)sdz6-lGjb+cQbUY zK~28pjEaLW(n)m3b6T*kJou9WoeGXl)A1G|tIqjnz@d8$;wsrDfuirNTBBQqMNwz% zJnv%vjJjDjeglI;(zNDfEF64-xib;^ijD*y#|Zz z)Muk@r>hYo#b{`QSA>Q@@@FeX{fyUNlW5xHwAi!kdhR;DVomy^s-Wv8H+$q|qbpvZ z?nvXaY^Hj0OeP$wpIGZv5SjEiCdt1P@$qyWDk?Sao(FE3mWh?r3~$3<8NCbB4Ba4s zgmc%ury!tb$hYifMHyR~t`NKn)_(JDIrArQ2qox;2t_X1u=PfzoLHaHB>rT~20t(!#;@Ym@Q zz8+}8-RXya{oI#;vog)xdMVA0ZN<)t{?>J&@I6ne7Zc;n%lvZJQAPzafSUt!mRdSpcOyV5Qs4Jcw{KtFj2XX?)i*7ZM=nlJCx z{{O))4^Or>--iJ8@-_99C0la~k{Hson;2Y}uSEBkoaGqaEbbpG#a8lXb2t&vkZC>s6a5N*-$5Q;|gpiBOn%s8|M)LjL zdHJtp-2~^*5(%nadoU@DfAuT*<9}o#(PHq!m(T5}d}Iddg6{wB$>Il=Y$Vr8aOe(J zrVN3Vg`C8?ST9M6k*N9bN(8iUw(LmrQ0rbRlo~NV`^|?#85R*xG+y@h&j~b^Fm84s zRm1!0hte~QV2~B1r>#47t!VDQEerZ1j{OF17DVwjw|J7(20($`8WM&5k zt_Jt`?`!jT3~L}7b@MPpsdOD@O`gwooAcvc3t}vk(%#y}^v&Q)%Vex=GHCBO}M;s9`!%|9I)?A4-+eU1V;ZHk#EGAH=}x5m6rK^#o#XWZ%LeXq7% zln>?v)?|168%kZMDUBS?U*DcjOkciHbwt{jNSJVDG>|L!iQf&si@m+|LxE?`k8LkF z6=ph)s>hEzujI{F5s{AC`e;|k{AeIV3vd-R3G_ivgYdBcWDU@>i1fL*3>qs=>)XYq z7zi>%pwj&Qa(WOoArjtBit=@XBt@Wg%*}GYJRmvQi?I9PK?%BY@{1Dcpx1 zd5Fr5DqpM~*n+}I>j;;S=feNZX9dUx0vHF=J+_g@VJ6k!nqhZd|6A=y{nGo086>L> z$#t3(I@uF^i;}VF`gy^A!(FEh83rwbwtuF~5nIb`uyLicZ2=|lYBab_Hiy3TR0x+4IA zR$%A|%2sp9zx=sGuhUv<4O)NF7sXI#u0g`K@0P+yPlOs{rIMMD!hc;E-EKeaG0QMI zEg$z8)$~h=#A@^T^O)_g&$CEFLjyGfBJ=Rhv)FKDT$>y_hVLb>({B(0uurL2jmUZC z$o0QLsIwZu*z~f0zg6W#e#>H(O?sX{&5v@HA74aA6|;q z$Dmn!`}{9D>oGQKu}G(=9sV0i#cNh)smJC6_1-CV=^>M{-A?dJLlz%9EoV&k6gLh# zX6?cIMMLTUId8xJpdEfM?kMv$xDVGkF3OxB&56(H+q!_7kPy-hnEZgZT-%y(s5r^# z!jlcpjYF8jn~eN$T3DQq8tgeE=BF%+SpTVLK~b+Tloyrw9yoXZ)=0_NRPBOYAc`38 zFa@7JU&DRY|rS?k?I{JkefjdmK|I=z4sN?m4>H(&fghrO9)C~PI=4huZ`y} zuCkov-)(~X!J!}V4An||Md&qNV%*F6^$olG>+#F`Bhs?^OPaL*5me4QhURGuYmqTQ z)4UACk75={Uy<9b>Bm!YmX;yVWZGj&0r5T~!ThtU_q9-a8{vSu@XF96m|^a@4mZBV zJT>ODkmJ4C56AFwa=XU~kkA@ZGeU6+se3dOa32{7VUfWnL(4D1j`qf>K68{MdJh(T z)js&>z|5Y)ES-(`9KW!#=ZS-jp%cPdwanXY9z9G$2_wWg&hX5kp#Qtau{vphdt9`PQnfJH?*EeQcBpUZ} zl~k2%nr3r<=@x$iSZpSWKTx*EgcQ69U^wk9aOB?mVnsLpR+XdEkybcAKVGnrZ&y|L3vu=RLJy*JD(=gE7hfJPjjDlM)cD}Tb{ya=CL-+rj^r4e^RZ@# zl;GVi7ZdsC89MhSko$Q)gh4Cpnc|@iVR91cKfj4kq~BQnD;^`*IWJXcP$gdUV*+yF zkQWO^aptfwR}z-s16b&OhqCg8yIC|BUq3*>Uys+Zhvh9<8QIx>|EZARcrBSoBP;aX zoD}|Yiyfd-^!5qHSP4NKs1+wM`ewwtRvdNE+_JRM>iBg@ArD{wZ`wQUBM-YNRB1gCmV>*sxo zuy~2~Kc^q?#R)P$oK!S9v41!v(=wxs)CRoV)B%}qK+sUE%gm6_L_90unCmsRqdXSP z_b$0a&H|2MV{jM;wRg5-pP~MTW;+ z8G~E(w5(7!713`(K36`^!Nx{Nuz(A@0S2R-+Y~WbjroRf$MdnePZ?5rb^Rk`>=B>}N(+`Cztm@bL50@W~1#|rys#Gacq+kHRfIf7gt~g%l zUzxClC1aHtozKzYt|;8@fE=}Hh30y$&ad(s?Me-ua*`2&iB_R(7|vgkkQLZZ8(+11 zv)8q|yRcU47zsTIdU{pRJNXf?Sh29m&@nJx*|jp5tYn^yeWKDqmgabDCXxdwM=w;$ z=_O7<-6O>>v`EMH1||z=R*buZKCcq04HBR{IU_Gte>D#IW1I~ljD53$dnlp!`D>3T zO62@e5jL7F|IXw9%>(7#RipNqO#3}{VEe9L@D9d>Y4~)U{L?u%xSxLWCLd6ABLrqh zK}sjr`XCfPzUt(tLB+xGvG$CwgyHRV1S5=&06@$ZDL_;bqd_P3!APLpyK|79 zjw$OxnYO!FSN?W)e!xI((1_VEvhypwRla}S0tRS7>uKAD@s{oysy1;z&plOqXS$HQ zVk>73FAr2ld{@`(hXRkUW1(`qpz8bW!L6Gspk?JVS^ihb(fBlP;rt7oN=lsXk|Y0( z3Q#G@s`+6vNP3w+ZCVAov!n3=>O|$IkuXvzfATJaf(rQ{{s)OW{)d_l@ZRwA4MaLY z%F)y8eZjmtW?WxMOth1~cGVpAHTK(`@XYC`x2^=@m?Lz%W-+GKfM2tbqhQd)eV5Y3 zMpZe=klU!${HR<#(=1nV0SiWeZ(hnI9kA~6?M(7SB;=wyIf9<_kWa( z!;K*-B$eY?=zfg@l3*ovk%TSJ^-I)2_chZ~%%o%S7Tw>}4xA+KYDr(XphHhQt zuR~ZMMjLa@QV)<1z)4*mK@aYfG|-;}%gWwk*FYyfF&neNmA7$B47X_U_%m8c1MU&G4-g zv6--YFs=Ip3|!=&>LIZDR>+*Ws`3Oymbx>X#r89^-Ald{cKC6Z{?V~MVqVKQo{83v zes|Lh5;jCyIZedKZqDEmY8&4G@tBuiid6&?lK*JX-y^H*3E)O3$u9zeo!c~8t;a^J zfotKa?$J>(%jH{JgXBN9wtxTd-yAxqWJ-1qX{l@(%IuE!SPeQ5$gp#hr0auZG({`6 z3@@04@yTF-9OWa0^E?lbKfT1>L zIJx}{XqOImAFD`nb5p_4)`Vcd;vmm+w4g82aN@qo%a#10w}xc;+s2C2+i#VNP*b#MzF?7iqfPQbCG+jWKqE?dRZ@Z)-!~R?<;YVg`GN->{Dh zjx|$F%Q#A+vKd6b6=4X>QPur;brVyzUQf&uQdTG|ZBHLZ^r(f=VFlW!jW48g10{hx zXy2Q#PSvM*Z>(W`sd}}nyC>L4&48O|MW3JdL;PS|7&1B1^#Wm8Utb|Rvkj5SW!2i? z)`MYPFLlXu;z7fYlq#gaI2MNaIL(<2byb0XO^!y*o=$F@j)K98ihk$f0JuAL{J{3hdDmu$Gb9;_O_k_(sYTX_9cI zBQDa)hTq7;tMGo}Fk`>3>>0^<e=0}fry>XOq?ol04G5n@fl=nDS=!znl$2d?cZELg~%ot1k_v`DJt8E}R3E%&A(3>dMKESgp(*U^anPy;B9PNB6!#I!>vnKa!j^M*uhZ z`IBrQ1^@2_E#&=$SzHcTok$Wo!+$sH@)i34<^^c`RuGNXrSfgErV{yo%Gb9O^Mf!J zU{DpL+MNq6;+;vd@1 zOS`}qJs>lf>5A*mN9B4u%njmDo~KyX$y4aQZ^+?8rWFu?;_s=~%8G5h7+kc!sZ1TQ zfmgp@nsMRJ_k!P)iLB>WI`Jfk8xmyuW6JFkynH{6B;^UR|8k&HSqB8C!Xcv%8U~DF zZ`H(BA+@z$|BYIQXR@)WyQ3}?A(sK=mo5Khx07kFLfF@3kC$Bs1$i%qPRyJCV-JaQ z?5;3$DQE`;U@|9^aaWoMFM zbj@NDv;TG&qrRk**!K!~`!l&Kr=RnC^O7Vn(E<7E1>WO)I>9e#_(1iaorwb$3!yJf zgR1uy=_A-_rM3rnI_!3-6xZhY^A=^uP5IDBhWGs-0TB8`M|AlQYJbJ6$Ml~?)kYiJ<7MzjoaO0D^l z=8rx)^pr3Ifspy1?~>RI@$XYo7llL^o8^<-;AFXlrS=R6Z9f5qMBtgcm$Pp4&@ zN`Dd&OKR_WW`YC-Rci40YJ(hhBs(`r87^>-f(h7&uo(|+uN1rgZk{E6@lVqdPS`CE z%lF5H$H!AOX=*6fCJTNffZwkbMQi(;BjuDv@a1}P(6@ZK<w`xg}=zpw0&01q~~FZXIi70fV3 zUC#*p!#tBKg8}D&AuwjKKu-Po;-}tl8lqkF@%fmqPNgWY>vecpacRNLEW*99q?azQ zcnGtz%U9heO14geXvSn6MN-ca{&{+gdb{(K>3EO$1*3z!B)~(^5y7=-)n05myzJ%rzQX4c z=-A*;o~?5y5t5LP<#S8~G4%koP-1<4B1FSK3En0KAS8X@+gZ3zR@HK>I7u<7mNCkT zN+ifgBQe> zQ}L&$_2-Ey5CDXFdjmlxcE(Mxb0V8r_Ah&%UDdzL3}TO?916x0i8hsDAdms({04)Rgg`0pBgqPhTb8+ z{otmg_k;9!Z8kTRe)@?g0i8KrgG&!<<)f(ZB8>I*g{LWZK-!+4;Q{{jqPb1x#?gt) zivOEp{?J_kh;R(bo%@`Y$(2mpQkW2GJcfqo9%hvJDI$z%`}*@^qdkFIEubnkx#@kn zEL{wu@`KhfZ2xAufs8yMa%BzF->j15-dA4Ez#zR?VWXX>IC3? z6(Kvd3-9v00rgPma)v>qUT=L^ESl#8IQ1TOec}1^zRWYM?s`5fBFy4l!-7Z1z+Kkj?X%FrnG+A${|9XF)xp9 z$)|R?$XW=;de^HP=0mXoK!hY@@{%47S9B`)^ltv=-n6Dcq6<&!Ui}czj||!YaAvXj z%2Jf(eI=GVI{)CB^ZNlU)$mDJfCDFtM@|Qq^2dJRs_~lC1ClQ(Yvo<}CO2?1D=Y7E z=R%-M9F>;2&?#xX*jscRDDH%a>r4vOILR5kx;UND-eP8Kahn;;5dh`Ou5S7w3S@ZJ zK^$6?K;g@9_LjxX=udnVshlcuQ`IcBeSF1H%L5POJa7BHp?;*9!E6w~5;$HQZOU5N z=Fl>Hj2rssk%vMOAa1!cNy&aC$YJ=D3sI;@_Zj($$|aSzSB_5WzMg>XHY}4piS-?X z9v)*?1&KZ;d-DyJ0^)9vum;@($;$mI4TTkaz%|yhUlQjOKvLueR%@k(y z%(OcRmfsEWR~DijHv93G^9hjS&dT3g+Ttml%SSh*QmFA*gp(iT$-Cn@x5naZpBNaqp5=bZ-0HsAjXU7t z?#%rjWOMGR60%hcYknL{V9QkWJnI)WkvgfIHB*XlxMY%QQU0pZ&T>|c-46sq5inI= zu*DMLoda>Bmi9&^#%50UbqCPn7l$ks64c>BPfZ;h>ooRred@6EA5LJf#M6i6sAwy8 zuPlO{k7S|a{mtf;b{IBpIHLrWi}F0X-9K+T=>Vtx zvkTXIxo>D_1`Kz@Vt%gAc;5sb2DQ3WeE*B(2SPGZCxau#wED2_;=qI;7P1%(c3G(! zo|Q7+x*KwY37k-Z$zP1-(64tgQg+*9S6pigYc1Y*Mk)63i#bI(Ch^dY7T zxw*o2xew_E2>KG_D}#r&!%ZyJGAx|oE6?eVa^379q;TSVaT3#{iVgkv6>*%bSLf1j z%x7gF0Rx-z=t@CWt6&#Met2-Ngu#yuVEZ4z5(PY_ z(W3;d{^Lj=Dst~8H$cWxV!7i3u*q@d9KDS~wf10gqyB1q=|~I1hP?48GbAEv0gWzT zazgegMj6f+iNYW`I=0}VuymSWxFf=V2bk4b6^6^3KP8Uma=SA zh0NdzPq9otS!-%%Q=fF$5;u^N21T3{iMvatRC@-BC#cumgFyq>aahW7q`rdp+1k1X zKUPb4TI97qTR+u}G*kM@STL6|7vT!o^aV@;J_n@a z@3WwG=&Vpalf~UCQX%~MXzuU9fWeONZk_lVOd3)&YV~->JuB3ls}&@bO#RTLe>suG zx2%_jxa2{5ohHtg2*wB#dEf|lr+VD_9SKc;v_lZVGeIRRTVM%ddQfX$#X3{kv%1PV zf$(2MdIR&g)tA2w1~Y7GHSuMOULEy7LB}7BGv`iNz09Z@!#Z-C*{3= z<|}p7`-?t#L2e)gVK${+Aexnjd%5$B)|7hb#kboz(y*erJsY^6Gk)`Pn_~*^nc`hv z-5;VucbpHHWYHgBskT-Vje-`?aR#;0H)pH`AshF%u#jMPiPT-xF^Ao%X=v+_z-3(| zIO$s6s}@_T(}$?7y)(LrId5A?{c$3V3wOXR?K>uHJRC!s*_2P`UMur6>50}pxQWSl z1_WE}WxX-TeeXcwaKfOo_oqPdl{6%cHMi;X<@w7w_Y~g5UeuOU%URF)1sAYRtkL zlsm}Jpwf87!hW2AMd15}0ayfQ%^=6RW`gL$^BrKVI=L&zWdS8Gle~VRi8qFxwIY30 zkXImbsrl!N<)3hzQ*+^vFN$=SgnfT8oN9=!gEDp++ob7a5&NIz3WhvyOz?3ZdYWI~ z{RhNuz*HR}p*`6*1C<$*7+__vre1e=GdI!qtXYjehZ3{jgPM)W)4clkEy?DAyUi7o zzY@4!GE_3qjvlU;`Er zC5dS*Dq7to@ks(nZbm(9fx_4Kz}(1vf0*XPY0?o)Y$FWPO#KP9*TvgqZE@GEUhVfM zM{iBdXL1q-?dKsEPlLuTtLE0_9$TS)@IWN?Qhq+0XIBR^OqpoL2Z{1el>z%=$eH1V zRLm#>kFEFbo)kv;eU>gS!hY9GW%N_{*l-xTcQhDYx8)I=<`WRv1FH*f=&uJWqej|P zZ-T!01q|`ugPDNX71rE?i;xz(ApNO)uM)B{Fc%J<2*i4Xt(e{jI`WP&<8$Scic+L>| zP^etpZ30c=*;P;^s%u|neA_6f^6yR z;$*bc`)a#o;?N7dR-MR8d-E`=dp<&MLJjuygA4Sb0eOhLO$rJ#@neO_LY?)rtY-&@ zd~0$;Xk5xUDJY^zORQ$+2ooenPbb#>cZicWgz~_fuwQ#pFbO9mr9%MD(rdx$vq%+&?BU5t{t{ZJjF_kU!7QX~4y` zgpA{mI+!*O9SP3p$7yXDUSViO)wMzE-pVfQ@<+tT;^LrWm>oF(bXQwl+_mVM7Pz=P z#uQ~Z45#_83t6i7UY2nGWEjl*-yR@{lOb%XR=_YBvZ>Y(T^(~~@0pn-W_Yd%)U`a_ zh|8Z5H7t4QFikQ`vunX&|9tQKi$Q>>80Mp#aIStb9-2pL9ye1P`}bN05KfRDhh4_6 zKCM(ir4Ba;vg(8eoOggsvSnm#exLBc;P=peZcD!od*Maop&3lMJtj;{!1S9$-{;kt zPeTnH7Hxp|8@E@)Aob6k8FFl`v@rofBD3ws52iK7BTn+sy=>T+l0ync)sTgPRAZGvJ%8 zYLcLNG~n5pCh=L4vHGl5VW5z-i-9m#ISbd21$zj`}q6Wrj^h42q zLvOE&F_};PQk%F8U|4FOr*zDhQ6#%Wln^CE8ZQXzd5 zkntNJ2|j93Fhx*im9+nSnns$ug29&f>F(m+zZGxRct3w!4T){neVOs&RXl4{({QDH zaD^Ate+IWkdCMv7q~9dmC!4;^Ad7W;E}!rFK)p*gSD7iMoUoosKVO{yt*d$LeOlSY6>z4I zlGR6zCX~qH`SLIja9upQxnY1L`bDLGG%CE%vHZX|adHS$puyjeg+cfR9nKa$^4_E3 z=6Z@dcgPX6FCb+9E2sAYdj#wEw!UB1zBfXf+rJcVxXnM?y&w8=3i`fI_`8{$Q{;#S z%6AcM&-m+#B>xaZgW2OwamA7q(NBC6jlWbcvjqTTTTkbJT*J6p&5^0g-nSu3IV2MUB% z(OzQq%z>8B*)PIoWm%Yfu4W68C}icrYsSn8_uI@qRjk!@FozKe&LAc$196@#lW_hyH-~-}|19DV;erlQhJ%@C)b853nwb*ErITw z@@#t51T0e_yw~)I`pgK= zAHCh!fd8t=XoFMJ)9h%y{9MF`UE@7N!u%bm8<<3ou`(|4>)1fucT!%*zt{%G_=NB_ zpcDRb3hWyEM5JEYTD^P38_UE^IbvxKnf@{ZE3W__F9VtCLkBi-(py#1q^Q2_JfIeS z_V^e?N!J$(wSQ!ayi*V|s>n>ivU4iMrRNHUPp9MIDch6JZUUiW`mbr{!#od1_kCwT zDi%pePtPyarb3a+?+nWCZ{X)t`Po1*_W_0=Q zXDK^TW}v-z>FrBP9TBwOy{HQR=H0jJ1*}UR9lRm4CH%Q4B#(W6xd7@YeIkMVR7|P@ zDaPq4x$nllle!f|Bz0ST_#sNnt|`*VTLJ~M^P>>5)7)5@32Sdx&)+)6ldKXne-vsP zME3`F8VtoEGfe!V0|{m{?er2$`C0GRzF-WUeBnJKTA##6nR0x0zY+4#R| z>jr2s^5`OiUrQKyfzm@>S&{ zYQKQw-a%s5sjSum^Uq)meyJ&tp>0Ztqru7-Tue9PsjJ7I@be;f*R%n0TF(lJSgK4_ zSPYoW<^0iy8WoP&DN}~bYDzJj`zz8I^<32V#UU%FVewGgRyy+dViZPR!K&V{1n>!d zZj*H8yZPaPKd$T!vPB_}jz4CJ9p%A8vHtS>5)x+E&IQ+ZdDiEU(T$6=@<|%c8??sJ z*R5vBv$1nnBGG;Fbxpo)6(1Sal|&n^&OpikFZ3e|+~h3;5DH%m|NH~=fDOoHJR!pq z*wNw8ef_DFfk&o0mtzdm(fu@$L~D=>!+?JtgA6m`!&KEePDYR4VN{1DT!?ff;{I$y zI0htpFXUMoXmfq>9Kj3@*q!f`gDxxr{dxEYo;CDM?kdrdTwsLywU*tu<3`a)t|B^? zlgE}PeNF@p@=#f~uXG7fS07n_xAqGa-wygbkRFRO^CvKpO7@%!Ih$6*@gnX*-`<;~ zQLpW?+X77zf7px{{63EjL-atM*a4o(WoguVAt=vpzqcO_AZ1MB3$ktp3Z$rfH^36H z?1I47Q0XP;gt=9Xusth}hwo4Eo=lX{s$>A-=7|ljZMsZ|`%Q(s_ZL#B4~FT&b|5pt zm41%^>VWhi&3VMBr)eE&P}c&(^LrC`i#1hKn3G{^7C@PpB8c%ed2X-zx8ci>;7k+3 zjh9UTbZ~!+Nf==UTmY_oxy32poh+WA|q$7+>?PQ+8mcz2EPv)eXb%F9vL`so*?(S@k=QRBC3BDv9&g90_ z3a_KDFfjB50bJH&sr0oCjDRw7@TnCL)_^dJ4KWp1oW!iWbN z6ki}1ou)(n0IesXJh9iwjA~u6^LY5Qvt5FhJqYzQH@(x7>{Zx6-x3`I@TNV2Ys(;6 zDUJ#KwFX%ggNyE-p{_6O!@`675B&5#BuM_Gu6D?chFVjte+w|To`6{##mCZbah~lD zhk?Pssb_aGvt^k>EFTUH&g#%)pFW+nhf5-1+~b#}bayLM1N;_Ip`{stguI_vXP-PP z=+_J|^c^mUGnF4*wjttPRbz!+mYV(>V^p!6$_!XokVuls%E^632S@kNvpmp)Fk6=N zsAd+p@x=lVT(x}7o-&h$mmg01h700?{yYLws6o^N=oIJ!|MX4kPks;TWsD2zPkOGn_S~snTu!@EgdxI=tBb5zl66edhvWv3YhciH1TlS^EG1-4{C% z<91y2Nsf5^M#f1MlnV(w`T(P>Gr;r$kc%vb98GMmFVxyDVmbgX9EU}EbZxE@PkHnh ztMNv$Wn&1fKrD3z5}S(pL#&<=K-cq#@d^nKP0jjT)zI!df~Aw3DH@)2cGKFDrcJ)aGRX9mKM0x>IsUY6EC z+sv~K>6=l=pYgK_K{M76HwMirw>+9$b^Yk5s&RCzEnq!SW+Ferh?2IiFuf&0pl0o|=E%@92 zi<+YJRT^gE?1RAJ5H*^W{j*ymaA)vbcLMdFjM{uz1yqG1_mrOTFH;>fQx{7C2Gj&HtRpP5nz;wx!0D= zR3CW*G=wKR9kb6t|B4YI+#t8EYX-%qnhu)`V}znh`mbC7GyL=&IQ$X<%8xoM*ZR(- z0C7LUk`*5pi5im2ldWez!V#y)3sgwKhnk1x0&#}3eb+&k%eRUl>KhGh$w}P!`HvrH zo?Fk8h+Edsdz}2HG&m5Q`XB@b+jXb_UVwpl`GiQmAU7ydF4giZKCXd6p> zSr-7o1-%cMd(9qa-|Zt^nek@YJw@O8pDOm@DGmX|N(5vp;>!nYncIlr~t#v^dfyV)8RAPgk?#`w)t5r&?txQDFp|A_ZcIhM)oYQ~}4cQ@8 zn21g!^c^AAHC@)C%&YhQu16$UOs))AyrdJmkYjtV z?&thgebE&+sRINO6LsFHVv|}zZH~mR|3gof0gK&o?NS%nB8)X*wA!*d^E@7m6x9TC z0cw})FfjBz`PDpA4m_1jLk$LyPxSmmjF1$Ppr0{!IZ6gA=sr@bS(j0@CnqknoP7h> z1c)B>4GlZ?&vL#8broHs$83js;Dn~L%Tu(xEK9I~n!Y%U_0v?t+CJ4&$?S7==MhjebIWoL7BB?_8Yd^|T5ETZbOG6<5IgJP-68%zs_C=hk!E>{Lw6mD@WT{VI`-^7KUUqKb_h z;~UW^wJ)qg6N^$|*26NBp+RuWB#>OU2$$dGsO$y%kp+%tbgdDl!Lw#sFO1<+B35d{ z=s(xP0Q7JFL4N#XLVU|J;-cmtvZ>KA{xcVuR6g3>301FAHO>_wFBAg*gzAWiU__l@ zM^$}0Z0u5SWCd2iTHFh}#)~L)fqq5lPH}ajfs4YyZ?Kn$^iF=aJO&;*#&q26IpD_4$WM6T5MZJ* z0!4rr@06VmP!7tXSZh$FOoqKRtl1!-j{=@x?w&qq#fa=3xiY`yYR#__u(Qa1{S-;Y zhuzc9W5T|?=e}|OsiV{teRI_C^dK9*+&9*MyKn}QQKE~H zQIH)BkBcd%hlLki={)>-vUM$8V^x>Olo@c*@Fv5WwHQERlAD^g&Yc_Q30it$%Jyc1 z%*gOYf}QI`eKl2^vY5h_gXsSc^c}0Jz864zg4|^&z`2D55 zw`hZ}aOc4MggKTI?#&>MCut39*YnTtI`jZwb<{NXk%!?Nw5Aut^qo8JPJjH>Y;TEs zu>R+2{^y0N>EUhv34!aVoc(Qu zCk3VR13wJ(9UwSLmHEAS7SH$8alhcq$oj}N?NRIe|FA3X**k|GZKHohVm8vEjfVgA}A7ja1oZ!Rw#K^95VU%+6R%3!}ENL_+YJc9h8+f$E z$%NrJQ%Wu`L-#NZosWkw^#FZ;v%;xEEA!0s0)%mk82#+8uj(|Ff@JBVM47-P-9dL% zJeW`ir|GRL-X`2D%u*0pIxLWqvD*-a_Li%&(M^*93hsi#P&| zVu1lOw3PCwZ@V+>utvL8T2Oy$ayl75@$$&xx4+CdIpxqmJcB$CUK#^n|WV2`*JBT#do4Qn_ z01z)K;c&+vTXv;3{tnI;tNUtY5~wqW1dX7>onUCeACmZ*Xv#IPgM7L5{B+pDacX)ZycvqDs}G{4@8O}o_ZFju4wn%43M{xeuTBx=W9VRJ zDO$}1wA~02(SBg+wtV7E+@Q(uqZW4Juo`~r*#Ps9K4Py+xk^iFvRX87uRWwJ4&@@NDxlJaL+3~7hk2EGTB7Z`eg;s44!3#Sxw zvQ#GQoo=>(BwT?{ahx@~>)@rH@l|5T=KRJh2ZO@cGXq+*#!ZUi=ALT_$~h8PA39@D z%wSCsbe0i$_h|H5m%s33-EFfwxV(3_@#YK*1R<7X@{k&=v1g15uMZCZ4DARR`qX-xYR4?D?I=7PJYxkvkY)>$s!r59A!ZmQzZv3Sy zYW8NlAq#jFZ(zs&)p;9YStx-GmmkO5Vz9PJJ3cB6PR*@7x)>hs1cnvj1sd;8MOL!V ze|o{%SX!W<0L;Mj0BP{K-0VU*4%j_!B&og7O;o{t(tH_Dn~-3UuL7u5xwiSu*Nakvr!`yRCrHU^4UxZEP)ysT*^F99$YZG@f?<}QCrPGHUkdg73P1E6 z!eI=aA1aAbY83l1P!A6r;uxPRxo%ELHgx_z0LOT(C$_YlZ>dUWMa<78sM2<&IQXh? z!>AUa4VI5FHm{xMU$gDp_o@n(grTr@;0e2T`~hpj{6F*|&%4&-l?hk%Q@Hyvd-106 z)Jw5FQF0TZq(=}i>gcBk8#YnS{bpH3nJ&_Jn$v? zEF2$VRJ_ej`cgOL!l_sP05kL*%+r~xjOeMUIH=BOIWg{V>2TVR7B{DMAOTXTax7QE z9(vT6kC2damIQ@bR*cPc?CQCA$j%tx@YG(!%ArPD@3OZ7jB_J|-GZdntJ48$mjDC6 z^aTM%(*#+S^|<@3G4ao)a45TPqNaafZtZK_*G?mPPQqJFGacDO3S^Put`>poyRn5B zxTPu=iapPS7WY$zA1o?Jx;qF6-&f=-)?D`UtZu`@*E~Et^a6x;sG6Xt8{kmAKn{f8 zIP#~(q|XyE+%RxR38PV9F10&vz1}42u!it}e|m1kV8VQqAN`{5F|q-eWbK1C#rGJA<8JO&u(i17BKX0nCNjUv zps-}UR@SG63m0Z335THvGTw@5kgd@fI4Q;;lo$S}>oY&} z0*k^EYC0XO`szfT3Pa7-W7dr@s~1!O?=5G9(aQt!a9J)Y|KYXw7+5AgIM%DWE3#!ALKp*CpiBJp@| zMz5QBg8TM`%+Zy|aS{Z?RoEBGc(a{rZY;hVCAOL+bJ)1rTV8d&7X6TNIktu@L4e_Y zoZulrO;t}0=g=aJSyEDUrG#ox#Jj=bAacOmJ08&^#N%> z0K@R~0I@85v#-;CSmtjs=V)bZfCa6dY!G8fZ60nl8&KPX;q&aoEhT>_-*T5p>Ekf# zqbMfYB@hbl3>|{(RYI#BVGs5!XxwQ_RuZS*N9LWw|G>io@bn!oqtyqK_gYa09mf3i zR-lB18{o&Ssh7j(WDvVLRwIqFXCUdN5eJ`j`M~N^5$!KE=z?hhn?mV{$NWU83N=OC zutu068!DhEw>;uGSbaA!H!nP@Ra0L-4D=u;Prk4Oqu@uy1*!3PdNKk!?idk=tNL0x z1O;Q25M}rb(ljTWOpHzEodzbc2Et|*z-h12Uqf%oI51+#jth?iKvxowx%2xV9Przg zhF-SSd_OD0&-4Pb&7Wnj@9^&0v<+W?7T%I^SqvYwR+ZEfb~O!G>w}X;#MDg0yZ?G7 zXL4m>Jc2$1SzTjh$RT_I_S z&b-VuTl4@O>9MW4ZqWSp`Nxgms2{}Vd|j8hRh|1k=nbp5j8P{7h5uVD`LTJ?o=rws zOjV%PMpp2ba5UqwBvwc|DIc=4NzMoX)s>GXXLo8Qzc7+N^$+t<|IG9s-KafBk^m45 z8rhxkRPkP@LYyzWp|f189HSY7s~%*uy|7w5Nntjz)CbVT{pY*`yyoh*6egz5KM<(# z`jrs@n)0X`TB>R}BW;L3g{i65tN#N(F!Tb9dFBCGRAf6+&hHUr6FmTk-kA8NqoM#} zd2Mw=hk6sOXRoN%YYXZmJVgOaI3SApcvvly%?1`sC+g2DAcvJ9gkDXedTJ&aPCm`i zTt5Z%p!599^c@_>@&C#Z?s6kvgM|Rgq~F`qx8piHQs3YO43M4+J=qSLY`CUGgtRQh`b)M(>!$0DJe`gVUF8=Q}c6>5XP3;^^3gr*6rz6)+^ zU)&E9xUkPu(;_T&>lFjzo0+Kmo5jRRSoKr2%S2%nd@QZm(4e0Ntj|Ar(RBsOtm<5( zMXT%+CGa2#C|oXe<(dXMIK>Z2X) zt%|g7<C0t-%IQI9>+taOP3|w& zl36BL2#DAlfZs!1WtrbVz0Xr%zeR+1B$$Xp4gwGWb3CULAM)r!ZBJ5SE!;{zk{RSV zd(cGTY7d+Ofm{5*mvxQm)mqWE7~B}S$FLp16<6y&|MVZ-4vot9%WkPBCQfp+bMbUk z8xlBb2r=;MEAIxyO;n$^L7X}pbRgG0p%oi71DOD-@+EY)L$0|OPMg+bgOf2AO#>`W zWOJ7MuiCXuuol68*8o5K^Z;r>_jvB_ty|nZB=+@eqAFOH=<90TxP?0o=r`}p%Gd|x zk9()SU+1t8ht4OFiImE`<%WqAzNngHC`F%Ab)^D02R_GfmY=(zOBxg)!B_v+KQm8f zXHx(KC<6ij00000i=(>z0{{R3apFeK6j)nVS6Nqhi>8oqY<+%udWMF0e)J#PK<~jp z(#tD`C^FojZZRA8UuRvYP$#;XIqVSdxn@IGOksc+j1_D1jNkO};Ofep1yRa9mu7I! z+T#k8kqnnB=4gFayjG5^JbJ~bA5U`^!oU9y^Yk6s#63LbWN)9f1ZDyuznB-`#{v?Zej*kf4K*e#x;j5P{M??LiTKZKhFwgV?$-{3XCeRO_QLZaOF=;0) z=9(j}E&i00hxSlP5UcD~-?JnnB`g;sd=k5P+cm4VE>#-#l}2@*j)7NV@&zbi&Bu<$ z%{GABSQ3}Dk(pkF;wS+_JU;;R9UwN0&e3((efL#aH zNd!sMeJG0QF-5i>0BYO0!rAK(42`2VS^>&~T>HHub%J7=S^JD6(pHBAL%MVB1Jo$| zJkRuWjGDo7&jmu}HuvlB30DS2Ay`|6+yUKgFbvRY0$Jh&$saeL%Ccq+@^lPJWbP4w zHK}_q51@ezLW!`_P}~0j^$5h{x3}ZQ5+Y^bTE8B&q>9-k?hyR5z3=$=0644!2~DwB zB*6pD0JactRAvADeL|Q21I+Y_p@z1}cw$%IR%A0c{H`y1V|}UH@If(EIIK#D#BUz5 zh@}VR zcD}b$r}&&wJm>o&*IWJgJ-A&0-wD}uDuYdr3Nwy;f5fs#*!ABsGJm%q9FvGe_f>9J z+VqN~2cK2-CgrL_%li3>04F?hI|QaZo$+WkO3PdXr30w_UG!560_D`iRA@h~*3-ta zxmWHS$N1Y(kAq=aOjXU3RgrW0=QZY~l>?WwAOHXW000000hynlm}WA3`5&EMOC(%S z3D2WdPQ~&lOsvR!1xnM189NS@Uj0f>5_2lWCd;2dIA&nSF_NqOj4kqS3KW0j)Lm_+& zgHeQw&jV0PaX|rV2=hg{iIK`?Gfu>?j0UH0 z#9|D>Di#$zJb%np4*`8ttP0Nb zwH0u_Noy-na2DU~Qx}VGK8pb$#9I16AQ}oPHVrn>;yS_ELqX6(jc8*ICC{yG1rZVU zzftt+;r5(obUFXU06w|a*NLz}y^j3~wRR@lY{!+3N*2+$k&_CUBDa_OdrwFkYPr5o z|NaWOfEtwq^tBatcoK0$g#1_z95}M4Sd#?VeatxTkZhIK>H})uMYjp85FwEpXEpp& z3$xWZhnVJK@{WQy*851u#!ny)6`aXf#Vu9_I0Sm|NqJ@#7ibvHIXMrkP}qIrhWSuy z`VTJPNlG6}f%*8v^nI$|L^$9qy8^EJJHF-OOx1QhEM^M%drliNjAz{ps!6G-@98J6KAPgpYAL1CqW2e9Qo72;xt zX6a*jq^+lA1^qKh3CI<>7JLz2ZJQ)TyqOk)s*k`Hzw~_(B`6~X{V9fG(Y?g3l;D8m zpgdt&sXiLJWvI_ z=<~(&il*o(YoE)Jf)HH=_-rS&hxX&*02FX0AMDMIfP}T6WZA1%Q2Y7<)f|%hnZxA< zb+Y~jV`y8vb}WygC9r0G9#sezGbfFi@tPE|AYJ=YOUAx7xgET9!~h3fw0h{F#`gAV z6J@Aaj=+Kj!2=5(e#H2C(uM|LV)UQDOPZZXgFyifI#|=&LrkCM?Hb2^o)%xDObsaK zfuMj&*yASnWfA-tBREzIK5!@VYC6v)+==#BetDh;8KSRtlbB^l+?Rmj;Gk%T?t^s( z8j1S5R7A%tx?nrz8YUqKkL_D42Y8g9fBv}UK9TZU5C8i7Fb{(CifbVASHxH1I2%eg zFrsGV0HQf>UKfSWDst3IH$Z-gi}kWvMr5&|L6;GNxSav#$?>IR=b5oyc}Zur zh<@TKUhro*uoXZHMt|W);PjvT67bE@8-Hd@UO^)NLn09qO&rrql}k5-(dhcn7Fcvu zY_*c(3RueV1B_${KOoLm5bR`vv2-PG=$>O?e|Ls+58_Fi<3s~24Bu^rS!%Tnpbm_{ z8q-AH+uhQ!jLt&4PcrXwnCQg_megBa|=syGVHK4}ywH0vG ztaXkw3SEbF$2@iVcqZVg0dalE<|+E8g!moZ+C}L#f`)i51S$Hz=ag+gn>(BWZ-b#< z9Oz=h%=035UZ9SMe{mgmeI3}A8F&`t>-BAv#FS`M{n_g;CcU$M~Me`&GlH-rwODWMYIo28xy@2ASk@^sg8B; k{p`E|!9v?XcA$-*Vx>8vUH~J}jvOJ6gxgj0v>tia(5{|$_5c6? literal 0 HcmV?d00001 diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java index f75ffc08cf..0913d456aa 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OggExtractorTest.java @@ -38,17 +38,17 @@ public final class OggExtractorTest extends TestCase { public void testSniffVorbis() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(120, 120), // Laces + TestData.buildOggHeader(0x02, 0, 1000, 1), + TestUtil.createByteArray(7), // Laces new byte[]{0x01, 'v', 'o', 'r', 'b', 'i', 's'}); assertTrue(sniff(createInput(data))); } public void testSniffFlac() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(120, 120), // Laces - new byte[]{0x7F, 'F', 'L', 'A', 'C', ' ', ' '}); + TestData.buildOggHeader(0x02, 0, 1000, 1), + TestUtil.createByteArray(5), // Laces + new byte[]{0x7F, 'F', 'L', 'A', 'C'}); assertTrue(sniff(createInput(data))); } @@ -66,8 +66,8 @@ public final class OggExtractorTest extends TestCase { public void testSniffInvalidHeader() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(120, 120), // Laces + TestData.buildOggHeader(0x02, 0, 1000, 1), + TestUtil.createByteArray(7), // Laces new byte[]{0x7F, 'X', 'o', 'r', 'b', 'i', 's'}); assertFalse(sniff(createInput(data))); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OpusReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OpusReaderTest.java new file mode 100644 index 0000000000..a0cfe9a00e --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/ogg/OpusReaderTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.extractor.DefaultExtractorInput; +import com.google.android.exoplayer.extractor.Extractor; +import com.google.android.exoplayer.extractor.PositionHolder; +import com.google.android.exoplayer.testutil.FakeExtractorOutput; +import com.google.android.exoplayer.testutil.FakeTrackOutput; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.upstream.DefaultDataSource; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; + +import android.content.Context; +import android.net.Uri; +import android.test.InstrumentationTestCase; + +import java.io.IOException; + +/** + * Unit test for {@link OpusReader}. + */ +public final class OpusReaderTest extends InstrumentationTestCase { + + private static final String TEST_FILE = "asset:///ogg/bear.opus"; + + private OggExtractor extractor; + private OpusReader opusReader; + private FakeExtractorOutput extractorOutput; + private DefaultExtractorInput extractorInput; + + @Override + public void setUp() throws Exception { + super.setUp(); + + Context context = getInstrumentation().getContext(); + DataSource dataSource = new DefaultDataSource(context, null, Util + .getUserAgent(context, "ExoPlayerExtFlacTest"), false); + Uri uri = Uri.parse(TEST_FILE); + long length = dataSource.open(new DataSpec(uri, 0, C.LENGTH_UNBOUNDED, null)); + extractorInput = new DefaultExtractorInput(dataSource, 0, length); + + extractor = new OggExtractor(); + assertTrue(extractor.sniff(extractorInput)); + extractorInput.resetPeekPosition(); + + opusReader = (OpusReader) extractor.getStreamReader(); + + extractorOutput = new FakeExtractorOutput(); + extractor.init(extractorOutput); + } + + public void testSniffOpus() throws Exception { + // Do nothing. All assertions are in setUp() + } + + public void testParseHeader() throws Exception { + FakeTrackOutput trackOutput = parseFile(false); + + trackOutput.assertSampleCount(0); + + Format format = trackOutput.format; + assertNotNull(format); + assertEquals(MimeTypes.AUDIO_OPUS, format.sampleMimeType); + assertEquals(48000, format.sampleRate); + assertEquals(2, format.channelCount); + } + + public void testParseWholeFile() throws Exception { + FakeTrackOutput trackOutput = parseFile(true); + + trackOutput.assertSampleCount(275); + } + + private FakeTrackOutput parseFile(boolean parseAll) throws IOException, InterruptedException { + PositionHolder seekPositionHolder = new PositionHolder(); + int readResult = Extractor.RESULT_CONTINUE; + do { + readResult = extractor.read(extractorInput, seekPositionHolder); + if (readResult == Extractor.RESULT_SEEK) { + fail("There should be no seek"); + } + } while (readResult != Extractor.RESULT_END_OF_INPUT && parseAll); + + assertEquals(1, extractorOutput.trackOutputs.size()); + FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0); + assertNotNull(trackOutput); + return trackOutput; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java index 3d9b4848c4..30793e2488 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/FlacReader.java @@ -46,8 +46,8 @@ import java.util.List; private boolean firstAudioPacketProcessed; - /* package */ static boolean verifyBitstreamType(ParsableByteArray data) { - return data.readUnsignedByte() == 0x7F && // packet type + public static boolean verifyBitstreamType(ParsableByteArray data) { + return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC" } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java index f41aa0bbc8..6593af3bfa 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggExtractor.java @@ -38,20 +38,19 @@ public class OggExtractor implements Extractor { ParsableByteArray scratch = new ParsableByteArray(new byte[OggUtil.PAGE_HEADER_SIZE], 0); OggUtil.PageHeader header = new OggUtil.PageHeader(); if (!OggUtil.populatePageHeader(input, header, scratch, true) - || (header.type & 0x02) != 0x02 || header.bodySize < 7) { + || (header.type & 0x02) != 0x02) { return false; } - scratch.reset(); - input.peekFully(scratch.data, 0, 7); - if (FlacReader.verifyBitstreamType(scratch)) { + input.peekFully(scratch.data, 0, header.bodySize); + scratch.setLimit(header.bodySize); + if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { streamReader = new FlacReader(); + } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new VorbisReader(); + } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new OpusReader(); } else { - scratch.reset(); - if (VorbisReader.verifyBitstreamType(scratch)) { - streamReader = new VorbisReader(); - } else { - return false; - } + return false; } return true; } catch (ParserException e) { @@ -83,4 +82,15 @@ public class OggExtractor implements Extractor { throws IOException, InterruptedException { return streamReader.read(input, seekPosition); } + + //@VisibleForTesting + /* package */ StreamReader getStreamReader() { + return streamReader; + } + + private static ParsableByteArray resetPosition(ParsableByteArray scratch) { + scratch.setPosition(0); + return scratch; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java index c17d4b61f3..ba7f7ba4ef 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OggUtil.java @@ -87,7 +87,7 @@ import java.io.IOException; * as argument. * * @param input the {@link ExtractorInput} to read from. - * @param header the {@link PageHeader} to read from. + * @param header the {@link PageHeader} to be populated. * @param scratch a scratch array temporary use. Its size should be at least PAGE_HEADER_SIZE * @param quite if {@code true} no Exceptions are thrown but {@code false} is return if something * goes wrong. diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OpusReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OpusReader.java new file mode 100644 index 0000000000..f2f620a3a4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/OpusReader.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.extractor.ogg; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.extractor.Extractor; +import com.google.android.exoplayer.extractor.ExtractorInput; +import com.google.android.exoplayer.extractor.PositionHolder; +import com.google.android.exoplayer.extractor.SeekMap; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * {@link StreamReader} to extract Opus data out of Ogg byte stream. + */ +/* package */ final class OpusReader extends StreamReader { + + /** + * Opus streams are always decoded at 48000 Hz. + */ + private static final int SAMPLE_RATE = 48000; + + private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; + + private static final int STATE_READ_HEADER = 0; + private static final int STATE_READ_TAGS = 1; + private static final int STATE_READ_AUDIO = 2; + + private int state = STATE_READ_HEADER; + private long timeUs; + + public static boolean verifyBitstreamType(ParsableByteArray data) { + if (data.bytesLeft() < OPUS_SIGNATURE.length) { + return false; + } + byte[] header = new byte[OPUS_SIGNATURE.length]; + data.readBytes(header, 0, OPUS_SIGNATURE.length); + return Arrays.equals(header, OPUS_SIGNATURE); + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + if (!oggParser.readPacket(input, scratch)) { + return Extractor.RESULT_END_OF_INPUT; + } + + byte[] data = scratch.data; + int dataSize = scratch.limit(); + + switch (state) { + case STATE_READ_HEADER: { + byte[] metadata = Arrays.copyOfRange(data, 0, dataSize); + int channelCount = metadata[9] & 0xFF; + List initializationData = Collections.singletonList(metadata); + trackOutput.format(Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, + Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, + initializationData, null, null)); + state = STATE_READ_TAGS; + } break; + case STATE_READ_TAGS: + // skip this packet + state = STATE_READ_AUDIO; + extractorOutput.seekMap(new SeekMap.Unseekable(C.UNSET_TIME_US)); + break; + case STATE_READ_AUDIO: + trackOutput.sampleData(scratch, dataSize); + trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, dataSize, 0, null); + timeUs += getPacketDuration(data); + break; + } + + scratch.reset(); + return Extractor.RESULT_CONTINUE; + } + + private long getPacketDuration(byte[] packet) { + int toc = packet[0] & 0xFF; + int frames; + switch (toc & 0x3) { + case 0: + frames = 1; + break; + case 1: + case 2: + frames = 2; + break; + default: + frames = packet[1] & 0x3F; + break; + } + + int config = toc >> 3; + int length = config & 0x3; + if (config >= 16) { + length = 2500 << length; + } else if (config >= 12) { + length = 10000 << (length & 0x1); + } else if (length == 3) { + length = 60000; + } else { + length = 10000 << length; + } + return frames * length; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java index 5bac358dee..8fd8b77565 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisReader.java @@ -51,7 +51,7 @@ import java.util.ArrayList; private long totalSamples; private long durationUs; - /* package */ static boolean verifyBitstreamType(ParsableByteArray data) { + public static boolean verifyBitstreamType(ParsableByteArray data) { try { return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true); } catch (ParserException e) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisUtil.java b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisUtil.java index a325e4e03e..e392bac7ae 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ogg/VorbisUtil.java @@ -121,15 +121,23 @@ import java.util.Arrays; * * @param headerType the type of the header expected. * @param header the alleged header bytes. - * @param quite if {@code true} no exceptions are thrown. Instead {@code false} is returned. + * @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned. * @return the number of bytes read. * @throws ParserException thrown if header type or capture pattern is not as expected. */ public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header, - boolean quite) + boolean quiet) throws ParserException { + if (header.bytesLeft() < 7) { + if (quiet) { + return false; + } else { + throw new ParserException("too short header: " + header.bytesLeft()); + } + } + if (header.readUnsignedByte() != headerType) { - if (quite) { + if (quiet) { return false; } else { throw new ParserException("expected header type " + Integer.toHexString(headerType)); @@ -142,7 +150,7 @@ import java.util.Arrays; && header.readUnsignedByte() == 'b' && header.readUnsignedByte() == 'i' && header.readUnsignedByte() == 's')) { - if (quite) { + if (quiet) { return false; } else { throw new ParserException("expected characters 'vorbis'");