From a9f2c1ff99fb99945a5023bb9f41fda0660ddc06 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:36:10 +0200 Subject: [PATCH] fix(player): shape ffmpeg demuxer audio extradata into media3 csd layouts VP9/WebM-origin files stalled at 0ms in ExoPlayer on 2.17.0 and fell back to MPV after ~40s. The ffmpeg demuxer published raw extradata as the only initializationData entry; Android's Opus decoders consume their first three input buffers as OpusHead/codec-delay/seek-pre-roll, so they ate the first two real packets as configuration and silently discarded every decoded sample, pinning the audio-driven clock at 0. Vorbis (unsplit Xiph lacing) and FLAC (unmarked STREAMINFO) had the same shape divergence. Shape audio extradata into the layouts media3's own extractors emit before publishing the track format. close #2088 --- .../androidTest/assets/ffmpeg/stereo_opus.mka | Bin 0 -> 19537 bytes .../assets/ffmpeg/stereo_vorbis.mka | Bin 0 -> 11583 bytes .../FfmpegExtractorAudioCsdPlaybackTest.kt | 132 +++++++++++++++++ .../edde746/plezy/exoplayer/FfmpegAudioCsd.kt | 134 +++++++++++++++++ .../plezy/exoplayer/FfmpegExtractor.kt | 8 +- .../plezy/exoplayer/FfmpegAudioCsdTest.kt | 138 ++++++++++++++++++ 6 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 android/app/src/androidTest/assets/ffmpeg/stereo_opus.mka create mode 100644 android/app/src/androidTest/assets/ffmpeg/stereo_vorbis.mka create mode 100644 android/app/src/androidTest/kotlin/com/edde746/plezy/exoplayer/FfmpegExtractorAudioCsdPlaybackTest.kt create mode 100644 android/app/src/main/kotlin/com/edde746/plezy/exoplayer/FfmpegAudioCsd.kt create mode 100644 android/app/src/test/kotlin/com/edde746/plezy/exoplayer/FfmpegAudioCsdTest.kt diff --git a/android/app/src/androidTest/assets/ffmpeg/stereo_opus.mka b/android/app/src/androidTest/assets/ffmpeg/stereo_opus.mka new file mode 100644 index 0000000000000000000000000000000000000000..a868c100687e14679cc4cbe3ab0ceea8b9b444e2 GIT binary patch literal 19537 zcmb1gy}x*|Q(GgW)AvS3r%#P6PM;e&oSHgv6HAKni?b7*+8bG%S{s=pf*aBq89=~C zR?v5L$$|YXDLyvKeRp>UuWk|bT$vWUrg0&JCl<0UJ9tgwN8jB&!K+(@LJp?~uW4dT zg>YqpcV`E$Y4Txw12K&u7-S}uF~~iq_qQ0^#;no0zOk9#$-%dw*C(+o&CE#8&`8hF zz$n}im0!GMLd}=5#6OqLOI`mtNkTwkqlQaohr|2?u**O`dcVIVR#{y^yM^V*7KY|VriT;f zH!^MQaClw-HZ>`wQ6ThPBSZ3{$^HeU#U81NDU3`Oj19ZNvY>EtXx`s4xv%SFaq)uW z0}bgXX1OkIclC4jcX17JX-D%w@%7{b-N^_SrJq=Yta_?XVp+18k)DwS$omR8nMwHt zrN!wd7P>C(a0v}^4Ds~$b7`M!U|o^42vDD z8yOfH{?sweeE##zSHWw-f35y}wJE7^W8AyVMo&a3rs*f=jMedW#!q(5FIDqUX8Juz zyYbLomD5Sq%#Qz+UdVR-(ha%sxXf_TZC>77jo`y-0y->vA6UBR7XMlK-P-56qWNEe zRRK8?3a7%^jrn>HtVl_h>G4-Peet2C$(LQf7Msm1II_KTL0I#F`Cm+gWR`S$EPs&W z?!6}Bmif=qC&bpLzpXx-AjtW;{<#LP%IDtPe81_9HrwRQJ{;u|IP4(5>BfSq96ezx z+U+_nOk!XDsCQ%Klz>kv0VO=5fpU6(1ahxWQ3zuFoGvX^t1;_E+ls}XGb0-&vQ7WI zpx3MY(|5W2*A3HzYQp}h1b9nDx7XxtFFpG9QOvX?zt=8m6+2aZEi5Zeii<4fSovi# z&pr1`_n+KUP+{_lU$7`>^3Ms29S$}!h&KG$xOm@rCkO6W4*eU!T|S51-KrX2u6O@Z zdG5t6&WZ}A+w*)Th_jdN(CKFH{M1Y%fvk1PdoL}=+)#I{!adX z#lCZh27ErUn&+Ld*{k{I@1&%v+%*<{m{f85ui->isg^d@A2|z-n6*zT&U4h{Z3|#x zb8n3Q!X59pEXbVcP|NL{m+aKo&Ly3EN$S1)SXvvkpoPX(T0HB79>t*0>9|7;A6Td-j3 z8)aj!Y_G8I?-o03X=Dg&__IO&MGgKr zJB1%kyV%TXU}fjtRIYvK8cWXd{`DVsbk7#v(vcA2iFny{pP%xx6UN?zFX|Dy^$dm z6zG#xRhDGeK215Nrn773jGqZ&htDvi3AElbluZd1`K)$BWYz0Gv0ahRk20H_yv3Nx z{G6+48wblrh8-I_r#w%wQ#n@oRlCLTh7OyHAtyQ4xZFo#<57X4im+B;uRoeLO>gf8`w2hzZQFA6l#sQtL`12&$=6osZY+?SP^U#w6(V7-ldW<~|?--K!Fm1<>Og@3R7z>@#R z#$w*Z?J2c$SMe@6BXO?%bNL1LKgN{|4xfdO-I;nvUCd~T(<|p((f^@Jr8@*aGO+)X zuGL@P{#VF2Z*$zcQq6aZ9kw+x^nyZtwr|L-YZaUxZ{pwQe4jlp`r~#f?{|-ODp+@I zJY4bMLEOt#R{e9VpKq+a_Uf2LVe!-RI(E6mM}!j>?)##@RC}rb`uI%6cY`i|F?bOWUQAchjIlR|odg9N((5BUUrrBE6 z@L{`&)hp=dWFUUiSY7L;c^13Z@3$nRov`H|%JI zB$tH^&^$UvciHt_!KRuTt8|w4Y6l1`+Vb9sX59jiw`NT^XD$$Ue+MjvY4ZK`>b8v zzOpNvCO&Ssm-I;4w&W=T|G$LL@b3?AVc>k`&qT9nv>+5q|9=z-Jo2vh`akdNuH-049^Kl=aI)df20dsV%{a^a=WgMwqgm39ON@T1ExOn-^?788 zhqha>##IB02SqkZiY6qoIn*5gbbd+!!xOC?)|TtmO@93-V-3eYa~D33u&;GWc?X}^ zGuzx#++cNSqw|#Y?T+E84LPsx*fM2)Jv03zFXtD&h>i}ueY-W^%)NJ`YTCV)74dKF z?(3XmQIq?SBEEmm%-$;=UkaU_L$(wb7VdD}T_d;u1G{WRctiLrC718e9J;lU;VwML zK{?dw&IFDrlew%XoaH|Jq@*l&C8z%Zo%d2F*M4X6G?m%=CHF$R#U;G@0v&Ygmxdes|LWc~%QE&cQ2*U zTr*a$v@A;Szpz1OgTNnuCI?nA^RJe>{#mAe2;s2JpTGbAuD+}pC8le825Ry?T7CPll!C*y%?b;ro=i{EbZt(Yb-C(Xsj+tD_4x8X z|J}DXZtR)3^5@PaNB>{QE(^IZac`E8XWZexmD&fkUsZl)XP&>dHJbbIlhrk=np!;y zmEIftHC0 z7p@mNCUNndc+K_ch_5==O95Gvzg;~2Tb?HGxuxc%eJgSDA2uoPt>ya!G(;CpOHTfr z!|hzoaUn#RM3xv;g<&`hwKJWoHwf3LHx2 zR<(bjV4iIyP%~rq{^HhcUsP89e(${Q`{@#)Ew9s#zuFwSVwV3Dt7QSn-^1cQwY2)r zw+H3TP>t|c4Dh_Uqmj`O5$Jle1eYc*oOWE-XR~TWc*^JA@Fw=ev$ws@)`m)^OI7e4 zXzyy)NnPT<5D%YLmNS=3Q0OyI-3#rA+nBg2yLJ zYik4UuB^DJ^-e%T;%ZzLU*(pzRSUWrLUNAERP^jzcg8^={;S%%m-&46FZ4ECn#N?e zVRibNRhA7GcUi7kyR&8A=lsvCHA*%);Ou#U{V!7X+}6k#3JP>c_GIsEXSl?>cv_bU zzr&`0m@Pd0lLPjA5q~DVbYaq(t#w}UlS`UkeyzKC#x?kj(_jAJ+xxPVV*aw;{=nQl zLq&JRQ-hqyqo-PGI%8$bc0Y=|?D{{NBk%KB?c%>je{P+S+Pm`N*3WIfE_j@}(yN;j zXINJ9_)x?vj`N!%!-Tdh&~;3xcYWX^fA9E}?|TwjzB5NyJ$DPRz0<}U0Lq}X6&0)u zNEvinBV#7S;S(8p&L92N+R!EbL+{1rRPlgM^|IgQy!^Ir&bi4eP6(-N>tE04Y&@s- z_SD8irt=c%_jP%8sQB5ZRQUL+Ij%@#70lf)WK%OmD&TVI^L5eIbIQ}sgv;EYvQOfU z&x*Nk?`M<>vi!9?bM@6t%WLzNt$UXGAwtzRRJMDItd@=VZ9DU&mMt5entwTU(LJ~H z>4My_gu@FQq-tUpS#Iuio$0{*e{OgKEBicf4h6Suw>C1?!V=3$iLDMy8SUTiKdCqW zB5#uzCtMeEQ^Y80%ZsV;!f&?l`m8$KdvWWEZ|SnqPHbNmNmeUNTb}Xb_Py(seq0|{ zIi}S9QJUWxGXIV#gI~mD_Zz$4Y{;Bob9X)Sgam_7sbU=izUNbz1ExDlNzYWfpQ-!i zs&#GdLZMdH%7B%8BF+6;Cr+|1opVEaYuMaXkNGw1`KD_3=zVB6Og8~%&rtSDf9sGd zPR5C#04JC|*Y%3y%$_dxy{`WDwI>d$7PLI9KYufBdHubpOY9SEuL~$HDeta&uyw|g z{kLavnZ{{tuvm1HWB#W3Yr1*3!)0{#+Sgb->v>%zG)3U!Yn|GEtAvjTvCPsu5LABQ zqJIC3uiGAOvDrD_{sFseMX1(wq|CX!k#Q+3yunRd_mfNidaET(f3vj9p3~!zLG_Kd zzFQg_RU+DEsy04Ui~0PZ&g$~^f;OcICN1s(j>Zx5`yAx9O645-<#q2){*BXt>8Bn} zl01Jfe#6Xr7mr?FwRKgZ%c*NJH`BA{3vr8UPW^o0mv?b9%c)tK6K2)@opd+n-|5}& zcLnxT)~K)E{`T~$Q+iDXN3wYw<0j2iunzCHR@*Kzr?AQaQg!|nIXeSUbu#V*g*Yg0 zHkU_U{cw1vdqM5SoTlSy=?>P{Z!C$eJsrgyry8HUDgAu+V;<8xNfT0jtGclrdU|xV z-Kvj<1#B#QAC*5Z_s@1Hl$`e_^*Fy~pFm7yjrl5rBlB+6KKP*h?)e7I28H=sm70I%y4X8={X6t^v$>Y;&VA)S&F@va z*>2L#d9+reSH?qU?avAO=Y(G3y7_4HgV2_p@dXYSQ>SzMWDHn0ZyColH;0!~uGnn- zrf%%X*0)yX>xR>tKXP_{Zpn1c56!Il{evy6eZmIyC(9=v_@cV}<<6KH(SOwEE6E@H zb--)kHp$)_F@6snR_O+ByYKWr`S(JN9Y-}ll_w}`>Ov|{aMs-3$oLSERNgtXy{oZr zInU{$daML+ZUA`;$G!m zcYL``Zs^u$_LYrgs*55oiBHqYRMxt|v35(zJB<{9>+7HMraqjI@7X^sLOa6bmeSQ3 zFW&y=ZrS$f;0C?01v}o&2x;eXJioKY#_ROW&Cw3rUTO)8=J8EnO1ier)d!nxSZ{K~eYp_z zeJNk1_?&I`IfQI4%!B040DWlgTng?3F>%6!{OI4KZ*1Qm*i)PHHF(J^=MCQ8C5D$C zg#KMQt0kvoZQqk-`F#gDW9Kk(&r_{!TI1pJE3`=2&%!!2WAj7tgEyV_^W8HJ-B1#> zeM8RGZgunZ*Y9Z*nVra$uKB5Tm~+M1^*@t(wk$e-WYW9YV!Ae)_X*S`Ciph1zj(C0 zgn>Q(h1-D)OWXaiLUSsgh@aN?5c#*il6^~M;H;HfO+NpDr}2+_I6&1BX*TBl(UWq$9l<*^F^zH^^s@7@vVw$-fCKK1QG7o&<}?Csw(|7!cM z`f>E-TZYxii!buX%$KN{EgIsn}79ug8P;%Pn6gk8Gd$4pIn|2q~ct)s`{vXX^xzAkob*T>~5FX z*GTXG$52}lz{=ExXxVOSWHN-f8`QF0cH`}tTZTFj8%3LMSnOW<`Jw3InLPp1)bx*C z?NOOt)AGhSC1b08@u#}OCnJu3`0cQMVO*()R_K!l;+Zi&C$OgexWYJl^>erO**mx2 z&%Gt}$!y6-hdowbU+VGnEmTyFj4E|?x^zV3nq%~g5Ayp}-DU0H_=x)3KeYLAZ{|Fa zrN_BkK2}{_^t=A)@i1pIZQXd=>)OiV+SQ*R6=(ZjR+QYy=HUMb79mf)6=`2 z34Y6DKQa46v@d@g7eb+O=lIWFL&TRW_2Aotmg5qb@pj?`v;|ud8hkp zrP(x{apt*o%R5R%#fBxCm*?Udy$^F_e?qFxAk;qRwnnB%P>@rUJDs+NEAcY@jBoLk z*lb+pwazK3xtV3prKtGX_t@?#1oOHd=_|CdOkrd=J6ZBY@}fybE!w40_qe0g)%4!q zd6*}xynUgjLr?CrFoR#Ll{L-prrq1sy7u!V??d71`RecP$Vi9ePLzi2_C}^$c#t2% zY}hvL?=QdmZj!S=ugKwBXJ$YD-#>Ym+joY-+QO$Athvo=c1tDInjf3}EmYu+c#qbJ z^vN3M>mRi*d9(P`x&F&x4h}y=HU3s|Wi68Wko_?JanYov^z$wDIeGI{|6bu+q44Ga zhtjL_)|L(rZ~qe9AQg15e%+~Sftvmn`!0*!<5yUlv2Mn9?^4V3PvG2nf&Kp!MBTZq zk*N_BRb!G{f4VJSGj#&m5u7`BL&kP9z0`8 z{8$#Y^aArq)5V#fcC9Q%yOwDx#NqhbwM(~N({%ZjSou)u3ilbc9DU8R`)f{zsn7ft z;1?V6UbS!Dq!`1OJHi*UzvSHa=JO}sfGfWe9z-^-z35z%b0$)0+v7)Ufs&g7T2j~T zQ?X$!oA}k-pz)!qyQF=v&F6RiknE|y^iklI^e0WLNwKXz<72nMq}Ll#dA47u|A%PVZfj)P3kq^*_QX0Q6#DWu zfgz!rRxAgl)G89zPi0`Kxh(KuYPnF!>acyUFE+Q9l-h;p8uDA_mv;nS{XK1e@SB~D z&hL(}&R;h9#66$bYbP&g=PkL{vXsft=U~I z+J11;HUPQbxxJC;A}q*1fwJe6?{C*@M=y=cESh?SAw}8AhhIk5Vv(Qk>z)JVQXc2l z7M8tPu)Gkohpkw zc|XiNC=UF-6zcc~5h+5biJFZ4DiEb`thXCC}? z!Q|hOe^Xfcqq{cWHaV7ePi{7A5j1;VN6ns3K|v19o>)eOt}Q>XF;l~(#8V^ln}tvC z#l;H(-z%;qedH>k?GwHe~Pu!l>`74|xUL}=ojttXvOpwb{lB@i7!_)Te zwQ}_~k9aX}o0PO2cE|ZSE=~qFZY!9YP;1Yh5Qk%}J>{xXa~?cDZ^OLNy~1tsk@fd- zR(vbH}wC@3nRB}}7+ovpd+%<|f z8ZT)6e0S;`%Xnj>%7vYOD#|zIm?}ukRqhe+a@BS$s#0-3RQYkQ)tX}O+Bv)?KU^iB zt`W(a^=0F&O@;T1y@MM%r}jW=Pn3b7?TyU5@U((;$hIM_d#$;lkEry?*aO)=rCbxX zpD#CZ-tba6f1cXx?n(KznWpj09`Cyf4}P}%`h>-6%^soEr>Xq_mGLl@A|Kuh(__Th23=4&?K+T@artl!AWXRU6wey>{!j)w~ z-kVq^8>W31`*tU5_bGumsZ*A_<~@D)VW$FnSJ*rCx@8e*Ws-j-i`vrnUIgdQZ~H$m zmM2X3qMTr|v~l&yz8fObuFrj!zhQUM<^`ZZ+uUjaaahk2dC+!iBeN$c$RXJiy={Bo z$bv@|5rr?i`}92aaBQiHHnV-fCN}L$gIwAqyC}|sH>dE}X#`$AD_&H-xZSCaIrHZ# z*7qTmKCd{h`W?TatIqyZM_4ha{K(aemFn@9J|aI-A7(DvvL*TH!*womKyBMaTf=Wc zb7#Pcde;X&6B&;b>a{IrjS2i%mL+QWV(~2AE0DHr1ychTqHVjikvSIPZb;YDwx?9N z&f-V$p4o>^mrYXHIOR#9?v>@sRvi*pA(R9sKFu6)x{&|yv ztx&7N9t-nhm;W$dk{8nC6>ARlQ&LY)O2D;U~8HDbJGr zFs@)ac#V4!zgUr6?a8jQi9S4@4iAp$#7$(i*VEi#aQn-iRqiS?KmQbR>EO?VWX=Xw zu({OpDi9| zCNiYI@OnFOMxK~}!D*qJt6GIV@2u-*uXmXHN8#(IYA;n;)3cnW+B+g1KX6@Nwn;CU zbJ^kPGgmoh2VJ?bZ~r>Gt;Y`a)*Ooe=~%1qV(Im|5G`ls@Y$FAK8Zb3Y%BW^@I<*q z|I)MR#X>snp{MrjRDC~v<0D(hg#+;g}16Yq1=#LP2JpX;lA z3`ztgFMFQOPJQ@0_lJt9QS{>JtKR&v>et!0kz?jG>AiC2e{E6N?<9HXmgoI^r{ImD zi(Bu1auBwR-Wza2;D4`Ck(L4LrDT)syOy5=jo4OQg%1irx}D535dp3@OAxK*w2x{F zEj?CimpuFami5Q}>(^chwzj!d^O<{$1>EIl-Hg?o<1wBU)i zWIY$US{K+HoSW^|cJJ%qr#__yS6Ah5{rBCeK7IKOIZ2EBUc0RZ*AgO6ZTbDo!FA_{ z>dKpeNfx{ac&28CU+=8__K5lkNNYYq4^}S8t!*sGim%Tlmu+OnDW=ou07%lb%oT zjOSX{YJRmyK?ZEaBb|SE+MieWE%Nv09=@9#jzS{hjG?$1w#)}14hJ=Cj~ppGTE^4& z#PG#ezU`lSJ&Z0rOkUNU^8d_^)h7fNIM)bFX9&_=`gG-wJN<`ExOzT4c)qS?TR`QJ zX>Z<|=O1HyWfT>iqQb$;Qtr4t&J50$e=9-4L8 zS>g=$r((cHYKY!?+C~cDCW^_LNPO;&-AGdXv zU-;Re_@}>2cdQY~*?ninImd5YUK`@O4#sZd^5#Bv4&1PvzU1IM=QPE(T+^)^ZU)|e zb@cyHpEvqyJ%r8M94$7Q42P!MuE=-V-$7ISUwKPe?4;7{+8Dv&G=^VaL~H-)lF&+A#H0 zC|i%>`otXXiXn_N)kB4;!8!OvA5brh*j$~w(# zFPrsoiHo4cfrZojvK8Z`G@KauCbMzXDY_oMJUgv_k!!fK;sM^y85gFWt-2lJwRvjh zFNC|8__%$(xStzQ`>863~^>1AwSz|2`H z9Cf~>^$pxFmAfLvf9(qH{4Kn8)~OAj3k9>dSuKAZZ~pzt@@lAe4P$T&8<;&m+g-1TQqz1hk2E0)1RODvhPKemvqsN zMZO2?W2&{*-JI9xdtpi>`wLLT`8Rw8TE)p?2~Q}Pt=d1UE*(9dX!X5)?XGq93df&r z@S1({*;@l8S(U!~Hm%9b*Jgf+TbI9nZ_D0ql?obS?*#4Fiu7y0SG@3Ql5P20fyh%1 z`6+gW4~lE<<4P1qC=LZ)Vp%C9luMB?Z?;kFYJ~RKQp;uPg!o9+5G%MZ^Q5(9eTTD3f^pq z{B9BLY5n$v#}Du0jEBNjT9l_>+dccWkaGU^O((Da3r+H={lEJntWAqNUc0T4B@q_j zE8XIPDsKOHU8C}+MObO0{v!MD>>exnv;W^bzv%AYr3b%XyTy<_?Z4yA;~S1lbowAs zUiOkPLI2q7zXCDmmUt@8J~#bDZ!Tx}h9?#AnHL=L9_aXNN)|Y|c8B@qtccd6Q%)yY z%*yu%l|-LlU9$K2`i_Nf#TgX3-_}1+dUsIqh1-ElZ)SU6p*b9TYf@CN-`Q2Ez5Yn$ z&h#~+uWDf#^Vb*TbqFk_pa932G55I1$duYuPc;3d{rysyQ{%QX>#|?%IM95x#S<6 z#<>T0-!H#mx<`Rk{MU|BRcnr;vrcW8x|9)|F*{o}`@K`zt|gkI@?*Dl&^dPFIak-f zGiDZYi*iqozR7v6&t6lx5lVwp@9bI#s3E4{_w75h(}CrZ=WO)qoC zb|@%EJn)^iaM3dxX%7PqsasWQi%*}@J+?=V)oI@uuNm8ya3vW%4u5x^k^OD)w>8oi zp2RmxI4HNP%3Lyi|4~f9#l(BgXSu^iWtJW{cCM^aYMQ0)EEOqNxyd=$H~+u(JB#fZ zkV=#N|5~KM+O3T&b7AS^(=jIQKT>|0-(LH!F!^}y>}->>%?&C-W;PY-l zbxr3K?ibG!oXnOghs(_?zVJzwr%I>!)0fX{-ZY;OYFpr!*}m!8(!J-ZwmtAW{(c>g zKU2)|L}|ey!>)v?zMVOfE0noe3lE%=jjxpN-zv1yc9+d8-s05HYmc;AtM zPFtDVonGVc_2(Uy-ui!&HSTvGdX~{{wqT{URNM|JmJiO84>2?|#eckS*2leB7M|=IBrud|?29hsB;X^^78gpwS z%TZW(uXM|wB55M5Fvy=`-h7t_2?PhJ~W&OLs6 z@}8*<8t=;l)@W+?d9AjI_5B-* z+P72&&U-4Q7Yxak7uf%nAjWC8G_u?Tg*PSHlIwx*k-0}KRz5xEvwhF}H!Zzq6ddlz z=y5!mCuk|b8n|k;(?sD@rWJj0Pxgw;lMFf&`*?cirDzWQuLZBX$^<*!ZTd3h+_&FM z{ZsmcIn{fY$meAgoso{75Pa{N%~FP|SA@aCK;gd{kh0~rMwYh_XHNvragx~4Y+SYY zd6R+>2Sfh+zph6k6nwDcg71Rh@C#V87vFhTX$=1uC{Gr+0_?xB5j? zbN*3n$=%ddE#h$J<%^l>KRtK6+FZ)owu_-$WlH-(iOBA0`}G#7Zk{C7_T209@;Nrg z94x$8ii`f_L@yFji4u5U@@UPKq-$&&zxU*X?C=3kq9h&#^J7EI8ffh~jIeT2b@lLjUBb#(5wqMWWlWvb)TcgEY zg^cB1ksOT|dpy<6CP%*Bq5W(>XmL>Z1=uJMC}(bKWYt6tan!YvLbq&$cog>Mq*e9j zPo1}+#_xvxF=q4W?=9_f=BaeDWt1@V*4=AJODX8OUtcrzPzh7pmk{AyGd8_7e}DJ9 zXS4qF@ZYLu9)H(14SpSw7Wg7IT`Z|VR4klby!CK=%A&|qTYl}HnZSJUsPlHvAdpPu z{|Kj=t(<#SSuJN|Qu}o1dR%?)SBtn7=pYc<1ShL4D8NBk^I8RFPqQ;|g9hXIi8*%g%{lOJiPc=R3}+-@;tAH&VRRf{bQm|xm^l?is6PEEQZ#;ENj z^Odom{riu#0_Uu}Cmd=Ja~2F_y1rKX38czI9|U3zgt!}&H__X&IWNDjy6bY_B)h%! z0w;@KO_|n#xAi2{_%F)v#cDPiU7FD25|rJyP1wCb)nKEE>xH`Cf#2?3*fdGkA60kBN9UZ+v&Of4eq2@Y_hsl8 z(+ma^>7u&DM$t!iA9(m-PV}==H*6d3=7Out@D=RHRpz!v)>PaHMU;fL?CHxU#&bL} zRF)m-&apRM+Wh`_kAnPh75(W`C#B21Ju2-CzT~KEjF;{8EWG!XQ}pKd zSzMn4gCv4-?@eZ(#BIo9BzNuR!3o`SUp&w|((a@8;q(o^J)knPwKqq{VxJ5_`gOsstVaK?|0i{_urNmZSBd^;_-(B6lv`PHgUnQ?!&u_gyzW3(ObNvZH7Jjw7PqZ(m+0XF~ zeUiBIQ}q9nV%_V030?53;n*VAq*y)4G*+j-HR^HHjAl(|{k(GzeBSCUy8K4P;R1iG zw$pu4*Thhs{e``fGd}-(TOM}#JUCw~kApBEom# zY)|(vm}f@5w%D4zWNzNe$v-Uv=6`z?6uJ2Y7jL`jZx`P`u`~avAY^vugUj=v^OTmh6%+`g5Ia%GEWXT2sEEf|UWO*4)y_3R*S}-jp&o8Z&2p zvnfZ-nQu4AeD=4y^xSX-)AmQp&j{>ZoqT-xB0 z{m&Y8OSUiluuAeq*n#~F-VbU5o3`-JS?NEy!sM~#1HS&BEuNxLo6d- zC&gFxK&Mz-d~x;kEBYyuwWGgJT?(o*|LVg#nc$htt&OapCF8K&D5&Yg_vMYg&gzRV zR&F!By_{oJ6L0;qMUISCdoSFWX~2KkXYZ7+YFzTq<)`;uDE=^w{l^QvxNN?{&7HkZ zXHEDtBWWcctKyB$=~qN+cE7IpxL5qnnVZXE%)HZ1xhfV^*H&(O7^=PnG_kq+Lr5Fj zdW)867o0N#x^lB~HoV$#%D|K-M*39Rnp=OZU=t@RYEf3~v4WP2<6Y`bU|w@U=#kB> zDRuKdTr*cM>brQb{PoNo+L25C-H$v!J!a`I^|seRD`NNk^;uX^AF-3yFj98Myw`E& zfp7k=zL~|)R;?f5<%YJSzT@6_iq>nOh zvaOL7v}7C{-k*-?On7%Q_uBqDQ}1~!44=}c@UQTEYLV)7$&G*VcOICbk$>}=!L-6R zB0HblUJ;@bKP59z!7*#@78Zlh*G}7c4|>ip{~zqP@sm53OrfOb+=6e(zu&t{+LdWv z6ZbT6eg8~K--C7eOgp9Jl@@;Py$W+a%-tyRx9(U4xBQ~)502@(Ctun5@8%;XyBue} z-RX5Q6YbZVg?~6;$`b{d)m&STJT|+vkqx?N9JCV!v&JNPZ1!A7t-_+`*Zbu)U(U?y zvD6D{=TtplI5S`5*xp*sL-z{8YoGG&nUiUJBVP1!*%9X_C10i;((?(PHRBa%QBWA; zs_IKw#b1Rv)qg(mUaw^z_2labr^h{@PUZ=0HRiTPHqfGRaK4Hi(oNxM2~or==sJqmL^tqPoMQhYkFpnuWg zoI;&d4)y!Km42PPSVkl*bzVJaXcjc7*_V54^VelJ?nwM~z5B;fc>hKl)3u!rVf+U{ zIdj(ac9bPS+Zx$GOU5B76NMsI0Mui_LS%x}%-e{T5y;u7nExTK-&c@x_pN7k+BpSn_GXtQ&#X z;zG`RkTv8n&b;NF2yV=7%9-Dz_n~ifFnC1}<}MUA(28-wSrcD3^T_<(yUX+(7tH#+ znk}vE_hhLH4c%UBy*HNifzL|U9 z<#4ac`&~zOgSwedXH9mltl6M`HHB4GpxSo#yea|3>I3$-pQ9}Z0ySp0H?o12i$ek& z>xu&&?dKL~D-QlGe}q_Z@HP7HW7WgU6oNVo{N_pqRnMPPxorA*zh}X3MUPB;_RmJ_ zOvFa}#trdn&m20XSnKE={$~Eqn{#5$8mqmjc0ItMbobKzCpS7bLk4EAUKM^DCz^6D zp6_BQZ)(v~pKvv2jdKSfdGi8ms0d>GXL}2E3?VIamzfI12@4c%?}5jxQHL!?opr9*syA=S>5d4RqX37Hg#luu~+zb zVhZ03*K3-)7g&g2ndwu-t7Uzxf5x#>oNxIV44t%s*J$5dv-)|CsqCaD_KQBK@xOm$ z(RG|@-T%C^HjYf|a`xsfegc}*1lO8KlbYKa*+2`%p}7-%2q@W2jp4>xLR(RmhF^a1 zA@A0wR$H5kfy-_Gy=6Vn-?vMlPfpK0`0v`p1s~7sH9oXZUFyqujtsFocMpqBdU81F zhF71dAMa|%**j#vf;yW%DJLd9o?W=+>C0{AO7;Oe&RhJuX3AalBgJ*$Q*eV;A2oMw zZDa$j7zZa7jP@0wdUM|8gN(M}uliGdAI{phOXP4A)ARdbZ~Sf2&)Y6|*wond>_v>7 z#g3*JF|KMJ5rKcaeqF`81^J39S1oBg_+PpC#INGd^N+63{d87Tu~qHvOM^dHvMRsnQ9TSAMHI|7#albXin` z;=|*d6K`LbvcS#keewH}!g&f)f_u28tdCtbDNK}iS)X%!c{;ZC?Dj@B(0Xxjcw=7< zL{#S7w~f)vU}DUIoic09R`pCdDV9;YAf|D~ryV{PPfiL2R7dwCdtXw!3Oq4 zufLlAKFw^a<~(`Ac`=KwD|WAK&E4Y;Hf!JC_BG+|HG!iSjh-B}wD;TaY{k1xpH{fF zc1kRKKF>n-&F8b3tTu`ECZSal3@nK(Y>?5}TC`na+Zx$G3&z1&b8hq(7lOk;znh== zRoU@h6I|6(Fg3{LXz$@`KEd1(X>Z@y`b(3kUzBbLm#75pt$ z?!&z&lmCBvz2kkrG{ZBJtYwE1ZZrKUwpd?sYU4BJ>Y5k!T(gfY-S_Qrhg0Xtt&41 zum9e>X|g*L&>Z^8VcPZSGsnltYCR3u5;s3#NYB^CD+zuD&+4^(+`?T``t7fJ- ztkqo&-G&l?ynl0hBRgohI3&ce)|`aa>Vaxb_3S9wB@gaCw4zjz^ z?yu{-&6=yfp8apjwUw-sC)K>M$l&Oah7Qr9)SO!y*`W)@LvCT6(!@1$@>O4Z)?K8T zli(OmQUBojB_fCa9ll#x!TKnsqKbz*UAg~f0*qG{=xh~>&u;Ydrx=W-!Ppy z{fqwBlI0T5K*K3U}w*M&- zPJA1x6Lh(%!ropq<)fm1x;$^&?c6l)ss*bj6;5Dzz}NTd9sYdYe7TdVTvR zw@6CuxWD9Qar}FM)@}xm`-Z1HejMKpsx{|^xBt~enK@yPgpEpoMrcvT3q`kwnG#;8 z7c!}sWzEh5wROCyzgIr~^T)Nc`^%~wbM9ueC#_k0OlPY3zGLs(jPjo?JNLkGS}D(J z#(gfz$w~f~*R;I7P!ja+;(5`y7MmX}2`O#RMeat64;690<9qLX{`V?{&_qzJiQc5$ z(#W0*8kL}66lmQP*3xGh(`I>S8Ef21jb&Gw_36CMA;n3T&zUFgiFDUVTUWaJ-Ho!T z5*}>UlSCJX&wVPgE0fc1$6emHyLBA|K0G$kGGC~4+g0iRU(wS6N%Q|1?lW+kZ2x{j zWavp_^~~F#r3O-xHGe;-r;fZ*M%}3ANKK>yw z{E-r8z3SYDH}6hd_;BOh)f>OtHz@6Pc_$zG(msCU#e40Kt-k*>OLsD3_KL9ldQsC` zJ5u13PPwgTW8HbVJnaHGNtr5em3ct}dHcz>Mt0~raZr^><^uOqzc_Eall^#}Pg2n4 z(#IY)ZL^^4z^g({+k(U6mM@Fx6<~{gS^M(RO*eXZB8<6)oLj*Q+}$iWD(2=~ z3u~Xu6J{iSE%E< ztDr6RJy}r5twP+^`Iy$RfOsvF)lJv(%3LLDJX3;ld`>N3Or0^uUu3wmRkKCp?;ck}Un#*wlDq3Sed0On zdL)LK#pHV0&b#$1`ur}1`!TP#O!;X=W z68}@4itV1K%IwnrNl{9z{{yoeOYBwVFYEq4Z#rx9$!7-R{})$k{;l)oOjt30{qnaP zPWYuij+gwb;JaE~c}diNr3RLcAXS4EeW@}FAJ0|GN|VX^_3Xj29foPE3!IkUs1GuQA*;5wguZ+8 zZfi><%Wj85OP4t)S4}FKaO;ZZO2UqwzzFNxJIdh}nuqQOh)NrL8* ze<`z7SQ?_U%TBDC6clt~?d(SO(nT!{Xa6P01ns0i*x` literal 0 HcmV?d00001 diff --git a/android/app/src/androidTest/assets/ffmpeg/stereo_vorbis.mka b/android/app/src/androidTest/assets/ffmpeg/stereo_vorbis.mka new file mode 100644 index 0000000000000000000000000000000000000000..c9b24341773f9161e85a6a3650b3aff83b878029 GIT binary patch literal 11583 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 M@naL;jt`BT0OhRU!2kdN literal 0 HcmV?d00001 diff --git a/android/app/src/androidTest/kotlin/com/edde746/plezy/exoplayer/FfmpegExtractorAudioCsdPlaybackTest.kt b/android/app/src/androidTest/kotlin/com/edde746/plezy/exoplayer/FfmpegExtractorAudioCsdPlaybackTest.kt new file mode 100644 index 000000000..d37792c76 --- /dev/null +++ b/android/app/src/androidTest/kotlin/com/edde746/plezy/exoplayer/FfmpegExtractorAudioCsdPlaybackTest.kt @@ -0,0 +1,132 @@ +package com.edde746.plezy.exoplayer + +import android.content.Context +import android.net.Uri +import android.os.Handler +import android.os.HandlerThread +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.extractor.ExtractorsFactory +import androidx.media3.extractor.text.DefaultSubtitleParserFactory +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.edde746.plezy.libass.media.AssHandler +import java.io.File +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** + * End-to-end regression coverage for #2088: audio demuxed by FfmpegExtractor + * must reach MediaCodec with the per-codec csd shapes media3's own extractors + * emit. With raw ffmpeg extradata as the only csd buffer, Android's Opus + * decoders consume the first two real packets as codec delay / seek pre-roll + * and then silently discard every decoded sample — the player never leaves + * STATE_BUFFERING, which this test observes as a playback timeout. + * + * The extractors factory contains only FfmpegExtractor, so a fixture that + * falls back to media3's extractors fails instead of passing vacuously. + */ +@RunWith(AndroidJUnit4::class) +class FfmpegExtractorAudioCsdPlaybackTest { + + @Test + fun opusPlaysToEnd() { + playToEnd("ffmpeg/stereo_opus.mka") + } + + @Test + fun vorbisPlaysToEnd() { + playToEnd("ffmpeg/stereo_vorbis.mka") + } + + @Test + fun flacPlaysToEnd() { + playToEnd("ffmpeg/stereo.flac") + } + + private fun playToEnd(fixture: String) { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val context = instrumentation.targetContext + val fixtureFile = copyFixture(instrumentation.context, context, fixture) + val playbackThread = HandlerThread("ffmpeg-csd-test").apply { start() } + val handler = Handler(playbackThread.looper) + val completed = CountDownLatch(1) + val playerReference = AtomicReference() + val errorReference = AtomicReference() + + handler.post { + try { + val ffmpegOnlyExtractors = ExtractorsFactory { + val extractor = FfmpegExtractor.create( + { FfmpegDemuxerPolicy.Preference.FFMPEG }, + DvConversionMode.DISABLED, + DefaultSubtitleParserFactory(), + AssHandler() + ) + assertNotNull("FFmpeg demuxer JNI library is unavailable", extractor) + arrayOf(extractor!!) + } + val player = ExoPlayer.Builder(context, PlezyRenderersFactory(context)) + .setLooper(playbackThread.looper) + .build() + playerReference.set(player) + player.addListener(object : Player.Listener { + override fun onPlayerError(error: PlaybackException) { + errorReference.set(error) + completed.countDown() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_ENDED) { + completed.countDown() + } + } + }) + val source = ProgressiveMediaSource.Factory( + DefaultDataSource.Factory(context), + ffmpegOnlyExtractors + ).createMediaSource(MediaItem.fromUri(Uri.fromFile(fixtureFile))) + player.setMediaSource(source) + player.prepare() + player.play() + } catch (error: Throwable) { + errorReference.set(error) + completed.countDown() + } + } + + val finished = completed.await(20, TimeUnit.SECONDS) + val released = CountDownLatch(1) + handler.post { + playerReference.get()?.release() + playbackThread.quitSafely() + released.countDown() + } + val teardownFinished = released.await(5, TimeUnit.SECONDS) + playbackThread.join(5_000) + val fixtureDeleted = fixtureFile.delete() + + assertTrue("Player teardown timed out for $fixture", teardownFinished) + assertTrue("Playback timed out for $fixture", finished) + assertNull("Playback failed for $fixture", errorReference.get()) + assertTrue("Fixture cleanup failed for $fixture", fixtureDeleted) + } + + private fun copyFixture(instrumentationContext: Context, targetContext: Context, fixture: String): File { + val output = File.createTempFile("ffmpeg-csd-fixture-", null, targetContext.cacheDir) + instrumentationContext.assets.open(fixture).use { input -> + output.outputStream().use(input::copyTo) + } + return output + } +} diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/FfmpegAudioCsd.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/FfmpegAudioCsd.kt new file mode 100644 index 000000000..3d8084278 --- /dev/null +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/FfmpegAudioCsd.kt @@ -0,0 +1,134 @@ +package com.edde746.plezy.exoplayer + +import androidx.annotation.OptIn +import androidx.media3.common.MimeTypes +import androidx.media3.common.ParserException +import androidx.media3.common.util.UnstableApi +import androidx.media3.container.OpusUtil + +/** + * Shapes ffmpeg codec extradata into the per-codec initializationData layout + * media3's audio decode paths consume. MediaCodecAudioRenderer maps the list + * entries verbatim to MediaCodec csd-0/1/2 and the bundled FfmpegAudioDecoder + * rebuilds avcodec extradata from the same entries, so both expect the shapes + * media3's own extractors emit — not raw ffmpeg extradata: + * + * - Opus: [OpusHead, codec delay ns, seek pre-roll ns]. Android's Opus + * decoders consume their first three input buffers as exactly this + * configuration; given only OpusHead they read the first two real packets + * as delay/pre-roll and then silently discard every decoded sample, which + * stalled the audio clock at 0 until the MPV fallback fired (#2088). + * - Vorbis: [identification header, setup header], split out of the + * Xiph-laced blob the way MatroskaExtractor's parseVorbisCodecPrivate does. + * - FLAC: the "fLaC"-marked STREAMINFO stream MediaCodec documents; ffmpeg + * surfaces a bare or block-headed STREAMINFO depending on the container. + * + * Everything else (AAC ASC, ALAC cookie, passthrough codecs) already matches + * media3's single-entry shape and passes through. MP3's WAVEFORMATEX blob is + * dropped on the JNI side before it reaches this shaping. + */ +@OptIn(UnstableApi::class) +internal object FfmpegAudioCsd { + + // RFC 7845 section 5.1: the identification header is at least 19 bytes; + // OpusUtil reads the pre-skip field at offsets 10-11. + private const val OPUS_HEADER_MIN_BYTES = 19 + + private val FLAC_STREAM_MARKER = byteArrayOf(0x66, 0x4C, 0x61, 0x43) // "fLaC" + private const val FLAC_STREAM_INFO_SIZE = 34 + private const val FLAC_BLOCK_HEADER_SIZE = 4 + private const val FLAC_LAST_BLOCK_FLAG = 0x80 + + fun initializationData(mime: String, extradata: ByteArray): List = when (mime) { + MimeTypes.AUDIO_OPUS -> opus(extradata) + MimeTypes.AUDIO_VORBIS -> vorbis(extradata) + MimeTypes.AUDIO_FLAC -> flac(extradata) + else -> listOf(extradata) + } + + private fun opus(extradata: ByteArray): List { + if (extradata.size < OPUS_HEADER_MIN_BYTES) { + throw malformed("Opus extradata is not an identification header: ${extradata.size} bytes") + } + // [OpusHead, pre-skip ns, 80 ms default seek pre-roll ns] — the same + // shape media3's Matroska/Ogg/MP4 extractors publish. + return OpusUtil.buildInitializationData(extradata) + } + + /** + * Splits the Xiph-laced header blob (identification, comment, setup) into + * media3's [identification header, setup header] pair, mirroring media3 + * 1.11.0 MatroskaExtractor.parseVorbisCodecPrivate. + */ + private fun vorbis(extradata: ByteArray): List { + try { + if (extradata[0].toInt() != 0x02) throw malformed("Vorbis extradata is not Xiph-laced headers") + var offset = 1 + var idHeaderLength = 0 + while (extradata[offset].toInt() and 0xFF == 0xFF) { + idHeaderLength += 0xFF + offset++ + } + idHeaderLength += extradata[offset++].toInt() and 0xFF + var commentHeaderLength = 0 + while (extradata[offset].toInt() and 0xFF == 0xFF) { + commentHeaderLength += 0xFF + offset++ + } + commentHeaderLength += extradata[offset++].toInt() and 0xFF + + if (extradata[offset].toInt() != 0x01) throw malformed("Vorbis identification header not found") + val idHeader = extradata.copyOfRange(offset, offset + idHeaderLength) + offset += idHeaderLength + if (extradata[offset].toInt() != 0x03) throw malformed("Vorbis comment header not found") + offset += commentHeaderLength + if (extradata[offset].toInt() != 0x05) throw malformed("Vorbis setup header not found") + return listOf(idHeader, extradata.copyOfRange(offset, extradata.size)) + } catch (e: IndexOutOfBoundsException) { + throw malformed("Vorbis extradata truncated") + } + } + + /** + * Normalizes to the "fLaC"-marked metadata stream. MediaCodec's decoder + * feeds csd-0 straight into libFLAC, which requires the stream marker; + * ffmpeg's canonical FLAC extradata is the bare 34-byte STREAMINFO, and + * some demuxers keep the 4-byte metadata block header in front of it. + */ + private fun flac(extradata: ByteArray): List { + if (startsWithFlacMarker(extradata)) return listOf(extradata) + if (extradata.size == FLAC_STREAM_INFO_SIZE) { + val blockHeader = byteArrayOf( + (FLAC_LAST_BLOCK_FLAG or 0).toByte(), // last-metadata-block, type 0 = STREAMINFO + 0x00, + 0x00, + FLAC_STREAM_INFO_SIZE.toByte() + ) + return listOf(FLAC_STREAM_MARKER + blockHeader + extradata) + } + val declaredLength = if (extradata.size >= FLAC_BLOCK_HEADER_SIZE) { + ((extradata[1].toInt() and 0xFF) shl 16) or + ((extradata[2].toInt() and 0xFF) shl 8) or + (extradata[3].toInt() and 0xFF) + } else { + -1 + } + if (extradata[0].toInt() and 0x7F == 0 && + declaredLength == FLAC_STREAM_INFO_SIZE && + extradata.size >= FLAC_BLOCK_HEADER_SIZE + FLAC_STREAM_INFO_SIZE + ) { + return listOf(FLAC_STREAM_MARKER + extradata) + } + throw malformed("FLAC extradata is not a STREAMINFO block: ${extradata.size} bytes") + } + + private fun startsWithFlacMarker(extradata: ByteArray): Boolean { + if (extradata.size < FLAC_STREAM_MARKER.size) return false + for (i in FLAC_STREAM_MARKER.indices) { + if (extradata[i] != FLAC_STREAM_MARKER[i]) return false + } + return true + } + + private fun malformed(message: String): ParserException = ParserException.createForMalformedContainer(message, null) +} diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/FfmpegExtractor.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/FfmpegExtractor.kt index bf97518c2..a8d201036 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/FfmpegExtractor.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/FfmpegExtractor.kt @@ -336,7 +336,13 @@ internal class FfmpegExtractor private constructor( val extradata = FfmpegDemuxerJni.nativeStreamExtradata(index) if (extradata != null) { - builder.setInitializationData(listOf(extradata)) + // Audio csd is codec-specific in media3 (see FfmpegAudioCsd); raw + // extradata is only correct for video (already Annex-B via the JNI + // bitstream filter) and text tracks. + builder.setInitializationData( + if (trackType == C.TRACK_TYPE_AUDIO) FfmpegAudioCsd.initializationData(mime, extradata) + else listOf(extradata) + ) } when (trackType) { diff --git a/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/FfmpegAudioCsdTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/FfmpegAudioCsdTest.kt new file mode 100644 index 000000000..969ae520f --- /dev/null +++ b/android/app/src/test/kotlin/com/edde746/plezy/exoplayer/FfmpegAudioCsdTest.kt @@ -0,0 +1,138 @@ +package com.edde746.plezy.exoplayer + +import androidx.media3.common.MimeTypes +import androidx.media3.common.ParserException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Assert.assertThrows +import org.junit.Test + +/** + * Spec for the per-codec initializationData shapes the ffmpeg demuxer must + * publish so MediaCodec csd-0/1/2 and FfmpegAudioDecoder's extradata + * reconstruction both see what media3's own extractors emit. Regression + * coverage for #2088: Opus with a bare OpusHead csd made Android's decoders + * eat the first two real packets as configuration and discard all output. + */ +class FfmpegAudioCsdTest { + + private fun opusHead(preSkip: Int): ByteArray { + val header = ByteArray(19) + "OpusHead".toByteArray(Charsets.US_ASCII).copyInto(header) + header[8] = 1 // version + header[9] = 2 // channel count + header[10] = (preSkip and 0xFF).toByte() + header[11] = (preSkip ushr 8).toByte() + return header + } + + private fun nativeOrderLong(bytes: ByteArray): Long = ByteBuffer.wrap(bytes).order(ByteOrder.nativeOrder()).long + + @Test + fun opusBuildsThreeCsdBuffers() { + val header = opusHead(preSkip = 312) + val csd = FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_OPUS, header) + assertEquals(3, csd.size) + assertArrayEquals(header, csd[0]) + // Pre-skip converted to nanoseconds at the Opus 48 kHz clock. + assertEquals(312L * 1_000_000_000L / 48_000L, nativeOrderLong(csd[1])) + // media3's default seek pre-roll: 3840 samples = 80 ms. + assertEquals(80_000_000L, nativeOrderLong(csd[2])) + } + + @Test + fun opusRejectsTruncatedHeader() { + assertThrows(ParserException::class.java) { + FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_OPUS, ByteArray(11)) + } + } + + private fun xiphLace(length: Int): ByteArray { + val bytes = ArrayList() + var remaining = length + while (remaining >= 0xFF) { + bytes.add(0xFF.toByte()) + remaining -= 0xFF + } + bytes.add(remaining.toByte()) + return bytes.toByteArray() + } + + private fun vorbisHeader(type: Int, payloadLength: Int): ByteArray = + byteArrayOf(type.toByte()) + "vorbis".toByteArray(Charsets.US_ASCII) + ByteArray(payloadLength) + + @Test + fun vorbisSplitsLacedHeadersIntoIdentificationAndSetup() { + val id = vorbisHeader(0x01, payloadLength = 23) // 30 bytes, the real identification header size + val comment = vorbisHeader(0x03, payloadLength = 300) // > 255 exercises the 0xFF lacing + val setup = vorbisHeader(0x05, payloadLength = 40) + val extradata = byteArrayOf(0x02) + xiphLace(id.size) + xiphLace(comment.size) + id + comment + setup + + val csd = FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_VORBIS, extradata) + + assertEquals(2, csd.size) + assertArrayEquals(id, csd[0]) + assertArrayEquals(setup, csd[1]) + } + + @Test + fun vorbisRejectsNonLacedExtradata() { + assertThrows(ParserException::class.java) { + FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_VORBIS, byteArrayOf(0x01, 0x02, 0x03)) + } + } + + @Test + fun vorbisRejectsTruncatedExtradata() { + val id = vorbisHeader(0x01, payloadLength = 23) + val truncated = byteArrayOf(0x02) + xiphLace(id.size) + xiphLace(64) + id // headers end early + assertThrows(ParserException::class.java) { + FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_VORBIS, truncated) + } + } + + @Test + fun flacWrapsBareStreamInfoInMarkedStream() { + val streamInfo = ByteArray(34) { it.toByte() } + val csd = FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_FLAC, streamInfo) + assertEquals(1, csd.size) + assertArrayEquals( + "fLaC".toByteArray(Charsets.US_ASCII) + byteArrayOf(0x80.toByte(), 0x00, 0x00, 34) + streamInfo, + csd[0] + ) + } + + @Test + fun flacPrependsMarkerToBlockHeadedStreamInfo() { + val block = byteArrayOf(0x00, 0x00, 0x00, 34) + ByteArray(34) { it.toByte() } + val csd = FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_FLAC, block) + assertEquals(1, csd.size) + assertArrayEquals("fLaC".toByteArray(Charsets.US_ASCII) + block, csd[0]) + } + + @Test + fun flacKeepsMarkedStreamVerbatim() { + val marked = "fLaC".toByteArray(Charsets.US_ASCII) + byteArrayOf(0x80.toByte(), 0x00, 0x00, 34) + ByteArray(34) + val csd = FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_FLAC, marked) + assertEquals(1, csd.size) + assertSame(marked, csd[0]) + } + + @Test + fun flacRejectsUnrecognizedExtradata() { + assertThrows(ParserException::class.java) { + FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_FLAC, ByteArray(10)) + } + } + + @Test + fun otherCodecsPassExtradataThroughAsSingleEntry() { + val asc = byteArrayOf(0x12, 0x10) // AAC-LC 44.1 kHz stereo AudioSpecificConfig + val csd = FfmpegAudioCsd.initializationData(MimeTypes.AUDIO_AAC, asc) + assertEquals(1, csd.size) + assertSame(asc, csd[0]) + } +}