pax_global_header00006660000000000000000000000064143762051210014513gustar00rootroot0000000000000052 comment=61f69be453c8f531cfe0318b10083594a965b10e webrtc-3.1.56/000077500000000000000000000000001437620512100130755ustar00rootroot00000000000000webrtc-3.1.56/.codacy.yaml000066400000000000000000000000561437620512100153020ustar00rootroot00000000000000--- exclude_paths: - examples/examples.json webrtc-3.1.56/.eslintrc.json000066400000000000000000000000361437620512100156700ustar00rootroot00000000000000{ "extends": ["standard"] } webrtc-3.1.56/.github/000077500000000000000000000000001437620512100144355ustar00rootroot00000000000000webrtc-3.1.56/.github/.ci.conf000066400000000000000000000002771437620512100157630ustar00rootroot00000000000000GO_JS_WASM_EXEC=${PWD}/test-wasm/go_js_wasm_exec TEST_EXTRA_ARGS="-tags quic" GOLANGCI_LINT_EXRA_ARGS="--build-tags quic" EXCLUDED_CONTRIBUTORS=('Josh Bleecher Snyder' 'Sidney San Martín') webrtc-3.1.56/.github/.gitignore000066400000000000000000000000121437620512100164160ustar00rootroot00000000000000.goassets webrtc-3.1.56/.github/fetch-scripts.sh000077500000000000000000000014351437620512100175550ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # set -eu SCRIPT_PATH="$(realpath "$(dirname "$0")")" GOASSETS_PATH="${SCRIPT_PATH}/.goassets" GOASSETS_REF=${GOASSETS_REF:-master} if [ -d "${GOASSETS_PATH}" ]; then if ! git -C "${GOASSETS_PATH}" diff --exit-code; then echo "${GOASSETS_PATH} has uncommitted changes" >&2 exit 1 fi git -C "${GOASSETS_PATH}" fetch origin git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} else git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" fi webrtc-3.1.56/.github/install-hooks.sh000077500000000000000000000010771437620512100175700ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # SCRIPT_PATH="$(realpath "$(dirname "$0")")" . ${SCRIPT_PATH}/fetch-scripts.sh cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" cp "${GOASSETS_PATH}/hooks/pre-push.sh" "${SCRIPT_PATH}/../.git/hooks/pre-push" webrtc-3.1.56/.github/pion-gopher-webrtc.png000066400000000000000000001623651437620512100206730ustar00rootroot00000000000000PNG  IHDR>% zTXtRaw profile type exifxڭivܸcYa9|UeDJ ohwŗ]J/{qx^şw]yO=8>^apܾta}{wίkYV.aϿ?a%P39ZKs>W5T_яS#s'yab{5sRz>G^SzlE+G\S]J??|ʹ{qJ!Cv}M铷/w.ƾ;GU}˜H߽{^^O7w|Sn,~MMW!rab9Xyq`#='Yƚz$?E9$Ϲ L?}sҽj 8PBp>p T{Ƞ=an,pHғĉB30&%$ %!FS)IZYL2 ɡ 7-$Rq٨54,Y6b՚u%uX)QGM5WjõrVZm6z ĵN?{F\=8agy,6+V^ʪƎ;mx]w}tSN=3.vnY{Z|2g8ZApb9 bT| 9GeN9]@hI2F nݷ̙F/ys$"?2町GY!dHmD~O 1/w!(pmRUnX'ߙK=+Bt_QK5s3fk-|J; >^Undc%2so.GV-vN}it d]IJ㔙m.(8tΞjgZ[J6-M-9sQ[Hi~3rdj{l(\e/OLcPnˉ匐ײ+sRS8kR! hsIr-S >nszceP?c2YT΍f7z BJCjw !"lzemg3N;YU>gg+toɵ}Jh( l}۸Z Kz3=FjY εA _-4Ϻ(ys>' U̓:s\)U`7[!я8Kʼs=g|2+ЕEwܓ+E?S+4wd~ͱ{ `Q[IA<1a  'тRzY.Hθ':;<3,P{`v=#LBVmdx+q:i:M*9"^[*e8K!<ꮞ$ډa{c늡-NVVD@'nmA1F0#v!iv!O]#\'xPWb.U`@imi vsWZ~@Dk2Wq~`aK{ITť |)P`}2ieEC~&# h% C9`D/LS,]=&B~ݥ FT,ޔx=|\U0qwEάX%t84 I3[x"B:0wH |ˊJ! Ujd(1 "HgvtpLmba$)| i!竑QaMAL`JÅѐMn  ʹi6A/n@p$d0"S%.Ӝ/40,3|P 2cpV|QlռO#:l P'c%"*"_WIPI칩< Xt<\ҢB,\ȃ,A,M l94 :΅X H 2VEq]YAP{Q_RUǺ|~Gkw012BMքHMI-kh-CL(|wʹ5tY)4p 9-i!XMI Bi2#"9Q%q#U J!!5(Sv;`f:`v>4G۞ѡ=f&-` T!0ZG.7-9k* Q+b!{qX T@%"12d5S-Fh' nDߒ5Ѓd ]5rJ鮃"(K)Ԁ@0,#ehqchJI<4bj#47ivLN5+KƇ$g&õhP'JXM;-pQ (@Tp BR*+-ew)ǭr,"/ >GC, ɷK[@bBљ `#aH` ȴt,:ӄLȟjy H= PX^hH~Էz)/ݲ. |}O(9{w ]ТÉ/mDzE\I푤k!̎O,Y9vB(K3rt Aa3Y՚+E^:#UuK$ne-*`ߗl³xab!@ H8K6#!q]6-Cxx/Lo`hcmn*Qdt7~8RT, H@O@ `l&-8ULm8&p@-@5JŖ( zUG},٦ig&v QǗ-G\@GF4d_cpta!(3 {Ϥ EBI}ƺ(khH'E~Benȥig/ .ڑFOB޽vkc`olA$! Ty3)"\\ҹ&-0KO sxD#e.fEtQKzn(-cNrіb x<+)=ʴ" Acs-96р!:p"OQx!B`Jzz2"%zW8^>4G r)ی2hB4s Ҍx,cMwsZD=5=%hu\e_74ذX0GZ1,(C (m(t͝E z㮏Le~ 렎(+ZAҰ BE"j.4rg̋=@OU8pA\_B6 ˊޠ<]T`>e_D'ld7pmat<\ #wGn+./mMPN;B׫Q~=Ka@lM>=|9u.» NV Յ\(#<꺡-L9֮ըPPDA׮D6-җ> P^E=}!ˡ09#x@5uߏ-.2;@q42i[3bIeD1ȟ?;,Q۠`"  xtmZmsa:Sr2p6W+8i,/̬^k]x s |*5[''=gzDh;2.!tbԤ 7-N=ǝc>¶jOCQH0ؠ=7p84Y?EH,.~r_Ƥ?KH@p7m.Eni5bD ) OBZiA$U/XnCy#zQ 6ӖTyz,%hM HPnrr`\c(`M@1Q_m鵇̉ہh( "9xhnrz9_>x+jJ @Pdκ6VAi 5?;'+9Ж.ECV|KӣР3ܜZp@|6ԤC#f Q[*?7,EӖ]F (T=Tb"NcsI庘_XY*{z٢6Vv<˙nvy R^ @܏h?Vԃ7Xv`KӾnt?w@35 U#!܆bKGD pHYs  tIME )-鎓tEXtCommentCreated with GIMPW IDATxwuSN=7lʦW%"қ UPw^رA41T!lǜ$m$zΜsf}A7@dt  <5֖p?DDp'Z^@8Bn1x nJµJ8Ab>;oD,y‘6b A~>;?+ <$O8BB)[D B߹7T& 下v16Asf\XCAeO"p< Bϙrb 5AJ3 0Y-kD~_ܜ)\"]$ ]l' -,JK> vYa0S;75?W  Yy CK "lAAȢJGm> K?Ƀ  sW'7gk!Br0 !V@AA8H МAyPFA,sYCM$ѭHKAA 6 peF\e|5YnAc=LaYp7kAc#q).C>c5_Ƹ otAˀvc{abJB- <)]'/P>$ٖkq%7MIkRlw$_ɿa=ܑ6 xEG't t`TFJ`t &0"*LRq[W-ؕߒ×FXhoVF-`  @ADFg&'$`oRŝ%j9ͽXJ#v\@AZ82 ?S&'_SD^1zBp+~%> ,gw7!"a(e9}2. iP7:M V氍t#qwh۴DlZ#6J݈E$@0A\˧я \,ˇ H n9s.f/ׁ2>u}yOi" `..E[iAM|.J5FFOG):6~N.7uM5m+Jai:q Go M4G0=/yRRI8uC4G)qmE,YфbkCEiImkuQٌ;/M_:f`Jt&fcRuH A&n> wCwktNN '>d0sTS&)3kך`Zb/~쉄uͽphk(-ZQӎE)b 2  Q%b9؎5bSoRߞ n+CMG'wbMlM W:NO/ePkVeԴ'ϳ"A" PF/`4M}\ \ ړ}4((03,gjhIBoGP"q;zCbnيBkF%\%h9m X& K@ ]dGs/Kj:Mv ՞<-A(G\nr_QGIwyRukhW*?y^3u}:z\;q,TXӴik4JEVkMX 1lWc5$Ѯ ܞv5v/>m-yj}TGaA(Go__ysЯsr&0 SJOЇ0˶-AKσPѳuN}%EΤ=enA΂9Tș;ֱyoG'FD `#0%Sx,{:78+wwfQm?%)uz 9LcdEƔxW08|8#vlWSܻ>k1~ ,M V Jܕ ;ngI(9^%2CZp#η(p/Zν@[KUH ^2q<êkRhABӒ`{ctS]̌YVO;I!8nRtvu\-}Duoqt aɛ;{8 `ܚXIAueΓk~ %7Q% sBy>Gy%v&`$n9 J6#׉%7r`={Rp7cb{d0 sd{+j[E--_&`A(Gp]kl׼= kwm`7\KNWb0kY])\21% F"3yA08PJ%+ƐSZx>֝e1)Bej8~La-=ȉPԷ z’FD y|/,l9N_y}*.4 ܪ#%9OФo|+إy.QNsf[CLfZ#vW7U܍kNw#ratb:mrAb\Th[Ҩrmom3ir3Ђ P$EIĵ6%1I/#Q a;0.bֶޫmF\~8y涥 |c̨ L^Sʰh±^y\[q#kJ,򼺺xnu<ꆮiQյ%h0v5dz,O0=}xulޓ|nvAD GRRfJ`Lh`drym\^j8I)d&][yv{ȘVftøRmQIEkʝp7΄_e}<=fpZ#vm;F7nڷFS> F9ŵh-]9YGK_=N)Z' -W61QڮnTWqݡ݉ p1f.ڦ'%гAu񚺉k'sM曥W/ٶpR5-UkDQ_ep=@$lmK ٛm[QB$`vL\7nĝ3BƢ ƚg?e',p\^%{q'I|9Q3/&=l:'/ ..ru"E+wVs9+>R/І6~L[NmYWW t _z| Ǐu ]͉FA@(1DsG M1ULⶶt,V:)Ƒ>Q9*;-b1>èbvmb};e%')D"aSІd!5 . 7 \ɢxE=]H{4mu- 64?5': 406>Y;# J \ެ;tW |v4}~Sce>[=J-ԍp$Vݥ',;sjc-%<»o Es)'qDb_xlq$e֓c:sF hQGmF8?VN!фl祶|6G/9JYVιD?`l9z0, ;zRz'Zz\ \8l2ajbJ-k]qOu /Js { ;/޺lky&*2D.=ܙEer9-Y'iVFNܼh!Kԑ~S[FETXͭڬQKLc+~ᰱ5lj ]˯ipo4'lC; :HZ&P 8'Z,*)DxٜSW(6l (X- gYώz{ R(l5~bm93 'o xGhTMs5 cBpeSCP+z^OOpQwtN~|yw]9RA#GODW-vt ͣirͭJfݞn͌\if +ҷ#tOVԯR2[d.qE S*|Eck!xGE0 J+<=PTcr\75Β)$p pa5Ksp{h=8<_QdRplm4kײ8`$,ˎXsߥ;~֞Wm䌴X_e{c)=oȑº{#ڮnw <Ɵ-lՙMp#AL¹s #,0Kz&Ijm9 4 cJ`Lu /-. qӫywn‘8 {YnkΕ 3,Z8վy#qǙ5:j.O4кj>NI]_D/as[kNyUW/~df9:P?ثU-HVmMhR-SFޯ00OӴ['b>\0(?4@`Θ<=ӵMcd;GU2.OPpuէU|׵, CtAnڔuk&Nŝʭǭԇ'M-.>(khypىLα,|Z3'l|zNχ[Vc&U9&AӴ󝼨()g״kC+aa4 ߎՁQoh^C gRrci Ϯi9`|,pfnUɞn7m-#WDs Jֲ5-ao` <Ѷ?U]UB0OKt K묓_MZ^ͱ <;1lc-g)-o%ڹjf&)iut]{uqe>n:mkDxo_JPNa *N~ g9!S8sj{Z,>P.L#5|Su5kVfFK @A듣t嶖l Ey5J"dʻ\\&&A?uއ +oz ۛNRǍjVes wM[a q.p4QJ QiNyv8sO e#(>۸wd )-";[ O<u[x'jcǜ_* ,?3A9c嵾ۿꖶHWc6 'úGTq]rx [)+ =  =Ex>3 |ך(a͓N_hҮlωkux oTS Y\^W~TqۺbH4x=9ia39֯uGO!h;#JCȲ~bٔnt SS{yX!W "=_~|˶i3&;Gm&6\wMuz=Nʭd=n+,͇v0}ٗ0"hVJ%aH7v-ǵ_"% ꀏVA1`7px]mʦ։h[>P3x+Y(hHXZ$8 g7#;=+kw&bG&y^.\+kh G#຅/fQ|%)dNC b~Xr֡?H"M |pLwWA홍B?8A:Y빷l|V8fD-'-B54Jiua l6'nLFz%yÝ-UySw%7!}8'猴'U%0\DVokZ3}Jw:MdOXK'ǂ O=j'htmҥ)"I1lMܖorɒ_kB|W[q#5مR׹d Åhq|vae>kO&)3뀑қ XxdG׊;N`np6U,qe7o* ڿ\\m$orSmzFEqՁ|pck}LZJǵe^#m@ $k |8`aVh䅹>]>yޟl*g%Ə-We;!uQђ梷 IDATm-UiQ~ s!фÎ8Kwt-;ELtAAQUazeqe^T4̪v.sw Ɲ ;`,p0(:nʓБq"WsRے8Ñ,:ui%s-mWJLmW ,]r ţ=u8rWogVuES uĉm y鉝/ y8-W-Z0t0I]YQhzWP8֩f/qo<>\Ɣz|9iYfծ0Ĭ5M Aʏ|iIfO$W|n59h$D( &&Ӓ7wo4fQQV'M#S\}y[]WՌSLaBXƎA͸غƖDVeŊ477`Yp]-1Ѹ V6}o B&R P]d'=k[>tݿ1mh7 j8?ΫJ.zqƐ/֡5YaGU'W}<#_4 96ƌ'>q'3>S'2wLaQJ?xi.kEEH.@A 5:ZUQď}E_|?Uzr Quw訩knrQJYmך0Fm qHG.kAI`|$ׅ1~M9}B>8HY@۶plH4K/׿u6lؐиhn1M v<H.Y0t a0|N"T&Kcyo8cO/ͼ +2*ytBx(JF7Z8[xMo׶\ޤm!db_kB_#luʃmv, o494McϬ9VZu 1uL²ihhH 6T{0TE6ve[ C. ve6}&?Ε %EQH_;;79RYrV+wi4ihuU'Ԁc$:&{~dNZ#6ըn^2gt瘊|nYqY{ZşO~#'irgӟk֤-O؊gVP78i+LihjAmyZքD C\`G-ԩZ|8:S)U)R)\)R&h `iX'>W׷'lYvISXXȃ tG^VU#>nLN̪V 7tSsN˥JD @ĭ8Ƴ3~glµx@Z+{[a;j v}P;\)ԉ^f.oz0FϿךk&|=W\}ed Xw##YwX=l?$@\A ' eA²;ҭ:yf6tMri@:c=:SR{0 k|zSHa}Qz#}B8+Q?臜2Vֲzv4T^[48d @Ab] ZEyA߁hT3rip q1/k2ՃVVJ`~"pR dh:#p:&{5+iߢdDՠSͯif-k9vI !k:b&'AD B鲪(+2Ǐ-oz6GWPYa<__rZX Y>\z}w~xV vokBecgWd7|Jνz'=G1kw6y ynmkiFeKD B?uQ*Q9''WZRKk;01W Q-!cɶ/dhB><Ͽݜ6[Ȍ)//{?q˹Muz^jƎɱo7Jy[%"?di4eږi[=3?%GTՁ~G2vw[9B%7%pGV5W:j!d"G ^MaiŐُO(.s'lӫ\N.@A1fF\8eYfLj*rX .]llxc{ GJԲx bghƱ| _rrEWqçoxms@e%"=KD=ڒ}[Jm(jz 'c}L٣6jW$y*R +G: Z'wGed f L8>]njH+`OB 0%"f&a'lW6$=~[#fyeS{0oeR< iRi;bg_0p؛W2Cf%$a Й> 0A(}%r dZ̶1\u'o4nR5b}dbs]!pGcx *,,s7sp[/9E[Yk&"/,Kc{-ٟvGy](PcT_ [9mys~૸&V(QGF)h{]o^RTFImMIp1gtdwn)RXFlP`i2vڼ ۷ok駟 ˲H$ݻ'̞=ez>|dY·97FAԴYYqKD BxXIt Y[(Z[ƺٵEY\Y/mbYڹ"9ǯ'G4E>Ӓ!00o#<ڵk;]mF(م]׉Y=b}ھ+KmR=zV%@A3>mܻ/VI8voXZ$l' E~\M+' "I7p"UXoKԠ6qģMPx,YO< ZڷM:*[ȅb{=ip P{Z}̬I~4sde'Y Pq1!ʖ\.؏^ 4n_-A0Wp,ztXo۶ Cy'{9g-ʊn{,|ִL]/ d=C}P.U Pe!ݱ'_Vۚ@47"1%+X~Go|rDf9Me Ao6oFֽ슏?t,ӎi/4w>P(+A(E uڴXl/[7©]~iZ"q'Ts?%=\o_r`!M*aKw ASU3%4Ǐϲ &M7[T0iB~}d(FrDžr0llAyi71 }i*;0]!d:_:RڻYnLiɢ?rԷ;;cPYu=KsʦL+ 9Ia @a(>ZXQ^P(X(etkq FetYzo\?\R<0U_?0,P`Rً %"Lh"_)vڸz෯g<M%\A#}L=I&oF֮ȓ=n^W>ѽٓ ;M(hh9N˖ 2.˾:czNp$eO\'BGgF+C)VX;RV>ط,[mںXcX]9s/T9|:i֭e""tY>#gŀ(~_ƪrC/kGu["AӴrSQQ0h3:/+8=1PskkV@C˝ q曽Zȓi %"_Vkh1d%I`َ5z亜Vs{  Cp\5kxX_x18o<Ψ s'3%"?nSO.i-2ޑxOꛖMmq}lkH1x \GDߣ3:վt:ڛqS lTߥ-;sȓ#D ~ {j[̫?`7h{*+w^a}άi)oxmG;ԇ,&+Gxx9ARTzYP)e7vͿ[h*=HYwFzJ ]W4Ϥ1#?%_ Q_ΪpWַ8 a_0>Z4(0Y_+P+b*a@= 𪫊` /o>a4-O׎@ӓrg\GRbv?||}m2C&y⎸3O˛zvo4c}[8ar9eb&w!}|ZyOOe?r{ۻ7fǦAD @ںZalusLhl \^ۖͭ$I\C}UJ5(vm[ 4Nf+vӊTmMRv72e KJ*(d`[C\ K?-A3dqoxt\w}v7q[I8])adanq~x x+lkuFWV `wr#ɿVz!b s-7,6oجjZ}}bs][2g8#&iRs''ݿHE [7fmupbO855:nyyR q'xtP|Wm;Pj_0.՚3?O.`ծ0mZW!slbԔ~aє𙺭imQV]]eGSZ2 ?g S#FມO?0ߴ XU8@DXz>\< |.;( قG~zm{*?T $JpW?1Gr @k(WQ`*]5%ochh# l <oZEY!55Jp3w(Y IDAT:MV9({4Xp礵-ѡuu=zwxd ³:9:DӴI+EJTx"y}OJ˵ViǕR MZLo<ޥ锉A U|+g϶ћmUXfeSG_ΐ5nE=?_Au\/qe:2^q"a;8o5y^71KM8s2vXvŋihh8555\~唕9PHח/_eiI1&pdTg QTB״P^׌)i7 +uB1ۓqӁi^CϧMZyfTiPtmum->zJ{kk'֮=x͊$ZѹXdzI>66hz+|E0(ȺןK?5go,7G/ibo[cLWJ9]5C2b9=雁SC47xʑ#G.֍ 8qZhҒhllԖ/_ywxvB){7t-kjiIstT#nEO홆q_Iaz͊W_}vwr-O4eZ?sQ꯫[mM>Jo #]SQsCr=D:Zѿi ,١Y= 04ͪ.*j ::u??sH̲߮\5g[7'pZbOi~lYqYvG:]4M;;osyڶm]n˳5sqLowK Ai4{_/h^Ht,u{|_^9}444h󟵍7b] sO4MfΜivi#G& l[7Mg9MѨ[-6 0I$'ɓٔ-dl M !,مj,j{,YV׌[nj)w,ɒ8/Н;3w-s1 g6qECX?)Xn[W"Iyܨ,ҢE|IgI^HIجfi]ZP|=5FԿhzohS4rn^=q""䕪~Iƍq5]9OSc{>'|˫'E ?`^f;7oauO!j׾5LJ蒦ihiio}{XKP(p]|+2Mo`i>w}חrv)c9I ynwdGO r<[PiH_aUpO1 #]SlHQG/{?+TO aUSXPgm#ǐK @}^}Al9W9qgh{zKV)9nr& o3WP۷oo>7|Sڵka'd1Zy EK{޽{qIDID w{HDRYcO2[idaRyNr\u|oܸqv'*"-Z&[<ٜymJ~ EQuV}ӦM2!DVW;z{sH! ,aC+?s o=v۔):j1EH֗@,}/Q lb<Uz`V̱EM?}&[QBPWUw}jEL׍"K E#;Y 5.8ē*}ADb T9! .(^@8:,.*ý[pyueDAsC ay@OH+izzzڵkuVա mX8./<,}IqQ"q,@>~Ͱ+ԱފZk iuVb"*|8grdZiӦH G)/^/~ ;w;C`cg=ҁC}[E?&鎶RCSIp/[ɋM(!E1?n_BuQ>b&z'bL#^k(|$fXv*BEF0,j 2޻܍ W#;}C_ƂoJJQgH^|W4؇d+ضm9kƘחxc#m+fI;g/B,YOӆ(SDC *L3珆qkj_XX RPEUv^䕅|wq j&R`WjiX4v;Q=*,/c@F` )%+11`gKD=Nbq:+pND@&IPT9El[?_=k,S&6:|Q}T5GAi[֬Yy^l^s5!<{3ȳbn޼eeegȚ T:޽@lhzM3*-J? [GVfol:DJ0#kژ@rwR u]]t~zJc4~H׀ V{tQ cҌ'"9bz˺eNduVxqФpK}ڷHXאMh}*qtUDƘrV(wX`E Zo@94'-*AJJشiR}֭%DQ̏A!YqQN`ժUrV 8vX`局\r83-Ui?~7_6 b)=J?ej-"5+bT*zfNR#H cH}L%fo\Hюj 2\Yc] SW+س 3)jK[7Dg[چ畳tt9MW~']-4\)eMJ*%Ec'd(}&]TD k|l}y"Ѫ>/2r"^$S| E픐2#C/͋q]pɔ_ϝh &U-D:o@7E uee#s-g/bD:^pA-[>쳒йMeuuuI2JҥKs=\r8&fE[?aaz.H>׽A~ <Ҩ&1&nci%~[wH?=o20 65&cЙߙ Ct8ї$c5ss(l-!/VttƧ\h]]-{sA˗$Mce夿?{Ù锴Җ%/=⒥DT,A҅U׶_kO єq,uI(N$XBU›]O"O*s*躁2]A"-{/&R*"ȫp _`9KhasstR T0 -yeEre٢7ͦ+R?m'NQp!7WUUȳ m$IbK./t5L$ 5[WCcTױN UNV._47So -:F*BJsE\Te՗I"8(%g iQC!Iqz|!ҥtp`j.VܼI@DCN7ްȦ 2[--WxϦfi-4-9+++vhBVW5ά%>R4KA_X`,L "H_jW%.i\1) ɾqO}a \Ƅ.Bqu  ;#zCZ瑒6YCtSBH8>yQEf @vۅ*!Hmhr]Si4AE$.;Q$nwH-% d rjT7JBȴp_^꼈D]7^75_-Q.%G3TӕsYBUJk\[pħUUUYFZicxȃpfSc,Qf+=#,V+dtUVyFz=16,̲ pf:9rDp"JcB~ \fYEd-I>cl&&;AyjX4W?Y~%]v4!-9a-Eayrfr꯹,23gδa=:j(*)[uOrf9:e)oC{5־oE퟼,~z(PȰۆ?͗B$i|/ʳ`>BBn2[*?OR9nD\U4@G6S5MáC(=J Up$1[(_\r83!qF ?+ůdIgnD>~iJiAlr xT4tL<{26:mdJ !@L|Hҟ6 +-Hs%%C ZAxH&V677'L ~c[[ew0ZZZ<cRM.91? &sԥkm=/H E4hO_.C(PVg~Q !P0Ye Dbu9 JR/8,`q\q7躎;v^ l)e߾}{ +/VWUU;D'? < @g6 r2^G 5J pM>;[h( ܂^$Hk'+&PJ )@[bHA tY$ɔ&T,jiX`a`G3:q%Ky!4b)#sw[ 4 >1v| =Xԣ>jfi>9v ʾY%cE׿u(Sx, iwu8>w5|Z(RЁ &7xR\;܏ i]b @kM/WP_&c\649Ɂ uK%e6QTDJVAq[EoE͙3j\<6atC!yxw[phY)zg? ?"ۅe-;OYcwq S@KbEpӸp=Oe[' LcG"^tr|W4G9j$hRO:VuY_d8'C)~Wbk(/0ޙI~ }+Ƙ+vh7;ǿb%TgAڲV _N$'Hg?@0|)ːngת35H ]b8olϙm H7?>{OiWd\o|C<6OuK{]u3=m??&1쵋řr? UUɡC{ܹ^>RoV^z%H+LFdqx1GpUɅ[944=KIWH:^{D5 {/_Ytţ=IS˙h \^6*~M)o"l3{q޽#G@UL/)ϟ:u_N f@q7ݞxv/HTU=R7W_}wyA+N 4҅ԧ*1A 8 3uT:'n`u5",R3+ uP1XC)0'w78Y>?0;w,b|˖-NAƜ H$NȲ׶on޽rMQ50Fw>d_'N+0Ν Q !gc)c"H^YY)l߾tvvD阿/`@Җ7;kF ]_FTVVR BDQחMc>.9RzP$@\GSb_@ y9cIX̄YVNDq+cX/WR9+\pFyn7 HAF!`ݳg񠢢BpVHR1t]G* Ba!t:=6mʕ+* Q@:.mЙATUŁpQ`444 ~CƍKl6ɤ\QQNqݞd2 ݎh4:1' J)PB ӸQ`tM^~foiiɿ@ @g, Y#)GhPtY*:4jE0R-iRdO#PUUq9rjIȨ9 gp{/śFJoֿ#z&6"y 抵/_Yzm~pB-׽gMbϫZPӠP-34#ocK^Hhl,p!]dT^2F:\ ";^DV X9 %`?Adz˅Q? Sҁ [W[յFpڹ>hPxCךƟ_~v[>q8#o@6V!L,w|i 'F ,~tfo[w@OFiE:.]"k7XwWQJi1˦ё<dR.`άơtPPLL"AlBmb)>{(BeYFCw*BEpf*)H6`c@d<`a_G,'nY7#!T.Yt\m۶U644x=J:$?~de m>W^=9-*.DD(Ox"NɄ j``$rP唬߽n>չ6۽uh' C(->zwܽ}IhPɯ_!nh'?~Q?#8ѳfW.u(H5!1aMeXPUx[od2B M***(АjlljBu('6gJʈ]]]˗G~{[[N5r#^ @ A:'MN)PIhw¿VcrxhZصOO6ߗ3 t C:Ùq t} E eKo ݀7̄E6K/,_\<Ǐ 5#D#fU$t:"1a0 v3Q'I2`X Iz{{!IzjnbX 2$I2ѨJ)e֬&Y|9P$xPM& Tkǎ?ckeX9"%; @Trjp?i7/9&PD:6i"/iڵlٲeqJl"Q+EQ(z@Q&QIQG .`Q&^3ߠlUUʽ .:IT5={o_n0`aC%>񑋢.GI $~:83:b.Y%)hXe-[F\Fڕ͛5Y.X3[Y,#t,А 6%͗ґ㓻1o5b4TUp?d\}J,M~+?H0aZe63-b& jp0g !BN8cEU *كlÂ}裂$ HgVBU8 pz 57!@G'࣓!c:? D PZrBEHN(%I2Yp,Lș$d<( xmw[HԲƂkͩ.[F&(B+zMxي9V^€3H+"Va">ڬ=͑eL'B ͛7+W(#.u]Outt["z2>l1,01ΖzJ)Ҭt{5 1_˜ ;Goxx~_Yg"3+QʧDKKKN (kVtb;vP PeT `{{{wKKK?\r8O oDZ0'0!Liwzv ;W1qPD-a0v;A5EJ 2JU>h#V\/|nzߡP-{qX#0>σo +P`QEsZ޷%zN|K `ɲS)iA@)C{5m Eϰ q/YRl_8v:uY,BιommEKKM:TU=]% #. Dˁ̒?E'Ù0Z'8ކkVA* ak ~l7ќy -7\x`?S9YBH/EX$j! v<@?~ho_`]mw?Mf0? aۧb9E E:1/K0Bx衇|PhnnpL.744~J]*$u~@I|8-f6PBlZmiʲ3!z7"PF|Uc(s#gW /9UЇR!І @ƆvJ{nw^UW]ꪫJ)&Br躎xx02"(6&D\^F_?矈_f>2]ڢONsq,24ʥ\FOH=tvgp)?@f1|Qݍwy['UU#~Zy #m1iZI0 Cu}hV[^e@l}ħS_s84-!~Y%qCه}s_p߃Y#\)r=A5Ͻv[-rwD@LrY$:Ƶުo_3'bX n9-퓄f<0_' E>\r8&5t'P"U5qT0A<{B43Lo^kl/!@]uq$ DTxx߮,5ֹkJu}yH @NaNb1^f{0BI<Ι`cR4BPʥk%xδ&Q HvAل}oEt렔M뼁EU GzzzJb,NF;wY8@)#~7H[G{B:>\r8g0gBjHagKDDE޳mH#gOr,:u "-->\s0,4xM-)γE?o": e/&%%HgrnJH f# st nXz,tx❀PlARWzVϵ&3IP`Bh\o9Rry2ٺɞJS; g@I]SvU%lB1h4,w׎cZ ^KsL܆ZM q[\ex❀M٘KIB^Zi`~VI %?H8zDkPZ\^^ 9Bҟq^|]LN=\0.fpLgr7.p *^G56-XyPUi1 u: ̙(\˜kX%jiRR:/_쳅g<6QZJOx7Iĉ>be|dp3,@DpY|ɤ+bᐔҌqeRJ+`R$~*8ӑ@\Lܸ:T8D_Ridg]@a3iE,39y02y a.9#w%0Xilp2 aWpBոP~MA-w IDAT|Qg8Ԃk&#M >x'?zk/Y Ƃ=޹eR;q8\r8s"Z"Hլ"EU 4xt\s*VVFXadR0Jl;L/0@IC2 棔@Mxo}mh#Tz+^ɴv/JiF/iDWƢ*tB7hbdMM;}:E? ѽPH3@ܮF/9ROHzuҗ IVS/|3ɉzoDnRܑ0|6#YF珆5}k8ӊ 37DfYm+=c ' 1xDFsG[w'p+cɢvCQlN*f#|Xp;9n`I0Mp$U5 o^ƀStXO"W.喳:=ZTTc=gzEN-KSLo9:[p='Ě-)xuV*\5x{?~)aLՙмTr+ASOgMf=%uV/Y<ӏҐޑ1Ax(bf>̇ @gs( F DP񟯞c\ͬ& UgdyuVUJgHOg'B腍ͥdkH["YxxqIDM5rL{fH!#N<@+=eUZv\`6Q ZdIG[[ C λVH֗~_ e,gD9YX5 $t]ojj2{)G @gs W3`\/'-]XKK/oUۼq㒐516 ~Ⲟ}js(P(٫ح/E;ᱽJo'e @pwΖH {KUUσN؜^z {I/a،DGR_t m۶ 6 ~(p84 Ph$J)$IB{{;SUŒb%y稒$\{_|jJ阋 $>l9k?"i!Nybz?``V\qRaSH$VQH).*ά@Be00Ώgi'"HߡИ*c! AQJ)\.cn"ϑTUm|'?nlٲE;w(!laQMӒX,q=R ÙI,u?w{ţp뷝i&`Pp gVSfZZ|9ge[~|^+WĂ H(B(BEE$Ii|^{KK 4ʹ)~EsH-_fdAY-v QظnAQq# -1v5:[,c-0cر@!R]9;Yw' a*Cs_PXj_~8R6T(b1޻ ,b-qL`.%c:f;H'K"]79&-  J[9T?ᱽSmG{3!D^hOKj_x(d筚7[oqow=Q>R( d,'}|aMGyTmaF|Ly-k, :#ːX.uSgRMؐ9-l pLkvg @\G,e]^ʛU) d%nrުyHדX0+>phaTʶ31uKf|W{x i+e& >t{$拼5=v}>q~~̿lQiW̘yKC.pfoo%fpHӅTjW WTpuU:RO lai,*PHMWQp3ݐJ發 d##j4ǝY4q#$#'Jtw+Spc!K؝k+ݥBgoƠF{hv(R:?39lY4v-= ѽ ?ʿÖHw3a+ v 8\q:& @g؏,F(c ).i:'L\0g̯i¯NđBV9ƀ66uj%/p(ŎBIsy;c)X?k ?>)N 0Ƹi3%-ED }ZP̈́v @dřvNj3]=pN @'7/c(PW[r&GOt /<w /~wҨcIM"ׅw:lrY f#&7x`nT;H'ryo̮y?صgeWׂ9\r8JN"ȉ$T܊"oew EJi֏ H\t%TMg4q"'^L,t gEiI|f:8OD ~Z pLoe1ǜΪ{^2E m{գعeؼPfӗ甍YKiLT#݉3ψY`8z.Y3/s8\r8^dTtT1(\l(sy0SƧHNw_x7;P\Kd[_^OgbccO'X )D8 %~M%O܆zy!\ d @λ `pbК@QJB|+>oIƟΞVmΓ3V f<32W1(%2?E){g"E4)X;Bm<<ۀt'|,2[? Ù0^z2 2kA"V:9a fQD<$K)V{8z9gΖlIvS7B"FDD++ ^W++^"`APD, B i$ҳdm6N8ɔ=gvfx#)suuay/NuHer1*(pdmnBj4+ ;|TZد0@ޣϭSH !_:2MTK.Xd4 Q#7>p Nm!/5iFq]PZ cVY>2C] 2917+:F1/1>2 ?ο$4!r/j]ޱ}1$rY;+3~B'1.x;ֿ!8`cz˶n:pBS:L!A~Ikw'=#2Z*}jj^_gfmY<-BA]^F&Nrh羢ww bLw@]Ѝ(+SVBL!8]I$ W"5~٪p=+=-T'xZThNU^b)*߼3v@ڶKT!yC rh$6F{Aud\eE5/!P{z8DBGM=]=#JtxX cvTYW\v@ Z  !e!O[_Ğ!J#z̶k_p5w@W$1(ɔ3wZ(Tk([\̨~`Et,R'/|GG1q; M$ $,lA*K[- V4Y|bo'ۖήNpyX;Gu2Մ"X&c)eOReśH.SQPdHrq* >q%+P2yOs4hBx/"7.lRs/mý-§[W7l؛l[&xZȔNBb&fy*je`KPlnJ-$,Jtv#{\3Np#F'h ZI Ô|zczc&"%3fMN~FA#[ǛuY'fpvd7Ԑ)4a߅f(i7f&6Mrvʰ 1qVAxĩXU/ڳ8 ]o\ ` S eǓjIY^yacŜ <%= OJ-[]Dvo0>p/ۮPw߁O_6p +ϑ uMxjs`ȔE]+'^)+â튱nSqb3q^@Cma\V¾|A04$dq엝2қKP ^Z^y ]8εzRQӖ<5d*iS57'jgF^ObW!.r#5m}WO04$b<>dƆ&dcZ##=Xxt;-xտ͵HXJI2jGRVdכ2dfP|$X!QVv* xw;ۚ  !dXJ` a–Kګ 3(U 2a]D>*2 Dzd;0UTnC+UHXO t۹]1 s[fIBH$y9q63|tAN[mMd?ٺ("@&q~rs<"}!d2Umɬ{P%]VJR E13G;يS>7Suap\քPR")/x{G6!xĂY<8E`B݉poSC.懿,-mJQ: cF¯v5c}q^(:Q I~5g| hBͣاфP Y 5FMCi/mi!S"p#_Urq){w'LYv8 dV>#ޮpy9SZ2|`2$PJ`ͮL]?(\je22O"w"}Iy/Kv'O *5F؆"v$v1_p2lsś|bpY^ae[p":eos©r3BHH<.'/fmk^t6V;b^s@4,׺Sϫ[TJ ivu^|ai.)k Sh 6pye?RCUnq 8΅lg%B>Z  !%@~Sx; ۖTok׈i B@}hPnMcUgbOC`]Wҷ В6Ubz˵q?pKuOSBV=WZ'No `2$dR=՝ lIg4jgXs[]h1eMtjZPSd \Z3p[ŜYm=IE9-RzǾg˳|ah৽ʍ;QI;q|~voSecG wy&LJZ&3 k#J-igص!<█۲5a딓掷zTV5>_){`8iun؛,^Q](2]Ɨ1?Qg|b,eg:4n`BHHt~Sc 4J{׉˖wD,VB(@mmxb D^4pJ{Y~(ifnܛqb,R7 [fL4joIܛpʲJ]Ycg9ѫLUo]r8qm sICÞғQ 9,q>U)%`To>`b2& $dFNzi $-h ޞa)]UA!fz]0<6?<Ò},j<5\0f_bs9UB&f7?"pTՁ5  !%܁u]I &RV 9-['7w-M߹Ңx|O4M\sf,9|aRƊQAg;N.͢dB<{nkw'M!{9.SOV]Iξ鶧Dpbؿ )u]y!#sMa)RNUь83z+GȝPAl>{Qd @b SY8'/q8}. ULܠw^(|fKL6v4Qx^M^iOa x'YzߙK]A2\@Ħؑ_{(vZo*_KjZ˖ H!}9`BHHH{OؓU9cxeX).8xP '\y ̈@T񴐩"hQx y bJM:2.۹òMP/K( 뀰-@ꍏ$m7cٶM۸#) 'dJBh'ϭI{Op1Bn[۶25̀ؾ2ll8քPR"%w`k_Y-hLw=y՜_ݸ]˷3bX<%d*9qV=A Z1fAl"]YeMJEU6'p5!Ld=)ITn Y-۶ɔ:0iHٕԥd+`2uLr0 vPyIPef|g/NEJ1ʂЄIuzeGPwVkcKi]?eSOJK2SBP@ ca xUCL]SY/vq^UW`.q3K( w% PK(ƃTX7 ˸txLbwJl!"eu.*0T(?/pb|/fV}.= nf[-pyT]  !gVxn[\H9~FPS4^/]0mG*k͙ՈO?Z"%\^yN>Y4$~LR)Vmt= ժ;f)ayU:ϟ>IѪSZkL( );;Kƒ{ vd<BGNS*T&%ZÍaJ`= \=-T%;|yP׶ onuNP q$JrW} L( )?%eH1>*4Log*rJeRZ1كL9FMCJ/Q{.+ RQ8n¹τ{`U.*ycVtpz !e|{vk#)۽їQΝ/?5!O#O )\aD닛fLP[üWP6mTn!SO8Ɂ mP[OL( )%iH<9 (߽okݩT jx&yrBd3!^1Z8T|tD+c^c ȉٗݕܮKPVi/Uųǵy5 @BXc~fڰ|i f.6zakm"26/ڡܑ#[ Bʉ|zK̺ͼKqϝ@tO1b 2S-8zN~З̓~ڸ+|T&s5lC,$slK[_gwՓ=KE:],"#`أZa@>uv˸N#_{wNdaa_Q>V XXfL+vA}We;{B_7*$m/VV&ޚ$rzpܓJH)eKlMS䨽rmG̫ `{SXFt,ਙ=Ik+{RRa2>[fJ$~XvAwuڢoP|`pTp3~3MUF0qޥp(_励n^[E§pONC<X9s(aEℑe/o{SMg>=sȔR:;\1Pug4i_fuuVp/D{v Bc;O c].]/)68毎w)3(R-{э<'V{ToE4XS-R{n[$Zlr{u}!ò'(ZM s$ [uCKh6[;Os_7<'`8i% ۖ] ~+QpR߷{RڿñnA|vK,s~s+"]iX P2EiB?.5! Z8g/:^fr= M@]$1&dO[C9YRن*-34!tq֬5pòޘ_τVp_ͱ>)帢Ö2uCg+{Nx9[ 'Q~*TSZ).F4\qz\zLmC4GZ@3E6V1˖xmޢr0kMOMOo~{ꇒVj?> 9` `5K#u7SVO%Z0NUW(iHw-[s@ϣVMxsVԩ~R84,QE.C3".sMu`-@BHHE9Pky3DuMi 4 ϐ[rÖmYoFi45{r|_W/Dc)[&.j˴`_DZ'H~F7*!<:`]rR1>X"<5n?c2#_{fk|_,SEמ*QΥ~E-J<<]o.l162۝6p"S%]J( VcfC.|Aџp+7I)cou پ[+wWz3+.^'}Nr/D5/w]>=mrI+%LSDwv,nid^M1O+߻/o?PЀSUk#A-6UM42wZ2Łćro[!CZ g Uw}֌u4Z9xtAgH$|1^&p(HiC  !?M79>Gͨ ç-"yS~cO6scc77Fq[p] x:yYͩP@c"[j1(tg,?l~zK[V(;ϝ!ͮpi:q/{،L`PhBi.x.6~ܿ¿M\;!(0x҇uꗿqqۑ]Qڛy&nC!gS]qGMaԄktΝ1ZΩaIu]*T"FD["{K檠v ?n"hL2Sދ⺂^9 C* Euۧ{V1V!, ZΙ6 FjZnמAD"\OR<i\ؖlӦY_(* Q}Q_~gYe^uw܄L`7-L3 m˰$\wXt)_9vp.+`)BH_K4kΛa}¶BIw j"֐{+9kD#᠖;:&PՋӪ5m`نJj@%-PwQC7Eo"K&r3 '!N=EXNTJfU\=pr{N16 ŵ,! d\~̈ _x| !DE'65;CÉeOlvs"VWhUv*)9ivն5Ƥ!Dঞ4Z냘QKQ.g5q^ޝE# V~w rY>*n/Q߂|K:+,`C=s/_MطRK{ ipCxxC !Wp/ \yVsuRJnj؃-ph$n|~Fya(KWuǶF^-Odl>1*RRVuB=Y&Ye?2 R90ÂQ_͍ZmY uG$O^QѪʹ)%L[뺒MdIYS_WUtZGÊ}0vgP?yF3] Yq5*KRN}pr#p Rx?.wx|1Ԯ5(o 'fށ."B&go|8|5L[&7WoKk~b#>x=ksodŚ+hxfVY&l)cA]ԗV!%M["i &ޘQޔ/ r?B3jF,lZ3)!DdMgz91.W _Ug飐PDYGrQz=BH40-"oYM6$kKr8)uCљأZ颠i{\gWl}ܰT4Eh]WA]& .5ش҂c%`Z6ag[Vꠎ%xywB6D,ea羌9U-ѯm67U8<6mK]`;,KTÉI}U]Xɵ;>SBNHHq!`a+:F#Q!wL2\! Upg>8:.|榨eK+z%cIH`EG"wX!PuTEH64}IDAT"FjSQ&5'>Ml 4l#cJK[-qm` 5 %k0my~H߅n8^Qc;<y(ipko ToʴӃVa|\)L׀SeIpJ.vmqwvx\_NB`rYɇ"݊sxw^BNj0`ϬBP)y-K٣IY3:V(3"[U 7OoᇥtM[O; Gkn)IyI ']TmT5= b)ͮjL[bk_ P_HP-A!]--MNdy v@TJ)!1Rhߓc&BZ}( A#l2<i A FNأ1jk8cbom_I>JZܹBa]Nϫ>"ļ!deh9xF^+pUYuE22~ NJ(kPH+ǰP8 [h$d| o gXdI⯫ ;F{%<୬PPYry2+BMd8ؙyT]0wVDYp21'<"0'%00<;*VLk!;{\i;q8]17p)O۸:Z3m+ٗ,yi8f@v,6-(ܿ{bE+58q&p >dX;=uJx]8u~d4N7V0֛w  !ģ|?"oBkX19vg w y r9jt&í>'>,1v/HSM4+R!9̜\'7׎rOx?]nX '0>Vv76ǂ;=ɠ(@8^u@c-aYUcfDjk#՝ ܳzIz 81}įDk2pj'8H1M:HJ'tC]PB1SwqBRJvsn$~PZbKS-Zc](p+Q=A2밳ppw_Ç5p.pdKa ;Fݬ~NPZYYV3PE$+Y6l bvĬgyV 8 Z,M!8g'H9sYٲ93'rzs8VggLL?cI1/ ]u‰g df*χ'q$[Mpnc+6pb&e8HMNFebٱ,cB}YѶ ߻ܶl @c/?.p20w*]+xLDñmʊ70HNQ\uyH$@SOڳbwG=q3`)xC]\7 uӛk-Y1poV\mVg`yd D^9w+~BK= ,9?kfXk8#1>)p;5z=[5;z-Uuzgs[pܷX (Tv1Fap̒4r, 8(oX`sW2Zv'+Xo1K[4*py }.wH`?T=HHyR'/ T'`gEXbD N׏?%jp{TԋJ8. G=9 'Ok rq0{즢]ډcr8}t|!8=IΊ)YpbJ_o?CTFXɞ>?u79L݊c"ਬh"SHݣ85^ÁodAhd'VJ±{h⍕l± YcNx}X+ w% 'vlNYԌK(nmւs9 :g#\sI'h%P΍) :{|k_.0_+c?9؛=FcX'X1zNU(_rY@N,ՇíOmp bwJH!^D*9+ Aaron Boushley Aaron France Adam Kiss Aditya Kumar Adrian Cable adwpc aggresss akil Aleksandr Razumov aler9 <46489434+aler9@users.noreply.github.com> Alex Browne Alex Harford AlexWoo(武杰) Ali Error Andrew N. Shalaev Antoine Baché Antoine Baché Artur Shellunts Assad Obaid Ato Araki Atsushi Watanabe backkem baiyufei Bao Nguyen Ben Weitzman Benny Daon bkim Bo Shi boks1971 Brendan Rius brian Bryan Phelps Cameron Elliott Cecylia Bocovich Cedric Fung cgojin Chad Retz chenkaiC4 Chinmay Kousik Chris Hiszpanski Christopher Fry Clayton McCray cnderrauber cyannuk Daniele Sluijters David Hamilton David Zhao David Zhao david.s Dean Sheather decanus <7621705+decanus@users.noreply.github.com> Denis digitalix donotanswer earle Egon Elbre Eric Daniels Eric Fontaine feixiao Forest Johnson frank funvit Gabor Pongracz Gareth Hayes Guilherme Hanjun Kim Hendrik Hofstadt Henry Hongchao Ma Hugo Arregui Hugo Arregui Ilya Mayorov imalic3 Ivan Egorov JacobZwang <59858341+JacobZwang@users.noreply.github.com> Jake B Jamie Good Jason Jeff Tchang jeremija Jerko Steiner jinleileiking John Berthels John Bradley John Selbie JooYoung Jorropo Josh Bleecher Snyder juberti Juliusz Chroboczek Justin Okamoto Justin Okamoto Kevin Staunton-Lambert Kevin Wang Konstantin Chugalinskiy Konstantin Itskov krishna chiatanya Kuzmin Vladimir lawl Len Leslie Wang Lukas Herman Luke Luke Curley Luke S Magnus Wahlstrand Markus Tzoe Marouane <6729798+nindolabs@users.noreply.github.com> Marouane Masahiro Nakamura <13937915+tsuu32@users.noreply.github.com> Mathis Engelbart Max Hawkins mchlrhw <4028654+mchlrhw@users.noreply.github.com> Michael MacDonald Michael MacDonald Michiel De Backker <38858977+backkem@users.noreply.github.com> Mike Coleman Mindgamesnl mission-liao mohammadne mr-shitij <21.shitijagrawal@gmail.com> mxmCherry Nam V. Do Nick Mykins nindolabs <6729798+nindolabs@users.noreply.github.com> Norman Rasmussen notedit o0olele obasajujoshua31 Oleg Kovalov opennota OrlandoCo Pascal Benoit pascal-ace <47424881+pascal-ace@users.noreply.github.com> Patrice Ferlet Patrick Lange Patryk Rogalski Pieere Pi q191201771 <191201771@qq.com> Quentin Renard Rafael Viscarra rahulnakre Raphael Randschau Raphael Randschau Reese <3253971+figadore@users.noreply.github.com> rob rob-deutsch Robert Eperjesi Robin Raymond Roman Romanenko Roman Romanenko ronan Ryan Shumate salmān aljammāz Sam Lancia Sean DuBois Sean DuBois Sean DuBois Sean DuBois Sean DuBois Sean Knight Sebastian Waisbrot Sidney San Martín Simon Eisenmann simonacca-fotokite <47634061+simonacca-fotokite@users.noreply.github.com> Simone Gotti Slugalisk Somers Matthews soolaugust spaceCh1mp Steffen Vogel stephanrotolante Suhas Gaddam Suzuki Takeo sylba2050 Tarrence van As tarrencev Thomas Miller Tobias Fridén Tomek treyhakanson Twometer Vicken Simonian wattanakorn495 Will Forcey Will Watson WofWca Woodrow Douglass xsbchen Yoon SeungYong Yuki Igarashi yusuke Yutaka Takeda ZHENK zigazeljko Štefan Uram 박종훈 # List of contributors not appearing in Git history webrtc-3.1.56/DESIGN.md000066400000000000000000000050041437620512100143670ustar00rootroot00000000000000

Design

WebRTC is a powerful, but complicated technology you can build amazing things with, it comes with a steep learning curve though. Using WebRTC in the browser is easy, but outside the browser is more of a challenge. There are multiple libraries, and they all have varying levels of quality. Most are also difficult to build, and depend on libraries that aren't available in repos or portable. Pion WebRTC aims to solve all that! Built in native Go you should be able to send and receive media and text from anywhere with minimal headache. These are the design principals that drive Pion WebRTC and hopefully convince you it is worth a try. ### Portable Pion WebRTC is written in Go and extremely portable. Anywhere Golang runs, Pion WebRTC should work as well! Instead of dealing with complicated cross-compiling of multiple libraries, you now can run anywhere with one `go build` ### Flexible When possible we leave all decisions to the user. When choice is possible (like what logging library is used) we defer to the developer. ### Simple API If you know how to use WebRTC in your browser, you know how to use Pion WebRTC. We try our best just to duplicate the Javascript API, so your code can look the same everywhere. If this is your first time using WebRTC, don't worry! We have multiple [examples](https://github.com/pion/webrtc/tree/master/examples) and [GoDoc](https://pkg.go.dev/github.com/pion/webrtc/v3) ### Bring your own media Pion WebRTC doesn't make any assumptions about where your audio, video or text come from. You can use FFmpeg, GStreamer, MLT or just serve a video file. This library only serves to transport, not create media. ### Safe Golang provides a great foundation to build safe network services. Especially when running a networked service that is highly concurrent bugs can be devastating. ### Readable If code comes from an RFC we try to make sure everything is commented with a link to the spec. This makes learning and debugging easier, this WebRTC library was written to also serve as a guide for others. ### Tested Every commit is tested via travis-ci Go provides fantastic facilities for testing, and more will be added as time goes on. ### Shared libraries Every Pion project is built using shared libraries, allowing others to review and reuse our libraries. ### Community The most important part of Pion is the community. This projects only exist because of individual contributions. We aim to be radically open and do everything we can to support those that make Pion possible. webrtc-3.1.56/LICENSE000066400000000000000000000020411437620512100140770ustar00rootroot00000000000000MIT License Copyright (c) 2018 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. webrtc-3.1.56/README.md000066400000000000000000000172531437620512100143640ustar00rootroot00000000000000

Pion WebRTC
Pion WebRTC

A pure Go implementation of the WebRTC API

Pion webrtc Sourcegraph Widget Slack Widget Twitter Widget
Build Status PkgGoDev Coverage Status Go Report Card License: MIT


### Usage [Go Modules](https://blog.golang.org/using-go-modules) are mandatory for using Pion WebRTC. So make sure you set `export GO111MODULE=on`, and explicitly specify `/v2` or `/v3` when importing. **[example applications](examples/README.md)** contains code samples of common things people build with Pion WebRTC. **[example-webrtc-applications](https://github.com/pion/example-webrtc-applications)** contains more full featured examples that use 3rd party libraries. **[awesome-pion](https://github.com/pion/awesome-pion)** contains projects that have used Pion, and serve as real world examples of usage. **[GoDoc](https://pkg.go.dev/github.com/pion/webrtc/v3)** is an auto generated API reference. All our Public APIs are commented. **[FAQ](https://github.com/pion/webrtc/wiki/FAQ)** has answers to common questions. If you have a question not covered please ask in [Slack](https://pion.ly/slack) we are always looking to expand it. Now go build something awesome! Here are some **ideas** to get your creative juices flowing: * Send a video file to multiple browser in real time for perfectly synchronized movie watching. * Send a webcam on an embedded device to your browser with no additional server required! * Securely send data between two servers, without using pub/sub. * Record your webcam and do special effects server side. * Build a conferencing application that processes audio/video and make decisions off of it. * Remotely control a robots and stream its cameras in realtime. ### Want to learn more about WebRTC? Join our [Office Hours](https://github.com/pion/webrtc/wiki/OfficeHours). Come hang out, ask questions, get help debugging and hear about the cool things being built with WebRTC. We also start every meeting with basic project planning. Check out [WebRTC for the Curious](https://webrtcforthecurious.com). A book about WebRTC in depth, not just about the APIs. Learn the full details of ICE, SCTP, DTLS, SRTP, and how they work together to make up the WebRTC stack. This is also a great resource if you are trying to debug. Learn the tools of the trade and how to approach WebRTC issues. This book is vendor agnostic and will not have any Pion specific information. ### Features #### PeerConnection API * Go implementation of [webrtc-pc](https://w3c.github.io/webrtc-pc/) and [webrtc-stats](https://www.w3.org/TR/webrtc-stats/) * DataChannels * Send/Receive audio and video * Renegotiation * Plan-B and Unified Plan * [SettingEngine](https://pkg.go.dev/github.com/pion/webrtc/v3#SettingEngine) for Pion specific extensions #### Connectivity * Full ICE Agent * ICE Restart * Trickle ICE * STUN * TURN (UDP, TCP, DTLS and TLS) * mDNS candidates #### DataChannels * Ordered/Unordered * Lossy/Lossless #### Media * API with direct RTP/RTCP access * Opus, PCM, H264, VP8 and VP9 packetizer * API also allows developer to pass their own packetizer * IVF, Ogg, H264 and Matroska provided for easy sending and saving * [getUserMedia](https://github.com/pion/mediadevices) implementation (Requires Cgo) * Easy integration with x264, libvpx, GStreamer and ffmpeg. * [Simulcast](https://github.com/pion/webrtc/tree/master/examples/simulcast) * [SVC](https://github.com/pion/rtp/blob/master/codecs/vp9_packet.go#L138) * [NACK](https://github.com/pion/interceptor/pull/4) * [Sender/Receiver Reports](https://github.com/pion/interceptor/tree/master/pkg/report) * [Transport Wide Congestion Control Feedback](https://github.com/pion/interceptor/tree/master/pkg/twcc) * [Bandwidth Estimation](https://github.com/pion/webrtc/tree/master/examples/bandwidth-estimation-from-disk) #### Security * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 and TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA for DTLS v1.2 * SRTP_AEAD_AES_256_GCM and SRTP_AES128_CM_HMAC_SHA1_80 for SRTP * Hardware acceleration available for GCM suites #### Pure Go * No Cgo usage * Wide platform support * Windows, macOS, Linux, FreeBSD * iOS, Android * [WASM](https://github.com/pion/webrtc/wiki/WebAssembly-Development-and-Testing) see [examples](examples/README.md#webassembly) * 386, amd64, arm, mips, ppc64 * Easy to build *Numbers generated on Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz* * **Time to build examples/play-from-disk** - 0.66s user 0.20s system 306% cpu 0.279 total * **Time to run entire test suite** - 25.60s user 9.40s system 45% cpu 1:16.69 total * Tools to measure performance [provided](https://github.com/pion/rtsp-bench) ### Roadmap The library is in active development, please refer to the [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. We also maintain a list of [Big Ideas](https://github.com/pion/webrtc/wiki/Big-Ideas) these are things we want to build but don't have a clear plan or the resources yet. If you are looking to get involved this is a great place to get started! We would also love to hear your ideas! Even if you can't implement it yourself, it could inspire others. ### Community Pion has an active community on the [Slack](https://pion.ly/slack). Follow the [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. We are always looking to support **your projects**. Please reach out if you have something to build! If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) ### Contributing Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contributing)** to join the group of amazing people making this project possible: ### Sponsoring Work on Pion's congestion control and bandwidth estimation was funded through the [User-Operated Internet](https://nlnet.nl/useroperated/) fund, a fund established by [NLnet](https://nlnet.nl/) made possible by financial support from the [PKT Community](https://pkt.cash/)/[The Network Steward](https://pkt.cash/network-steward) and stichting [Technology Commons Trust](https://technologycommons.org/). ### License MIT License - see [LICENSE](LICENSE) for full text webrtc-3.1.56/api.go000066400000000000000000000036361437620512100142050ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "github.com/pion/interceptor" "github.com/pion/logging" ) // API allows configuration of a PeerConnection // with APIs that are available in the standard. This // lets you set custom behavior via the SettingEngine, configure // codecs via the MediaEngine and define custom media behaviors via // Interceptors. type API struct { settingEngine *SettingEngine mediaEngine *MediaEngine interceptorRegistry *interceptor.Registry interceptor interceptor.Interceptor // Generated per PeerConnection } // NewAPI Creates a new API object for keeping semi-global settings to WebRTC objects func NewAPI(options ...func(*API)) *API { a := &API{ interceptor: &interceptor.NoOp{}, settingEngine: &SettingEngine{}, mediaEngine: &MediaEngine{}, interceptorRegistry: &interceptor.Registry{}, } for _, o := range options { o(a) } if a.settingEngine.LoggerFactory == nil { a.settingEngine.LoggerFactory = logging.NewDefaultLoggerFactory() } return a } // WithMediaEngine allows providing a MediaEngine to the API. // Settings can be changed after passing the engine to an API. func WithMediaEngine(m *MediaEngine) func(a *API) { return func(a *API) { a.mediaEngine = m if a.mediaEngine == nil { a.mediaEngine = &MediaEngine{} } } } // WithSettingEngine allows providing a SettingEngine to the API. // Settings should not be changed after passing the engine to an API. func WithSettingEngine(s SettingEngine) func(a *API) { return func(a *API) { a.settingEngine = &s } } // WithInterceptorRegistry allows providing Interceptors to the API. // Settings should not be changed after passing the registry to an API. func WithInterceptorRegistry(ir *interceptor.Registry) func(a *API) { return func(a *API) { a.interceptorRegistry = ir if a.interceptorRegistry == nil { a.interceptorRegistry = &interceptor.Registry{} } } } webrtc-3.1.56/api_js.go000066400000000000000000000012461437620512100146740ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc // API bundles the global funcions of the WebRTC and ORTC API. type API struct { settingEngine *SettingEngine } // NewAPI Creates a new API object for keeping semi-global settings to WebRTC objects func NewAPI(options ...func(*API)) *API { a := &API{} for _, o := range options { o(a) } if a.settingEngine == nil { a.settingEngine = &SettingEngine{} } return a } // WithSettingEngine allows providing a SettingEngine to the API. // Settings should not be changed after passing the engine to an API. func WithSettingEngine(s SettingEngine) func(a *API) { return func(a *API) { a.settingEngine = &s } } webrtc-3.1.56/api_test.go000066400000000000000000000021011437620512100152260ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewAPI(t *testing.T) { api := NewAPI() if api.settingEngine == nil { t.Error("Failed to init settings engine") } if api.mediaEngine == nil { t.Error("Failed to init media engine") } if api.interceptorRegistry == nil { t.Error("Failed to init interceptor registry") } } func TestNewAPI_Options(t *testing.T) { s := SettingEngine{} s.DetachDataChannels() m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) api := NewAPI( WithSettingEngine(s), WithMediaEngine(&m), ) if !api.settingEngine.detach.DataChannels { t.Error("Failed to set settings engine") } if len(api.mediaEngine.audioCodecs) == 0 || len(api.mediaEngine.videoCodecs) == 0 { t.Error("Failed to set media engine") } } func TestNewAPI_OptionsDefaultize(t *testing.T) { api := NewAPI( WithMediaEngine(nil), WithInterceptorRegistry(nil), ) assert.NotNil(t, api.settingEngine) assert.NotNil(t, api.mediaEngine) assert.NotNil(t, api.interceptorRegistry) } webrtc-3.1.56/atomicbool.go000066400000000000000000000006321437620512100155550ustar00rootroot00000000000000package webrtc import "sync/atomic" type atomicBool struct { val int32 } func (b *atomicBool) set(value bool) { // nolint: unparam var i int32 if value { i = 1 } atomic.StoreInt32(&(b.val), i) } func (b *atomicBool) get() bool { return atomic.LoadInt32(&(b.val)) != 0 } func (b *atomicBool) swap(value bool) bool { var i int32 if value { i = 1 } return atomic.SwapInt32(&(b.val), i) != 0 } webrtc-3.1.56/bundlepolicy.go000066400000000000000000000041471437620512100161230ustar00rootroot00000000000000package webrtc import ( "encoding/json" ) // BundlePolicy affects which media tracks are negotiated if the remote // endpoint is not bundle-aware, and what ICE candidates are gathered. If the // remote endpoint is bundle-aware, all media tracks and data channels are // bundled onto the same transport. type BundlePolicy int const ( // BundlePolicyBalanced indicates to gather ICE candidates for each // media type in use (audio, video, and data). If the remote endpoint is // not bundle-aware, negotiate only one audio and video track on separate // transports. BundlePolicyBalanced BundlePolicy = iota + 1 // BundlePolicyMaxCompat indicates to gather ICE candidates for each // track. If the remote endpoint is not bundle-aware, negotiate all media // tracks on separate transports. BundlePolicyMaxCompat // BundlePolicyMaxBundle indicates to gather ICE candidates for only // one track. If the remote endpoint is not bundle-aware, negotiate only // one media track. BundlePolicyMaxBundle ) // This is done this way because of a linter. const ( bundlePolicyBalancedStr = "balanced" bundlePolicyMaxCompatStr = "max-compat" bundlePolicyMaxBundleStr = "max-bundle" ) func newBundlePolicy(raw string) BundlePolicy { switch raw { case bundlePolicyBalancedStr: return BundlePolicyBalanced case bundlePolicyMaxCompatStr: return BundlePolicyMaxCompat case bundlePolicyMaxBundleStr: return BundlePolicyMaxBundle default: return BundlePolicy(Unknown) } } func (t BundlePolicy) String() string { switch t { case BundlePolicyBalanced: return bundlePolicyBalancedStr case BundlePolicyMaxCompat: return bundlePolicyMaxCompatStr case BundlePolicyMaxBundle: return bundlePolicyMaxBundleStr default: return ErrUnknownType.Error() } } // UnmarshalJSON parses the JSON-encoded data and stores the result func (t *BundlePolicy) UnmarshalJSON(b []byte) error { var val string if err := json.Unmarshal(b, &val); err != nil { return err } *t = newBundlePolicy(val) return nil } // MarshalJSON returns the JSON encoding func (t BundlePolicy) MarshalJSON() ([]byte, error) { return json.Marshal(t.String()) } webrtc-3.1.56/bundlepolicy_test.go000066400000000000000000000017311437620512100171560ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewBundlePolicy(t *testing.T) { testCases := []struct { policyString string expectedPolicy BundlePolicy }{ {unknownStr, BundlePolicy(Unknown)}, {"balanced", BundlePolicyBalanced}, {"max-compat", BundlePolicyMaxCompat}, {"max-bundle", BundlePolicyMaxBundle}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedPolicy, newBundlePolicy(testCase.policyString), "testCase: %d %v", i, testCase, ) } } func TestBundlePolicy_String(t *testing.T) { testCases := []struct { policy BundlePolicy expectedString string }{ {BundlePolicy(Unknown), unknownStr}, {BundlePolicyBalanced, "balanced"}, {BundlePolicyMaxCompat, "max-compat"}, {BundlePolicyMaxBundle, "max-bundle"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.policy.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/certificate.go000066400000000000000000000160761437620512100157200ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/pem" "fmt" "math/big" "strings" "time" "github.com/pion/dtls/v2/pkg/crypto/fingerprint" "github.com/pion/webrtc/v3/pkg/rtcerr" ) // Certificate represents a x509Cert used to authenticate WebRTC communications. type Certificate struct { privateKey crypto.PrivateKey x509Cert *x509.Certificate statsID string } // NewCertificate generates a new x509 compliant Certificate to be used // by DTLS for encrypting data sent over the wire. This method differs from // GenerateCertificate by allowing to specify a template x509.Certificate to // be used in order to define certificate parameters. func NewCertificate(key crypto.PrivateKey, tpl x509.Certificate) (*Certificate, error) { var err error var certDER []byte switch sk := key.(type) { case *rsa.PrivateKey: pk := sk.Public() tpl.SignatureAlgorithm = x509.SHA256WithRSA certDER, err = x509.CreateCertificate(rand.Reader, &tpl, &tpl, pk, sk) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } case *ecdsa.PrivateKey: pk := sk.Public() tpl.SignatureAlgorithm = x509.ECDSAWithSHA256 certDER, err = x509.CreateCertificate(rand.Reader, &tpl, &tpl, pk, sk) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } default: return nil, &rtcerr.NotSupportedError{Err: ErrPrivateKeyType} } cert, err := x509.ParseCertificate(certDER) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } return &Certificate{privateKey: key, x509Cert: cert, statsID: fmt.Sprintf("certificate-%d", time.Now().UnixNano())}, nil } // Equals determines if two certificates are identical by comparing both the // secretKeys and x509Certificates. func (c Certificate) Equals(o Certificate) bool { switch cSK := c.privateKey.(type) { case *rsa.PrivateKey: if oSK, ok := o.privateKey.(*rsa.PrivateKey); ok { if cSK.N.Cmp(oSK.N) != 0 { return false } return c.x509Cert.Equal(o.x509Cert) } return false case *ecdsa.PrivateKey: if oSK, ok := o.privateKey.(*ecdsa.PrivateKey); ok { if cSK.X.Cmp(oSK.X) != 0 || cSK.Y.Cmp(oSK.Y) != 0 { return false } return c.x509Cert.Equal(o.x509Cert) } return false default: return false } } // Expires returns the timestamp after which this certificate is no longer valid. func (c Certificate) Expires() time.Time { if c.x509Cert == nil { return time.Time{} } return c.x509Cert.NotAfter } // GetFingerprints returns the list of certificate fingerprints, one of which // is computed with the digest algorithm used in the certificate signature. func (c Certificate) GetFingerprints() ([]DTLSFingerprint, error) { fingerprintAlgorithms := []crypto.Hash{crypto.SHA256} res := make([]DTLSFingerprint, len(fingerprintAlgorithms)) i := 0 for _, algo := range fingerprintAlgorithms { name, err := fingerprint.StringFromHash(algo) if err != nil { return nil, fmt.Errorf("%w: %v", ErrFailedToGenerateCertificateFingerprint, err) } value, err := fingerprint.Fingerprint(c.x509Cert, algo) if err != nil { return nil, fmt.Errorf("%w: %v", ErrFailedToGenerateCertificateFingerprint, err) } res[i] = DTLSFingerprint{ Algorithm: name, Value: value, } } return res[:i+1], nil } // GenerateCertificate causes the creation of an X.509 certificate and // corresponding private key. func GenerateCertificate(secretKey crypto.PrivateKey) (*Certificate, error) { // Max random value, a 130-bits integer, i.e 2^130 - 1 maxBigInt := new(big.Int) /* #nosec */ maxBigInt.Exp(big.NewInt(2), big.NewInt(130), nil).Sub(maxBigInt, big.NewInt(1)) /* #nosec */ serialNumber, err := rand.Int(rand.Reader, maxBigInt) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } return NewCertificate(secretKey, x509.Certificate{ Issuer: pkix.Name{CommonName: generatedCertificateOrigin}, NotBefore: time.Now().AddDate(0, 0, -1), NotAfter: time.Now().AddDate(0, 1, -1), SerialNumber: serialNumber, Version: 2, Subject: pkix.Name{CommonName: generatedCertificateOrigin}, }) } // CertificateFromX509 creates a new WebRTC Certificate from a given PrivateKey and Certificate // // This can be used if you want to share a certificate across multiple PeerConnections func CertificateFromX509(privateKey crypto.PrivateKey, certificate *x509.Certificate) Certificate { return Certificate{privateKey, certificate, fmt.Sprintf("certificate-%d", time.Now().UnixNano())} } func (c Certificate) collectStats(report *statsReportCollector) error { report.Collecting() fingerPrintAlgo, err := c.GetFingerprints() if err != nil { return err } base64Certificate := base64.RawURLEncoding.EncodeToString(c.x509Cert.Raw) stats := CertificateStats{ Timestamp: statsTimestampFrom(time.Now()), Type: StatsTypeCertificate, ID: c.statsID, Fingerprint: fingerPrintAlgo[0].Value, FingerprintAlgorithm: fingerPrintAlgo[0].Algorithm, Base64Certificate: base64Certificate, IssuerCertificateID: c.x509Cert.Issuer.String(), } report.Collect(stats.ID, stats) return nil } // CertificateFromPEM creates a fresh certificate based on a string containing // pem blocks fort the private key and x509 certificate func CertificateFromPEM(pems string) (*Certificate, error) { // decode & parse the certificate block, more := pem.Decode([]byte(pems)) if block == nil || block.Type != "CERTIFICATE" { return nil, errCertificatePEMFormatError } certBytes := make([]byte, base64.StdEncoding.DecodedLen(len(block.Bytes))) n, err := base64.StdEncoding.Decode(certBytes, block.Bytes) if err != nil { return nil, fmt.Errorf("failed to decode ceritifcate: %w", err) } cert, err := x509.ParseCertificate(certBytes[:n]) if err != nil { return nil, fmt.Errorf("failed parsing ceritifcate: %w", err) } // decode & parse the private key block, _ = pem.Decode(more) if block == nil || block.Type != "PRIVATE KEY" { return nil, errCertificatePEMFormatError } privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("unable to parse private key: %w", err) } x := CertificateFromX509(privateKey, cert) return &x, nil } // PEM returns the certificate encoded as two pem block: once for the X509 // certificate and the other for the private key func (c Certificate) PEM() (string, error) { // First write the X509 certificate var o strings.Builder xcertBytes := make( []byte, base64.StdEncoding.EncodedLen(len(c.x509Cert.Raw))) base64.StdEncoding.Encode(xcertBytes, c.x509Cert.Raw) err := pem.Encode(&o, &pem.Block{Type: "CERTIFICATE", Bytes: xcertBytes}) if err != nil { return "", fmt.Errorf("failed to pem encode the X certificate: %w", err) } // Next write the private key privBytes, err := x509.MarshalPKCS8PrivateKey(c.privateKey) if err != nil { return "", fmt.Errorf("failed to marshal private key: %w", err) } err = pem.Encode(&o, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) if err != nil { return "", fmt.Errorf("failed to encode private key: %w", err) } return o.String(), nil } webrtc-3.1.56/certificate_test.go000066400000000000000000000056471437620512100167610ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" "testing" "time" "github.com/stretchr/testify/assert" ) func TestGenerateCertificateRSA(t *testing.T) { sk, err := rsa.GenerateKey(rand.Reader, 2048) assert.Nil(t, err) skPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(sk), }) cert, err := GenerateCertificate(sk) assert.Nil(t, err) certPEM := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: cert.x509Cert.Raw, }) _, err = tls.X509KeyPair(certPEM, skPEM) assert.Nil(t, err) } func TestGenerateCertificateECDSA(t *testing.T) { sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) skDER, err := x509.MarshalECPrivateKey(sk) assert.Nil(t, err) skPEM := pem.EncodeToMemory(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: skDER, }) cert, err := GenerateCertificate(sk) assert.Nil(t, err) certPEM := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: cert.x509Cert.Raw, }) _, err = tls.X509KeyPair(certPEM, skPEM) assert.Nil(t, err) } func TestGenerateCertificateEqual(t *testing.T) { sk1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) sk3, err := rsa.GenerateKey(rand.Reader, 2048) assert.NoError(t, err) cert1, err := GenerateCertificate(sk1) assert.Nil(t, err) sk2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) cert2, err := GenerateCertificate(sk2) assert.Nil(t, err) cert3, err := GenerateCertificate(sk3) assert.NoError(t, err) assert.True(t, cert1.Equals(*cert1)) assert.False(t, cert1.Equals(*cert2)) assert.True(t, cert3.Equals(*cert3)) } func TestGenerateCertificateExpires(t *testing.T) { sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) cert, err := GenerateCertificate(sk) assert.Nil(t, err) now := time.Now() assert.False(t, cert.Expires().IsZero() || now.After(cert.Expires())) x509Cert := CertificateFromX509(sk, &x509.Certificate{}) assert.NotNil(t, x509Cert) assert.Contains(t, x509Cert.statsID, "certificate") } func TestBadCertificate(t *testing.T) { var nokey interface{} badcert, err := NewCertificate(nokey, x509.Certificate{}) assert.Nil(t, badcert) assert.Error(t, err) sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) badcert, err = NewCertificate(sk, x509.Certificate{}) assert.Nil(t, badcert) assert.Error(t, err) c0 := Certificate{} c1 := Certificate{} assert.False(t, c0.Equals(c1)) } func TestPEM(t *testing.T) { sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) cert, err := GenerateCertificate(sk) assert.Nil(t, err) pem, err := cert.PEM() assert.Nil(t, err) cert2, err := CertificateFromPEM(pem) assert.Nil(t, err) pem2, err := cert2.PEM() assert.Nil(t, err) assert.Equal(t, pem, pem2) } webrtc-3.1.56/codecov.yml000066400000000000000000000005521437620512100152440ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # coverage: status: project: default: # Allow decreasing 2% of total coverage to avoid noise. threshold: 2% patch: default: target: 70% only_pulls: true ignore: - "examples/*" - "examples/**/*" webrtc-3.1.56/configuration.go000066400000000000000000000046401437620512100162770ustar00rootroot00000000000000//go:build !js // +build !js package webrtc // A Configuration defines how peer-to-peer communication via PeerConnection // is established or re-established. // Configurations may be set up once and reused across multiple connections. // Configurations are treated as readonly. As long as they are unmodified, // they are safe for concurrent use. type Configuration struct { // ICEServers defines a slice describing servers available to be used by // ICE, such as STUN and TURN servers. ICEServers []ICEServer `json:"iceServers,omitempty"` // ICETransportPolicy indicates which candidates the ICEAgent is allowed // to use. ICETransportPolicy ICETransportPolicy `json:"iceTransportPolicy,omitempty"` // BundlePolicy indicates which media-bundling policy to use when gathering // ICE candidates. BundlePolicy BundlePolicy `json:"bundlePolicy,omitempty"` // RTCPMuxPolicy indicates which rtcp-mux policy to use when gathering ICE // candidates. RTCPMuxPolicy RTCPMuxPolicy `json:"rtcpMuxPolicy,omitempty"` // PeerIdentity sets the target peer identity for the PeerConnection. // The PeerConnection will not establish a connection to a remote peer // unless it can be successfully authenticated with the provided name. PeerIdentity string `json:"peerIdentity,omitempty"` // Certificates describes a set of certificates that the PeerConnection // uses to authenticate. Valid values for this parameter are created // through calls to the GenerateCertificate function. Although any given // DTLS connection will use only one certificate, this attribute allows the // caller to provide multiple certificates that support different // algorithms. The final certificate will be selected based on the DTLS // handshake, which establishes which certificates are allowed. The // PeerConnection implementation selects which of the certificates is // used for a given connection; how certificates are selected is outside // the scope of this specification. If this value is absent, then a default // set of certificates is generated for each PeerConnection instance. Certificates []Certificate `json:"certificates,omitempty"` // ICECandidatePoolSize describes the size of the prefetched ICE pool. ICECandidatePoolSize uint8 `json:"iceCandidatePoolSize,omitempty"` // SDPSemantics controls the type of SDP offers accepted by and // SDP answers generated by the PeerConnection. SDPSemantics SDPSemantics `json:"sdpSemantics,omitempty"` } webrtc-3.1.56/configuration_common.go000066400000000000000000000014501437620512100176430ustar00rootroot00000000000000package webrtc import "strings" // getICEServers side-steps the strict parsing mode of the ice package // (as defined in https://tools.ietf.org/html/rfc7064) by copying and then // stripping any erroneous queries from "stun(s):" URLs before parsing. func (c Configuration) getICEServers() []ICEServer { iceServers := append([]ICEServer{}, c.ICEServers...) for iceServersIndex := range iceServers { iceServers[iceServersIndex].URLs = append([]string{}, iceServers[iceServersIndex].URLs...) for urlsIndex, rawURL := range iceServers[iceServersIndex].URLs { if strings.HasPrefix(rawURL, "stun") { // strip the query from "stun(s):" if present parts := strings.Split(rawURL, "?") rawURL = parts[0] } iceServers[iceServersIndex].URLs[urlsIndex] = rawURL } } return iceServers } webrtc-3.1.56/configuration_js.go000066400000000000000000000022441437620512100167710ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc // Configuration defines a set of parameters to configure how the // peer-to-peer communication via PeerConnection is established or // re-established. type Configuration struct { // ICEServers defines a slice describing servers available to be used by // ICE, such as STUN and TURN servers. ICEServers []ICEServer // ICETransportPolicy indicates which candidates the ICEAgent is allowed // to use. ICETransportPolicy ICETransportPolicy // BundlePolicy indicates which media-bundling policy to use when gathering // ICE candidates. BundlePolicy BundlePolicy // RTCPMuxPolicy indicates which rtcp-mux policy to use when gathering ICE // candidates. RTCPMuxPolicy RTCPMuxPolicy // PeerIdentity sets the target peer identity for the PeerConnection. // The PeerConnection will not establish a connection to a remote peer // unless it can be successfully authenticated with the provided name. PeerIdentity string // Certificates are not supported in the JavaScript/Wasm bindings. // Certificates []Certificate // ICECandidatePoolSize describes the size of the prefetched ICE pool. ICECandidatePoolSize uint8 } webrtc-3.1.56/configuration_test.go000066400000000000000000000033721437620512100173370ustar00rootroot00000000000000package webrtc import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" ) func TestConfiguration_getICEServers(t *testing.T) { t.Run("Success", func(t *testing.T) { expectedServerStr := "stun:stun.l.google.com:19302" cfg := Configuration{ ICEServers: []ICEServer{ { URLs: []string{expectedServerStr}, }, }, } parsedURLs := cfg.getICEServers() assert.Equal(t, expectedServerStr, parsedURLs[0].URLs[0]) }) t.Run("Success", func(t *testing.T) { // ignore the fact that stun URLs shouldn't have a query serverStr := "stun:global.stun.twilio.com:3478?transport=udp" expectedServerStr := "stun:global.stun.twilio.com:3478" cfg := Configuration{ ICEServers: []ICEServer{ { URLs: []string{serverStr}, }, }, } parsedURLs := cfg.getICEServers() assert.Equal(t, expectedServerStr, parsedURLs[0].URLs[0]) }) } func TestConfigurationJSON(t *testing.T) { j := `{ "iceServers": [{"urls": ["turn:turn.example.org"], "username": "jch", "credential": "topsecret" }], "iceTransportPolicy": "relay", "bundlePolicy": "balanced", "rtcpMuxPolicy": "require" }` conf := Configuration{ ICEServers: []ICEServer{ { URLs: []string{"turn:turn.example.org"}, Username: "jch", Credential: "topsecret", }, }, ICETransportPolicy: ICETransportPolicyRelay, BundlePolicy: BundlePolicyBalanced, RTCPMuxPolicy: RTCPMuxPolicyRequire, } var conf2 Configuration assert.NoError(t, json.Unmarshal([]byte(j), &conf2)) assert.Equal(t, conf, conf2) j2, err := json.Marshal(conf2) assert.NoError(t, err) var conf3 Configuration assert.NoError(t, json.Unmarshal(j2, &conf3)) assert.Equal(t, conf2, conf3) } webrtc-3.1.56/constants.go000066400000000000000000000021631437620512100154420ustar00rootroot00000000000000package webrtc import "github.com/pion/dtls/v2" const ( // Unknown defines default public constant to use for "enum" like struct // comparisons when no value was defined. Unknown = iota unknownStr = "unknown" // Equal to UDP MTU receiveMTU = 1460 // simulcastProbeCount is the amount of RTP Packets // that handleUndeclaredSSRC will read and try to dispatch from // mid and rid values simulcastProbeCount = 10 // simulcastMaxProbeRoutines is how many active routines can be used to probe // If the total amount of incoming SSRCes exceeds this new requests will be ignored simulcastMaxProbeRoutines = 25 mediaSectionApplication = "application" sdpAttributeRid = "rid" rtpOutboundMTU = 1200 rtpPayloadTypeBitmask = 0x7F incomingUnhandledRTPSsrc = "Incoming unhandled RTP ssrc(%d), OnTrack will not be fired. %v" generatedCertificateOrigin = "WebRTC" sdesRepairRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id" ) func defaultSrtpProtectionProfiles() []dtls.SRTPProtectionProfile { return []dtls.SRTPProtectionProfile{dtls.SRTP_AEAD_AES_128_GCM, dtls.SRTP_AES128_CM_HMAC_SHA1_80} } webrtc-3.1.56/datachannel.go000066400000000000000000000413611437620512100156730ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "errors" "fmt" "io" "math" "sync" "sync/atomic" "time" "github.com/pion/datachannel" "github.com/pion/logging" "github.com/pion/webrtc/v3/pkg/rtcerr" ) const dataChannelBufferSize = math.MaxUint16 // message size limit for Chromium var errSCTPNotEstablished = errors.New("SCTP not established") // DataChannel represents a WebRTC DataChannel // The DataChannel interface represents a network channel // which can be used for bidirectional peer-to-peer transfers of arbitrary data type DataChannel struct { mu sync.RWMutex statsID string label string ordered bool maxPacketLifeTime *uint16 maxRetransmits *uint16 protocol string negotiated bool id *uint16 readyState atomic.Value // DataChannelState bufferedAmountLowThreshold uint64 detachCalled bool // The binaryType represents attribute MUST, on getting, return the value to // which it was last set. On setting, if the new value is either the string // "blob" or the string "arraybuffer", then set the IDL attribute to this // new value. Otherwise, throw a SyntaxError. When an DataChannel object // is created, the binaryType attribute MUST be initialized to the string // "blob". This attribute controls how binary data is exposed to scripts. // binaryType string onMessageHandler func(DataChannelMessage) openHandlerOnce sync.Once onOpenHandler func() dialHandlerOnce sync.Once onDialHandler func() onCloseHandler func() onBufferedAmountLow func() onErrorHandler func(error) sctpTransport *SCTPTransport dataChannel *datachannel.DataChannel // A reference to the associated api object used by this datachannel api *API log logging.LeveledLogger } // NewDataChannel creates a new DataChannel. // This constructor is part of the ORTC API. It is not // meant to be used together with the basic WebRTC API. func (api *API) NewDataChannel(transport *SCTPTransport, params *DataChannelParameters) (*DataChannel, error) { d, err := api.newDataChannel(params, api.settingEngine.LoggerFactory.NewLogger("ortc")) if err != nil { return nil, err } err = d.open(transport) if err != nil { return nil, err } return d, nil } // newDataChannel is an internal constructor for the data channel used to // create the DataChannel object before the networking is set up. func (api *API) newDataChannel(params *DataChannelParameters, log logging.LeveledLogger) (*DataChannel, error) { // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #5) if len(params.Label) > 65535 { return nil, &rtcerr.TypeError{Err: ErrStringSizeLimit} } d := &DataChannel{ statsID: fmt.Sprintf("DataChannel-%d", time.Now().UnixNano()), label: params.Label, protocol: params.Protocol, negotiated: params.Negotiated, id: params.ID, ordered: params.Ordered, maxPacketLifeTime: params.MaxPacketLifeTime, maxRetransmits: params.MaxRetransmits, api: api, log: log, } d.setReadyState(DataChannelStateConnecting) return d, nil } // open opens the datachannel over the sctp transport func (d *DataChannel) open(sctpTransport *SCTPTransport) error { association := sctpTransport.association() if association == nil { return errSCTPNotEstablished } d.mu.Lock() if d.sctpTransport != nil { // already open d.mu.Unlock() return nil } d.sctpTransport = sctpTransport var channelType datachannel.ChannelType var reliabilityParameter uint32 switch { case d.maxPacketLifeTime == nil && d.maxRetransmits == nil: if d.ordered { channelType = datachannel.ChannelTypeReliable } else { channelType = datachannel.ChannelTypeReliableUnordered } case d.maxRetransmits != nil: reliabilityParameter = uint32(*d.maxRetransmits) if d.ordered { channelType = datachannel.ChannelTypePartialReliableRexmit } else { channelType = datachannel.ChannelTypePartialReliableRexmitUnordered } default: reliabilityParameter = uint32(*d.maxPacketLifeTime) if d.ordered { channelType = datachannel.ChannelTypePartialReliableTimed } else { channelType = datachannel.ChannelTypePartialReliableTimedUnordered } } cfg := &datachannel.Config{ ChannelType: channelType, Priority: datachannel.ChannelPriorityNormal, ReliabilityParameter: reliabilityParameter, Label: d.label, Protocol: d.protocol, Negotiated: d.negotiated, LoggerFactory: d.api.settingEngine.LoggerFactory, } if d.id == nil { // avoid holding lock when generating ID, since id generation locks d.mu.Unlock() var dcID *uint16 err := d.sctpTransport.generateAndSetDataChannelID(d.sctpTransport.dtlsTransport.role(), &dcID) if err != nil { return err } d.mu.Lock() d.id = dcID } dc, err := datachannel.Dial(association, *d.id, cfg) if err != nil { d.mu.Unlock() return err } // bufferedAmountLowThreshold and onBufferedAmountLow might be set earlier dc.SetBufferedAmountLowThreshold(d.bufferedAmountLowThreshold) dc.OnBufferedAmountLow(d.onBufferedAmountLow) d.mu.Unlock() d.onDial() d.handleOpen(dc, false, d.negotiated) return nil } // Transport returns the SCTPTransport instance the DataChannel is sending over. func (d *DataChannel) Transport() *SCTPTransport { d.mu.RLock() defer d.mu.RUnlock() return d.sctpTransport } // After onOpen is complete check that the user called detach // and provide an error message if the call was missed func (d *DataChannel) checkDetachAfterOpen() { d.mu.RLock() defer d.mu.RUnlock() if d.api.settingEngine.detach.DataChannels && !d.detachCalled { d.log.Warn("webrtc.DetachDataChannels() enabled but didn't Detach, call Detach from OnOpen") } } // OnOpen sets an event handler which is invoked when // the underlying data transport has been established (or re-established). func (d *DataChannel) OnOpen(f func()) { d.mu.Lock() d.openHandlerOnce = sync.Once{} d.onOpenHandler = f d.mu.Unlock() if d.ReadyState() == DataChannelStateOpen { // If the data channel is already open, call the handler immediately. go d.openHandlerOnce.Do(func() { f() d.checkDetachAfterOpen() }) } } func (d *DataChannel) onOpen() { d.mu.RLock() handler := d.onOpenHandler d.mu.RUnlock() if handler != nil { go d.openHandlerOnce.Do(func() { handler() d.checkDetachAfterOpen() }) } } // OnDial sets an event handler which is invoked when the // peer has been dialed, but before said peer has responsed func (d *DataChannel) OnDial(f func()) { d.mu.Lock() d.dialHandlerOnce = sync.Once{} d.onDialHandler = f d.mu.Unlock() if d.ReadyState() == DataChannelStateOpen { // If the data channel is already open, call the handler immediately. go d.dialHandlerOnce.Do(f) } } func (d *DataChannel) onDial() { d.mu.RLock() handler := d.onDialHandler d.mu.RUnlock() if handler != nil { go d.dialHandlerOnce.Do(handler) } } // OnClose sets an event handler which is invoked when // the underlying data transport has been closed. func (d *DataChannel) OnClose(f func()) { d.mu.Lock() defer d.mu.Unlock() d.onCloseHandler = f } func (d *DataChannel) onClose() { d.mu.RLock() handler := d.onCloseHandler d.mu.RUnlock() if handler != nil { go handler() } } // OnMessage sets an event handler which is invoked on a binary // message arrival over the sctp transport from a remote peer. // OnMessage can currently receive messages up to 16384 bytes // in size. Check out the detach API if you want to use larger // message sizes. Note that browser support for larger messages // is also limited. func (d *DataChannel) OnMessage(f func(msg DataChannelMessage)) { d.mu.Lock() defer d.mu.Unlock() d.onMessageHandler = f } func (d *DataChannel) onMessage(msg DataChannelMessage) { d.mu.RLock() handler := d.onMessageHandler d.mu.RUnlock() if handler == nil { return } handler(msg) } func (d *DataChannel) handleOpen(dc *datachannel.DataChannel, isRemote, isAlreadyNegotiated bool) { d.mu.Lock() d.dataChannel = dc d.mu.Unlock() d.setReadyState(DataChannelStateOpen) // Fire the OnOpen handler immediately not using pion/datachannel // * detached datachannels have no read loop, the user needs to read and query themselves // * remote datachannels should fire OnOpened. This isn't spec compliant, but we can't break behavior yet // * already negotiated datachannels should fire OnOpened if d.api.settingEngine.detach.DataChannels || isRemote || isAlreadyNegotiated { d.onOpen() } else { dc.OnOpen(func() { d.onOpen() }) } d.mu.Lock() defer d.mu.Unlock() if !d.api.settingEngine.detach.DataChannels { go d.readLoop() } } // OnError sets an event handler which is invoked when // the underlying data transport cannot be read. func (d *DataChannel) OnError(f func(err error)) { d.mu.Lock() defer d.mu.Unlock() d.onErrorHandler = f } func (d *DataChannel) onError(err error) { d.mu.RLock() handler := d.onErrorHandler d.mu.RUnlock() if handler != nil { go handler(err) } } // See https://github.com/pion/webrtc/issues/1516 // nolint:gochecknoglobals var rlBufPool = sync.Pool{New: func() interface{} { return make([]byte, dataChannelBufferSize) }} func (d *DataChannel) readLoop() { for { buffer := rlBufPool.Get().([]byte) //nolint:forcetypeassert n, isString, err := d.dataChannel.ReadDataChannel(buffer) if err != nil { rlBufPool.Put(buffer) // nolint:staticcheck d.setReadyState(DataChannelStateClosed) if !errors.Is(err, io.EOF) { d.onError(err) } d.onClose() return } m := DataChannelMessage{Data: make([]byte, n), IsString: isString} copy(m.Data, buffer[:n]) // The 'staticcheck' pragma is a false positive on the part of the CI linter. rlBufPool.Put(buffer) // nolint:staticcheck // NB: Why was DataChannelMessage not passed as a pointer value? d.onMessage(m) // nolint:staticcheck } } // Send sends the binary message to the DataChannel peer func (d *DataChannel) Send(data []byte) error { err := d.ensureOpen() if err != nil { return err } _, err = d.dataChannel.WriteDataChannel(data, false) return err } // SendText sends the text message to the DataChannel peer func (d *DataChannel) SendText(s string) error { err := d.ensureOpen() if err != nil { return err } _, err = d.dataChannel.WriteDataChannel([]byte(s), true) return err } func (d *DataChannel) ensureOpen() error { d.mu.RLock() defer d.mu.RUnlock() if d.ReadyState() != DataChannelStateOpen { return io.ErrClosedPipe } return nil } // Detach allows you to detach the underlying datachannel. This provides // an idiomatic API to work with, however it disables the OnMessage callback. // Before calling Detach you have to enable this behavior by calling // webrtc.DetachDataChannels(). Combining detached and normal data channels // is not supported. // Please refer to the data-channels-detach example and the // pion/datachannel documentation for the correct way to handle the // resulting DataChannel object. func (d *DataChannel) Detach() (datachannel.ReadWriteCloser, error) { d.mu.Lock() defer d.mu.Unlock() if !d.api.settingEngine.detach.DataChannels { return nil, errDetachNotEnabled } if d.dataChannel == nil { return nil, errDetachBeforeOpened } d.detachCalled = true return d.dataChannel, nil } // Close Closes the DataChannel. It may be called regardless of whether // the DataChannel object was created by this peer or the remote peer. func (d *DataChannel) Close() error { d.mu.Lock() haveSctpTransport := d.dataChannel != nil d.mu.Unlock() if d.ReadyState() == DataChannelStateClosed { return nil } d.setReadyState(DataChannelStateClosing) if !haveSctpTransport { return nil } return d.dataChannel.Close() } // Label represents a label that can be used to distinguish this // DataChannel object from other DataChannel objects. Scripts are // allowed to create multiple DataChannel objects with the same label. func (d *DataChannel) Label() string { d.mu.RLock() defer d.mu.RUnlock() return d.label } // Ordered returns true if the DataChannel is ordered, and false if // out-of-order delivery is allowed. func (d *DataChannel) Ordered() bool { d.mu.RLock() defer d.mu.RUnlock() return d.ordered } // MaxPacketLifeTime represents the length of the time window (msec) during // which transmissions and retransmissions may occur in unreliable mode. func (d *DataChannel) MaxPacketLifeTime() *uint16 { d.mu.RLock() defer d.mu.RUnlock() return d.maxPacketLifeTime } // MaxRetransmits represents the maximum number of retransmissions that are // attempted in unreliable mode. func (d *DataChannel) MaxRetransmits() *uint16 { d.mu.RLock() defer d.mu.RUnlock() return d.maxRetransmits } // Protocol represents the name of the sub-protocol used with this // DataChannel. func (d *DataChannel) Protocol() string { d.mu.RLock() defer d.mu.RUnlock() return d.protocol } // Negotiated represents whether this DataChannel was negotiated by the // application (true), or not (false). func (d *DataChannel) Negotiated() bool { d.mu.RLock() defer d.mu.RUnlock() return d.negotiated } // ID represents the ID for this DataChannel. The value is initially // null, which is what will be returned if the ID was not provided at // channel creation time, and the DTLS role of the SCTP transport has not // yet been negotiated. Otherwise, it will return the ID that was either // selected by the script or generated. After the ID is set to a non-null // value, it will not change. func (d *DataChannel) ID() *uint16 { d.mu.RLock() defer d.mu.RUnlock() return d.id } // ReadyState represents the state of the DataChannel object. func (d *DataChannel) ReadyState() DataChannelState { if v, ok := d.readyState.Load().(DataChannelState); ok { return v } return DataChannelState(0) } // BufferedAmount represents the number of bytes of application data // (UTF-8 text and binary data) that have been queued using send(). Even // though the data transmission can occur in parallel, the returned value // MUST NOT be decreased before the current task yielded back to the event // loop to prevent race conditions. The value does not include framing // overhead incurred by the protocol, or buffering done by the operating // system or network hardware. The value of BufferedAmount slot will only // increase with each call to the send() method as long as the ReadyState is // open; however, BufferedAmount does not reset to zero once the channel // closes. func (d *DataChannel) BufferedAmount() uint64 { d.mu.RLock() defer d.mu.RUnlock() if d.dataChannel == nil { return 0 } return d.dataChannel.BufferedAmount() } // BufferedAmountLowThreshold represents the threshold at which the // bufferedAmount is considered to be low. When the bufferedAmount decreases // from above this threshold to equal or below it, the bufferedamountlow // event fires. BufferedAmountLowThreshold is initially zero on each new // DataChannel, but the application may change its value at any time. // The threshold is set to 0 by default. func (d *DataChannel) BufferedAmountLowThreshold() uint64 { d.mu.RLock() defer d.mu.RUnlock() if d.dataChannel == nil { return d.bufferedAmountLowThreshold } return d.dataChannel.BufferedAmountLowThreshold() } // SetBufferedAmountLowThreshold is used to update the threshold. // See BufferedAmountLowThreshold(). func (d *DataChannel) SetBufferedAmountLowThreshold(th uint64) { d.mu.Lock() defer d.mu.Unlock() d.bufferedAmountLowThreshold = th if d.dataChannel != nil { d.dataChannel.SetBufferedAmountLowThreshold(th) } } // OnBufferedAmountLow sets an event handler which is invoked when // the number of bytes of outgoing data becomes lower than the // BufferedAmountLowThreshold. func (d *DataChannel) OnBufferedAmountLow(f func()) { d.mu.Lock() defer d.mu.Unlock() d.onBufferedAmountLow = f if d.dataChannel != nil { d.dataChannel.OnBufferedAmountLow(f) } } func (d *DataChannel) getStatsID() string { d.mu.Lock() defer d.mu.Unlock() return d.statsID } func (d *DataChannel) collectStats(collector *statsReportCollector) { collector.Collecting() d.mu.Lock() defer d.mu.Unlock() stats := DataChannelStats{ Timestamp: statsTimestampNow(), Type: StatsTypeDataChannel, ID: d.statsID, Label: d.label, Protocol: d.protocol, // TransportID string `json:"transportId"` State: d.ReadyState(), } if d.id != nil { stats.DataChannelIdentifier = int32(*d.id) } if d.dataChannel != nil { stats.MessagesSent = d.dataChannel.MessagesSent() stats.BytesSent = d.dataChannel.BytesSent() stats.MessagesReceived = d.dataChannel.MessagesReceived() stats.BytesReceived = d.dataChannel.BytesReceived() } collector.Collect(stats.ID, stats) } func (d *DataChannel) setReadyState(r DataChannelState) { d.readyState.Store(r) } webrtc-3.1.56/datachannel_go_test.go000066400000000000000000000376561437620512100174330ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "bytes" "crypto/rand" "encoding/binary" "io" "io/ioutil" "math/big" "reflect" "regexp" "strings" "sync" "testing" "time" "github.com/pion/datachannel" "github.com/pion/logging" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) func TestDataChannel_EventHandlers(t *testing.T) { to := test.TimeOut(time.Second * 20) defer to.Stop() report := test.CheckRoutines(t) defer report() api := NewAPI() dc := &DataChannel{api: api} onDialCalled := make(chan struct{}) onOpenCalled := make(chan struct{}) onMessageCalled := make(chan struct{}) // Verify that the noop case works assert.NotPanics(t, func() { dc.onOpen() }) dc.OnDial(func() { close(onDialCalled) }) dc.OnOpen(func() { close(onOpenCalled) }) dc.OnMessage(func(p DataChannelMessage) { close(onMessageCalled) }) // Verify that the set handlers are called assert.NotPanics(t, func() { dc.onDial() }) assert.NotPanics(t, func() { dc.onOpen() }) assert.NotPanics(t, func() { dc.onMessage(DataChannelMessage{Data: []byte("o hai")}) }) // Wait for all handlers to be called <-onDialCalled <-onOpenCalled <-onMessageCalled } func TestDataChannel_MessagesAreOrdered(t *testing.T) { report := test.CheckRoutines(t) defer report() api := NewAPI() dc := &DataChannel{api: api} max := 512 out := make(chan int) inner := func(msg DataChannelMessage) { // randomly sleep // math/rand a weak RNG, but this does not need to be secure. Ignore with #nosec /* #nosec */ randInt, err := rand.Int(rand.Reader, big.NewInt(int64(max))) /* #nosec */ if err != nil { t.Fatalf("Failed to get random sleep duration: %s", err) } time.Sleep(time.Duration(randInt.Int64()) * time.Microsecond) s, _ := binary.Varint(msg.Data) out <- int(s) } dc.OnMessage(func(p DataChannelMessage) { inner(p) }) go func() { for i := 1; i <= max; i++ { buf := make([]byte, 8) binary.PutVarint(buf, int64(i)) dc.onMessage(DataChannelMessage{Data: buf}) // Change the registered handler a couple of times to make sure // that everything continues to work, we don't lose messages, etc. if i%2 == 0 { handler := func(msg DataChannelMessage) { inner(msg) } dc.OnMessage(handler) } } }() values := make([]int, 0, max) for v := range out { values = append(values, v) if len(values) == max { close(out) } } expected := make([]int, max) for i := 1; i <= max; i++ { expected[i-1] = i } assert.EqualValues(t, expected, values) } // Note(albrow): This test includes some features that aren't supported by the // Wasm bindings (at least for now). func TestDataChannelParamters_Go(t *testing.T) { report := test.CheckRoutines(t) defer report() t.Run("MaxPacketLifeTime exchange", func(t *testing.T) { ordered := true var maxPacketLifeTime uint16 = 3 options := &DataChannelInit{ Ordered: &ordered, MaxPacketLifeTime: &maxPacketLifeTime, } offerPC, answerPC, dc, done := setUpDataChannelParametersTest(t, options) // Check if parameters are correctly set assert.True(t, dc.Ordered(), "Ordered should be set to true") if assert.NotNil(t, dc.MaxPacketLifeTime(), "should not be nil") { assert.Equal(t, maxPacketLifeTime, *dc.MaxPacketLifeTime(), "should match") } answerPC.OnDataChannel(func(d *DataChannel) { // Make sure this is the data channel we were looking for. (Not the one // created in signalPair). if d.Label() != expectedLabel { return } // Check if parameters are correctly set assert.True(t, d.ordered, "Ordered should be set to true") if assert.NotNil(t, d.maxPacketLifeTime, "should not be nil") { assert.Equal(t, maxPacketLifeTime, *d.maxPacketLifeTime, "should match") } done <- true }) closeReliabilityParamTest(t, offerPC, answerPC, done) }) t.Run("All other property methods", func(t *testing.T) { id := uint16(123) dc := &DataChannel{} dc.id = &id dc.label = "mylabel" dc.protocol = "myprotocol" dc.negotiated = true assert.Equal(t, dc.id, dc.ID(), "should match") assert.Equal(t, dc.label, dc.Label(), "should match") assert.Equal(t, dc.protocol, dc.Protocol(), "should match") assert.Equal(t, dc.negotiated, dc.Negotiated(), "should match") assert.Equal(t, uint64(0), dc.BufferedAmount(), "should match") dc.SetBufferedAmountLowThreshold(1500) assert.Equal(t, uint64(1500), dc.BufferedAmountLowThreshold(), "should match") }) } func TestDataChannelBufferedAmount(t *testing.T) { t.Run("set before datachannel becomes open", func(t *testing.T) { report := test.CheckRoutines(t) defer report() var nCbs int buf := make([]byte, 1000) offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } done := make(chan bool) answerPC.OnDataChannel(func(d *DataChannel) { // Make sure this is the data channel we were looking for. (Not the one // created in signalPair). if d.Label() != expectedLabel { return } var nPacketsReceived int d.OnMessage(func(msg DataChannelMessage) { nPacketsReceived++ if nPacketsReceived == 10 { go func() { time.Sleep(time.Second) done <- true }() } }) assert.True(t, d.Ordered(), "Ordered should be set to true") }) dc, err := offerPC.CreateDataChannel(expectedLabel, nil) if err != nil { t.Fatalf("Failed to create a PC pair for testing") } assert.True(t, dc.Ordered(), "Ordered should be set to true") dc.OnOpen(func() { for i := 0; i < 10; i++ { e := dc.Send(buf) if e != nil { t.Fatalf("Failed to send string on data channel") } assert.Equal(t, uint64(1500), dc.BufferedAmountLowThreshold(), "value mismatch") // assert.Equal(t, (i+1)*len(buf), int(dc.BufferedAmount()), "unexpected bufferedAmount") } }) dc.OnMessage(func(msg DataChannelMessage) { }) // The value is temporarily stored in the dc object // until the dc gets opened dc.SetBufferedAmountLowThreshold(1500) // The callback function is temporarily stored in the dc object // until the dc gets opened dc.OnBufferedAmountLow(func() { nCbs++ }) err = signalPair(offerPC, answerPC) if err != nil { t.Fatalf("Failed to signal our PC pair for testing") } closePair(t, offerPC, answerPC, done) assert.True(t, nCbs > 0, "callback should be made at least once") }) t.Run("set after datachannel becomes open", func(t *testing.T) { report := test.CheckRoutines(t) defer report() var nCbs int buf := make([]byte, 1000) offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } done := make(chan bool) answerPC.OnDataChannel(func(d *DataChannel) { // Make sure this is the data channel we were looking for. (Not the one // created in signalPair). if d.Label() != expectedLabel { return } var nPacketsReceived int d.OnMessage(func(msg DataChannelMessage) { nPacketsReceived++ if nPacketsReceived == 10 { go func() { time.Sleep(time.Second) done <- true }() } }) assert.True(t, d.Ordered(), "Ordered should be set to true") }) dc, err := offerPC.CreateDataChannel(expectedLabel, nil) if err != nil { t.Fatalf("Failed to create a PC pair for testing") } assert.True(t, dc.Ordered(), "Ordered should be set to true") dc.OnOpen(func() { // The value should directly be passed to sctp dc.SetBufferedAmountLowThreshold(1500) // The callback function should directly be passed to sctp dc.OnBufferedAmountLow(func() { nCbs++ }) for i := 0; i < 10; i++ { e := dc.Send(buf) if e != nil { t.Fatalf("Failed to send string on data channel") } assert.Equal(t, uint64(1500), dc.BufferedAmountLowThreshold(), "value mismatch") // assert.Equal(t, (i+1)*len(buf), int(dc.BufferedAmount()), "unexpected bufferedAmount") } }) dc.OnMessage(func(msg DataChannelMessage) { }) err = signalPair(offerPC, answerPC) if err != nil { t.Fatalf("Failed to signal our PC pair for testing") } closePair(t, offerPC, answerPC, done) assert.True(t, nCbs > 0, "callback should be made at least once") }) } func TestEOF(t *testing.T) { report := test.CheckRoutines(t) defer report() log := logging.NewDefaultLoggerFactory().NewLogger("test") label := "test-channel" testData := []byte("this is some test data") t.Run("Detach", func(t *testing.T) { // Use Detach data channels mode s := SettingEngine{} s.DetachDataChannels() api := NewAPI(WithSettingEngine(s)) // Set up two peer connections. config := Configuration{} pca, err := api.NewPeerConnection(config) if err != nil { t.Fatal(err) } pcb, err := api.NewPeerConnection(config) if err != nil { t.Fatal(err) } defer closePairNow(t, pca, pcb) var wg sync.WaitGroup dcChan := make(chan datachannel.ReadWriteCloser) pcb.OnDataChannel(func(dc *DataChannel) { if dc.Label() != label { return } log.Debug("OnDataChannel was called") dc.OnOpen(func() { detached, err2 := dc.Detach() if err2 != nil { log.Debugf("Detach failed: %s", err2.Error()) t.Error(err2) } dcChan <- detached }) }) wg.Add(1) go func() { defer wg.Done() var msg []byte log.Debug("Waiting for OnDataChannel") dc := <-dcChan log.Debug("data channel opened") defer func() { assert.NoError(t, dc.Close(), "should succeed") }() log.Debug("Waiting for ping...") msg, err2 := ioutil.ReadAll(dc) log.Debugf("Received ping! \"%s\"", string(msg)) if err2 != nil { t.Error(err2) } if !bytes.Equal(msg, testData) { t.Errorf("expected %q, got %q", string(msg), string(testData)) } else { log.Debug("Received ping successfully!") } }() if err = signalPair(pca, pcb); err != nil { t.Fatal(err) } attached, err := pca.CreateDataChannel(label, nil) if err != nil { t.Fatal(err) } log.Debug("Waiting for data channel to open") open := make(chan struct{}) attached.OnOpen(func() { open <- struct{}{} }) <-open log.Debug("data channel opened") var dc io.ReadWriteCloser dc, err = attached.Detach() if err != nil { t.Fatal(err) } wg.Add(1) go func() { defer wg.Done() log.Debug("Sending ping...") if _, err2 := dc.Write(testData); err2 != nil { t.Error(err2) } log.Debug("Sent ping") assert.NoError(t, dc.Close(), "should succeed") log.Debug("Wating for EOF") ret, err2 := ioutil.ReadAll(dc) assert.Nil(t, err2, "should succeed") assert.Equal(t, 0, len(ret), "should be empty") }() wg.Wait() }) t.Run("No detach", func(t *testing.T) { lim := test.TimeOut(time.Second * 5) defer lim.Stop() // Set up two peer connections. config := Configuration{} pca, err := NewPeerConnection(config) if err != nil { t.Fatal(err) } pcb, err := NewPeerConnection(config) if err != nil { t.Fatal(err) } defer closePairNow(t, pca, pcb) var dca, dcb *DataChannel dcaClosedCh := make(chan struct{}) dcbClosedCh := make(chan struct{}) pcb.OnDataChannel(func(dc *DataChannel) { if dc.Label() != label { return } log.Debugf("pcb: new datachannel: %s", dc.Label()) dcb = dc // Register channel opening handling dcb.OnOpen(func() { log.Debug("pcb: datachannel opened") }) dcb.OnClose(func() { // (2) log.Debug("pcb: data channel closed") close(dcbClosedCh) }) // Register the OnMessage to handle incoming messages log.Debug("pcb: registering onMessage callback") dcb.OnMessage(func(dcMsg DataChannelMessage) { log.Debugf("pcb: received ping: %s", string(dcMsg.Data)) if !reflect.DeepEqual(dcMsg.Data, testData) { t.Error("data mismatch") } }) }) dca, err = pca.CreateDataChannel(label, nil) if err != nil { t.Fatal(err) } dca.OnOpen(func() { log.Debug("pca: data channel opened") log.Debugf("pca: sending \"%s\"", string(testData)) if err := dca.Send(testData); err != nil { t.Fatal(err) } log.Debug("pca: sent ping") assert.NoError(t, dca.Close(), "should succeed") // <-- dca closes }) dca.OnClose(func() { // (1) log.Debug("pca: data channel closed") close(dcaClosedCh) }) // Register the OnMessage to handle incoming messages log.Debug("pca: registering onMessage callback") dca.OnMessage(func(dcMsg DataChannelMessage) { log.Debugf("pca: received pong: %s", string(dcMsg.Data)) if !reflect.DeepEqual(dcMsg.Data, testData) { t.Error("data mismatch") } }) if err := signalPair(pca, pcb); err != nil { t.Fatal(err) } // When dca closes the channel, // (1) dca.Onclose() will fire immediately, then // (2) dcb.OnClose will also fire <-dcaClosedCh // (1) <-dcbClosedCh // (2) }) } // Assert that a Session Description that doesn't follow // draft-ietf-mmusic-sctp-sdp is still accepted func TestDataChannel_NonStandardSessionDescription(t *testing.T) { to := test.TimeOut(time.Second * 20) defer to.Stop() report := test.CheckRoutines(t) defer report() offerPC, answerPC, err := newPair() assert.NoError(t, err) _, err = offerPC.CreateDataChannel("foo", nil) assert.NoError(t, err) onDataChannelCalled := make(chan struct{}) answerPC.OnDataChannel(func(_ *DataChannel) { close(onDataChannelCalled) }) offer, err := offerPC.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(offerPC) assert.NoError(t, offerPC.SetLocalDescription(offer)) <-offerGatheringComplete offer = *offerPC.LocalDescription() // Replace with old values const ( oldApplication = "m=application 63743 DTLS/SCTP 5000\r" oldAttribute = "a=sctpmap:5000 webrtc-datachannel 256\r" ) offer.SDP = regexp.MustCompile(`m=application (.*?)\r`).ReplaceAllString(offer.SDP, oldApplication) offer.SDP = regexp.MustCompile(`a=sctp-port(.*?)\r`).ReplaceAllString(offer.SDP, oldAttribute) // Assert that replace worked assert.True(t, strings.Contains(offer.SDP, oldApplication)) assert.True(t, strings.Contains(offer.SDP, oldAttribute)) assert.NoError(t, answerPC.SetRemoteDescription(offer)) answer, err := answerPC.CreateAnswer(nil) assert.NoError(t, err) answerGatheringComplete := GatheringCompletePromise(answerPC) assert.NoError(t, answerPC.SetLocalDescription(answer)) <-answerGatheringComplete assert.NoError(t, offerPC.SetRemoteDescription(*answerPC.LocalDescription())) <-onDataChannelCalled closePairNow(t, offerPC, answerPC) } func TestDataChannel_Dial(t *testing.T) { t.Run("handler should be called once, by dialing peer only", func(t *testing.T) { report := test.CheckRoutines(t) defer report() dialCalls := make(chan bool, 2) wg := new(sync.WaitGroup) wg.Add(2) offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } answerPC.OnDataChannel(func(d *DataChannel) { if d.Label() != expectedLabel { return } d.OnDial(func() { // only dialing side should fire OnDial t.Fatalf("answering side should not call on dial") }) d.OnOpen(wg.Done) }) d, err := offerPC.CreateDataChannel(expectedLabel, nil) assert.NoError(t, err) d.OnDial(func() { dialCalls <- true wg.Done() }) assert.NoError(t, signalPair(offerPC, answerPC)) wg.Wait() closePairNow(t, offerPC, answerPC) assert.Len(t, dialCalls, 1) }) t.Run("handler should be called immediately if already dialed", func(t *testing.T) { report := test.CheckRoutines(t) defer report() done := make(chan bool) offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } d, err := offerPC.CreateDataChannel(expectedLabel, nil) assert.NoError(t, err) d.OnOpen(func() { // when the offer DC has been opened, its guaranteed to have dialed since it has // received a response to said dial. this test represents an unrealistic usage, // but its the best way to guarantee we "missed" the dial event and still invoke // the handler. d.OnDial(func() { done <- true }) }) assert.NoError(t, signalPair(offerPC, answerPC)) closePair(t, offerPC, answerPC, done) }) } webrtc-3.1.56/datachannel_js.go000066400000000000000000000252541437620512100163720ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import ( "fmt" "syscall/js" "github.com/pion/datachannel" ) const dataChannelBufferSize = 16384 // Lowest common denominator among browsers // DataChannel represents a WebRTC DataChannel // The DataChannel interface represents a network channel // which can be used for bidirectional peer-to-peer transfers of arbitrary data type DataChannel struct { // Pointer to the underlying JavaScript RTCPeerConnection object. underlying js.Value // Keep track of handlers/callbacks so we can call Release as required by the // syscall/js API. Initially nil. onOpenHandler *js.Func onCloseHandler *js.Func onMessageHandler *js.Func onBufferedAmountLow *js.Func // A reference to the associated api object used by this datachannel api *API } // OnOpen sets an event handler which is invoked when // the underlying data transport has been established (or re-established). func (d *DataChannel) OnOpen(f func()) { if d.onOpenHandler != nil { oldHandler := d.onOpenHandler defer oldHandler.Release() } onOpenHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { go f() return js.Undefined() }) d.onOpenHandler = &onOpenHandler d.underlying.Set("onopen", onOpenHandler) } // OnClose sets an event handler which is invoked when // the underlying data transport has been closed. func (d *DataChannel) OnClose(f func()) { if d.onCloseHandler != nil { oldHandler := d.onCloseHandler defer oldHandler.Release() } onCloseHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { go f() return js.Undefined() }) d.onCloseHandler = &onCloseHandler d.underlying.Set("onclose", onCloseHandler) } // OnMessage sets an event handler which is invoked on a binary message arrival // from a remote peer. Note that browsers may place limitations on message size. func (d *DataChannel) OnMessage(f func(msg DataChannelMessage)) { if d.onMessageHandler != nil { oldHandler := d.onMessageHandler defer oldHandler.Release() } onMessageHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { // pion/webrtc/projects/15 data := args[0].Get("data") go func() { // valueToDataChannelMessage may block when handling 'Blob' data // so we need to call it from a new routine. See: // https://pkg.go.dev/syscall/js#FuncOf msg := valueToDataChannelMessage(data) f(msg) }() return js.Undefined() }) d.onMessageHandler = &onMessageHandler d.underlying.Set("onmessage", onMessageHandler) } // Send sends the binary message to the DataChannel peer func (d *DataChannel) Send(data []byte) (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() array := js.Global().Get("Uint8Array").New(len(data)) js.CopyBytesToJS(array, data) d.underlying.Call("send", array) return nil } // SendText sends the text message to the DataChannel peer func (d *DataChannel) SendText(s string) (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() d.underlying.Call("send", s) return nil } // Detach allows you to detach the underlying datachannel. This provides // an idiomatic API to work with, however it disables the OnMessage callback. // Before calling Detach you have to enable this behavior by calling // webrtc.DetachDataChannels(). Combining detached and normal data channels // is not supported. // Please reffer to the data-channels-detach example and the // pion/datachannel documentation for the correct way to handle the // resulting DataChannel object. func (d *DataChannel) Detach() (datachannel.ReadWriteCloser, error) { if !d.api.settingEngine.detach.DataChannels { return nil, fmt.Errorf("enable detaching by calling webrtc.DetachDataChannels()") } detached := newDetachedDataChannel(d) return detached, nil } // Close Closes the DataChannel. It may be called regardless of whether // the DataChannel object was created by this peer or the remote peer. func (d *DataChannel) Close() (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() d.underlying.Call("close") // Release any handlers as required by the syscall/js API. if d.onOpenHandler != nil { d.onOpenHandler.Release() } if d.onCloseHandler != nil { d.onCloseHandler.Release() } if d.onMessageHandler != nil { d.onMessageHandler.Release() } if d.onBufferedAmountLow != nil { d.onBufferedAmountLow.Release() } return nil } // Label represents a label that can be used to distinguish this // DataChannel object from other DataChannel objects. Scripts are // allowed to create multiple DataChannel objects with the same label. func (d *DataChannel) Label() string { return d.underlying.Get("label").String() } // Ordered represents if the DataChannel is ordered, and false if // out-of-order delivery is allowed. func (d *DataChannel) Ordered() bool { ordered := d.underlying.Get("ordered") if ordered.IsUndefined() { return true // default is true } return ordered.Bool() } // MaxPacketLifeTime represents the length of the time window (msec) during // which transmissions and retransmissions may occur in unreliable mode. func (d *DataChannel) MaxPacketLifeTime() *uint16 { if !d.underlying.Get("maxPacketLifeTime").IsUndefined() { return valueToUint16Pointer(d.underlying.Get("maxPacketLifeTime")) } // See https://bugs.chromium.org/p/chromium/issues/detail?id=696681 // Chrome calls this "maxRetransmitTime" return valueToUint16Pointer(d.underlying.Get("maxRetransmitTime")) } // MaxRetransmits represents the maximum number of retransmissions that are // attempted in unreliable mode. func (d *DataChannel) MaxRetransmits() *uint16 { return valueToUint16Pointer(d.underlying.Get("maxRetransmits")) } // Protocol represents the name of the sub-protocol used with this // DataChannel. func (d *DataChannel) Protocol() string { return d.underlying.Get("protocol").String() } // Negotiated represents whether this DataChannel was negotiated by the // application (true), or not (false). func (d *DataChannel) Negotiated() bool { return d.underlying.Get("negotiated").Bool() } // ID represents the ID for this DataChannel. The value is initially // null, which is what will be returned if the ID was not provided at // channel creation time. Otherwise, it will return the ID that was either // selected by the script or generated. After the ID is set to a non-null // value, it will not change. func (d *DataChannel) ID() *uint16 { return valueToUint16Pointer(d.underlying.Get("id")) } // ReadyState represents the state of the DataChannel object. func (d *DataChannel) ReadyState() DataChannelState { return newDataChannelState(d.underlying.Get("readyState").String()) } // BufferedAmount represents the number of bytes of application data // (UTF-8 text and binary data) that have been queued using send(). Even // though the data transmission can occur in parallel, the returned value // MUST NOT be decreased before the current task yielded back to the event // loop to prevent race conditions. The value does not include framing // overhead incurred by the protocol, or buffering done by the operating // system or network hardware. The value of BufferedAmount slot will only // increase with each call to the send() method as long as the ReadyState is // open; however, BufferedAmount does not reset to zero once the channel // closes. func (d *DataChannel) BufferedAmount() uint64 { return uint64(d.underlying.Get("bufferedAmount").Int()) } // BufferedAmountLowThreshold represents the threshold at which the // bufferedAmount is considered to be low. When the bufferedAmount decreases // from above this threshold to equal or below it, the bufferedamountlow // event fires. BufferedAmountLowThreshold is initially zero on each new // DataChannel, but the application may change its value at any time. func (d *DataChannel) BufferedAmountLowThreshold() uint64 { return uint64(d.underlying.Get("bufferedAmountLowThreshold").Int()) } // SetBufferedAmountLowThreshold is used to update the threshold. // See BufferedAmountLowThreshold(). func (d *DataChannel) SetBufferedAmountLowThreshold(th uint64) { d.underlying.Set("bufferedAmountLowThreshold", th) } // OnBufferedAmountLow sets an event handler which is invoked when // the number of bytes of outgoing data becomes lower than the // BufferedAmountLowThreshold. func (d *DataChannel) OnBufferedAmountLow(f func()) { if d.onBufferedAmountLow != nil { oldHandler := d.onBufferedAmountLow defer oldHandler.Release() } onBufferedAmountLow := js.FuncOf(func(this js.Value, args []js.Value) interface{} { go f() return js.Undefined() }) d.onBufferedAmountLow = &onBufferedAmountLow d.underlying.Set("onbufferedamountlow", onBufferedAmountLow) } // valueToDataChannelMessage converts the given value to a DataChannelMessage. // val should be obtained from MessageEvent.data where MessageEvent is received // via the RTCDataChannel.onmessage callback. func valueToDataChannelMessage(val js.Value) DataChannelMessage { // If val is of type string, the conversion is straightforward. if val.Type() == js.TypeString { return DataChannelMessage{ IsString: true, Data: []byte(val.String()), } } // For other types, we need to first determine val.constructor.name. constructorName := val.Get("constructor").Get("name").String() var data []byte switch constructorName { case "Uint8Array": // We can easily convert Uint8Array to []byte data = uint8ArrayValueToBytes(val) case "Blob": // Convert the Blob to an ArrayBuffer and then convert the ArrayBuffer // to a Uint8Array. // See: https://developer.mozilla.org/en-US/docs/Web/API/Blob // The JavaScript API for reading from the Blob is asynchronous. We use a // channel to signal when reading is done. reader := js.Global().Get("FileReader").New() doneChan := make(chan struct{}) reader.Call("addEventListener", "loadend", js.FuncOf(func(this js.Value, args []js.Value) interface{} { go func() { // Signal that the FileReader is done reading/loading by sending through // the doneChan. doneChan <- struct{}{} }() return js.Undefined() })) reader.Call("readAsArrayBuffer", val) // Wait for the FileReader to finish reading/loading. <-doneChan // At this point buffer.result is a typed array, which we know how to // handle. buffer := reader.Get("result") uint8Array := js.Global().Get("Uint8Array").New(buffer) data = uint8ArrayValueToBytes(uint8Array) default: // Assume we have an ArrayBufferView type which we can convert to a // Uint8Array in JavaScript. // See: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView uint8Array := js.Global().Get("Uint8Array").New(val) data = uint8ArrayValueToBytes(uint8Array) } return DataChannelMessage{ IsString: false, Data: data, } } webrtc-3.1.56/datachannel_js_detach.go000066400000000000000000000025431437620512100176760ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import ( "errors" ) type detachedDataChannel struct { dc *DataChannel read chan DataChannelMessage done chan struct{} } func newDetachedDataChannel(dc *DataChannel) *detachedDataChannel { read := make(chan DataChannelMessage) done := make(chan struct{}) // Wire up callbacks dc.OnMessage(func(msg DataChannelMessage) { read <- msg // pion/webrtc/projects/15 }) // pion/webrtc/projects/15 return &detachedDataChannel{ dc: dc, read: read, done: done, } } func (c *detachedDataChannel) Read(p []byte) (int, error) { n, _, err := c.ReadDataChannel(p) return n, err } func (c *detachedDataChannel) ReadDataChannel(p []byte) (int, bool, error) { select { case <-c.done: return 0, false, errors.New("Reader closed") case msg := <-c.read: n := copy(p, msg.Data) if n < len(msg.Data) { return n, msg.IsString, errors.New("Read buffer to small") } return n, msg.IsString, nil } } func (c *detachedDataChannel) Write(p []byte) (n int, err error) { return c.WriteDataChannel(p, false) } func (c *detachedDataChannel) WriteDataChannel(p []byte, isString bool) (n int, err error) { if isString { err = c.dc.SendText(string(p)) return len(p), err } err = c.dc.Send(p) return len(p), err } func (c *detachedDataChannel) Close() error { close(c.done) return c.dc.Close() } webrtc-3.1.56/datachannel_test.go000066400000000000000000000331261437620512100167320ustar00rootroot00000000000000package webrtc import ( "fmt" "io" "sync" "testing" "time" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) // expectedLabel represents the label of the data channel we are trying to test. // Some other channels may have been created during initialization (in the Wasm // bindings this is a requirement). const expectedLabel = "data" func closePairNow(t testing.TB, pc1, pc2 io.Closer) { var fail bool if err := pc1.Close(); err != nil { t.Errorf("Failed to close PeerConnection: %v", err) fail = true } if err := pc2.Close(); err != nil { t.Errorf("Failed to close PeerConnection: %v", err) fail = true } if fail { t.FailNow() } } func closePair(t *testing.T, pc1, pc2 io.Closer, done <-chan bool) { select { case <-time.After(10 * time.Second): t.Fatalf("closePair timed out waiting for done signal") case <-done: closePairNow(t, pc1, pc2) } } func setUpDataChannelParametersTest(t *testing.T, options *DataChannelInit) (*PeerConnection, *PeerConnection, *DataChannel, chan bool) { offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } done := make(chan bool) dc, err := offerPC.CreateDataChannel(expectedLabel, options) if err != nil { t.Fatalf("Failed to create a PC pair for testing") } return offerPC, answerPC, dc, done } func closeReliabilityParamTest(t *testing.T, pc1, pc2 *PeerConnection, done chan bool) { err := signalPair(pc1, pc2) if err != nil { t.Fatalf("Failed to signal our PC pair for testing") } closePair(t, pc1, pc2, done) } func BenchmarkDataChannelSend2(b *testing.B) { benchmarkDataChannelSend(b, 2) } func BenchmarkDataChannelSend4(b *testing.B) { benchmarkDataChannelSend(b, 4) } func BenchmarkDataChannelSend8(b *testing.B) { benchmarkDataChannelSend(b, 8) } func BenchmarkDataChannelSend16(b *testing.B) { benchmarkDataChannelSend(b, 16) } func BenchmarkDataChannelSend32(b *testing.B) { benchmarkDataChannelSend(b, 32) } // See https://github.com/pion/webrtc/issues/1516 func benchmarkDataChannelSend(b *testing.B, numChannels int) { offerPC, answerPC, err := newPair() if err != nil { b.Fatalf("Failed to create a PC pair for testing") } open := make(map[string]chan bool) answerPC.OnDataChannel(func(d *DataChannel) { if _, ok := open[d.Label()]; !ok { // Ignore anything unknown channel label. return } d.OnOpen(func() { open[d.Label()] <- true }) }) var wg sync.WaitGroup for i := 0; i < numChannels; i++ { label := fmt.Sprintf("dc-%d", i) open[label] = make(chan bool) wg.Add(1) dc, err := offerPC.CreateDataChannel(label, nil) assert.NoError(b, err) dc.OnOpen(func() { <-open[label] for n := 0; n < b.N/numChannels; n++ { if err := dc.SendText("Ping"); err != nil { b.Fatalf("Unexpected error sending data (label=%q): %v", label, err) } } wg.Done() }) } assert.NoError(b, signalPair(offerPC, answerPC)) wg.Wait() closePairNow(b, offerPC, answerPC) } func TestDataChannel_Open(t *testing.T) { const openOnceChannelCapacity = 2 t.Run("handler should be called once", func(t *testing.T) { report := test.CheckRoutines(t) defer report() offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } done := make(chan bool) openCalls := make(chan bool, openOnceChannelCapacity) answerPC.OnDataChannel(func(d *DataChannel) { if d.Label() != expectedLabel { return } d.OnOpen(func() { openCalls <- true }) d.OnMessage(func(msg DataChannelMessage) { go func() { // Wait a little bit to ensure all messages are processed. time.Sleep(100 * time.Millisecond) done <- true }() }) }) dc, err := offerPC.CreateDataChannel(expectedLabel, nil) assert.NoError(t, err) dc.OnOpen(func() { e := dc.SendText("Ping") if e != nil { t.Fatalf("Failed to send string on data channel") } }) assert.NoError(t, signalPair(offerPC, answerPC)) closePair(t, offerPC, answerPC, done) assert.Len(t, openCalls, 1) }) t.Run("handler should be called once when already negotiated", func(t *testing.T) { report := test.CheckRoutines(t) defer report() offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } done := make(chan bool) answerOpenCalls := make(chan bool, openOnceChannelCapacity) offerOpenCalls := make(chan bool, openOnceChannelCapacity) negotiated := true ordered := true dataChannelID := uint16(0) answerDC, err := answerPC.CreateDataChannel(expectedLabel, &DataChannelInit{ ID: &dataChannelID, Negotiated: &negotiated, Ordered: &ordered, }) assert.NoError(t, err) offerDC, err := offerPC.CreateDataChannel(expectedLabel, &DataChannelInit{ ID: &dataChannelID, Negotiated: &negotiated, Ordered: &ordered, }) assert.NoError(t, err) answerDC.OnMessage(func(msg DataChannelMessage) { go func() { // Wait a little bit to ensure all messages are processed. time.Sleep(100 * time.Millisecond) done <- true }() }) answerDC.OnOpen(func() { answerOpenCalls <- true }) offerDC.OnOpen(func() { offerOpenCalls <- true e := offerDC.SendText("Ping") if e != nil { t.Fatalf("Failed to send string on data channel") } }) assert.NoError(t, signalPair(offerPC, answerPC)) closePair(t, offerPC, answerPC, done) assert.Len(t, answerOpenCalls, 1) assert.Len(t, offerOpenCalls, 1) }) } func TestDataChannel_Send(t *testing.T) { t.Run("before signaling", func(t *testing.T) { report := test.CheckRoutines(t) defer report() offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } done := make(chan bool) answerPC.OnDataChannel(func(d *DataChannel) { // Make sure this is the data channel we were looking for. (Not the one // created in signalPair). if d.Label() != expectedLabel { return } d.OnMessage(func(msg DataChannelMessage) { e := d.Send([]byte("Pong")) if e != nil { t.Fatalf("Failed to send string on data channel") } }) assert.True(t, d.Ordered(), "Ordered should be set to true") }) dc, err := offerPC.CreateDataChannel(expectedLabel, nil) if err != nil { t.Fatalf("Failed to create a PC pair for testing") } assert.True(t, dc.Ordered(), "Ordered should be set to true") dc.OnOpen(func() { e := dc.SendText("Ping") if e != nil { t.Fatalf("Failed to send string on data channel") } }) dc.OnMessage(func(msg DataChannelMessage) { done <- true }) err = signalPair(offerPC, answerPC) if err != nil { t.Fatalf("Failed to signal our PC pair for testing: %+v", err) } closePair(t, offerPC, answerPC, done) }) t.Run("after connected", func(t *testing.T) { report := test.CheckRoutines(t) defer report() offerPC, answerPC, err := newPair() if err != nil { t.Fatalf("Failed to create a PC pair for testing") } done := make(chan bool) answerPC.OnDataChannel(func(d *DataChannel) { // Make sure this is the data channel we were looking for. (Not the one // created in signalPair). if d.Label() != expectedLabel { return } d.OnMessage(func(msg DataChannelMessage) { e := d.Send([]byte("Pong")) if e != nil { t.Fatalf("Failed to send string on data channel") } }) assert.True(t, d.Ordered(), "Ordered should be set to true") }) once := &sync.Once{} offerPC.OnICEConnectionStateChange(func(state ICEConnectionState) { if state == ICEConnectionStateConnected || state == ICEConnectionStateCompleted { // wasm fires completed state multiple times once.Do(func() { dc, createErr := offerPC.CreateDataChannel(expectedLabel, nil) if createErr != nil { t.Fatalf("Failed to create a PC pair for testing") } assert.True(t, dc.Ordered(), "Ordered should be set to true") dc.OnMessage(func(msg DataChannelMessage) { done <- true }) if e := dc.SendText("Ping"); e != nil { // wasm binding doesn't fire OnOpen (we probably already missed it) dc.OnOpen(func() { e = dc.SendText("Ping") if e != nil { t.Fatalf("Failed to send string on data channel") } }) } }) } }) err = signalPair(offerPC, answerPC) if err != nil { t.Fatalf("Failed to signal our PC pair for testing") } closePair(t, offerPC, answerPC, done) }) } func TestDataChannel_Close(t *testing.T) { report := test.CheckRoutines(t) defer report() t.Run("Close after PeerConnection Closed", func(t *testing.T) { offerPC, answerPC, err := newPair() assert.NoError(t, err) dc, err := offerPC.CreateDataChannel(expectedLabel, nil) assert.NoError(t, err) closePairNow(t, offerPC, answerPC) assert.NoError(t, dc.Close()) }) t.Run("Close before connected", func(t *testing.T) { offerPC, answerPC, err := newPair() assert.NoError(t, err) dc, err := offerPC.CreateDataChannel(expectedLabel, nil) assert.NoError(t, err) assert.NoError(t, dc.Close()) closePairNow(t, offerPC, answerPC) }) } func TestDataChannelParameters(t *testing.T) { report := test.CheckRoutines(t) defer report() t.Run("MaxPacketLifeTime exchange", func(t *testing.T) { ordered := true maxPacketLifeTime := uint16(3) options := &DataChannelInit{ Ordered: &ordered, MaxPacketLifeTime: &maxPacketLifeTime, } offerPC, answerPC, dc, done := setUpDataChannelParametersTest(t, options) // Check if parameters are correctly set assert.Equal(t, dc.Ordered(), ordered, "Ordered should be same value as set in DataChannelInit") if assert.NotNil(t, dc.MaxPacketLifeTime(), "should not be nil") { assert.Equal(t, maxPacketLifeTime, *dc.MaxPacketLifeTime(), "should match") } answerPC.OnDataChannel(func(d *DataChannel) { if d.Label() != expectedLabel { return } // Check if parameters are correctly set assert.Equal(t, d.Ordered(), ordered, "Ordered should be same value as set in DataChannelInit") if assert.NotNil(t, d.MaxPacketLifeTime(), "should not be nil") { assert.Equal(t, maxPacketLifeTime, *d.MaxPacketLifeTime(), "should match") } done <- true }) closeReliabilityParamTest(t, offerPC, answerPC, done) }) t.Run("MaxRetransmits exchange", func(t *testing.T) { ordered := false maxRetransmits := uint16(3000) options := &DataChannelInit{ Ordered: &ordered, MaxRetransmits: &maxRetransmits, } offerPC, answerPC, dc, done := setUpDataChannelParametersTest(t, options) // Check if parameters are correctly set assert.False(t, dc.Ordered(), "Ordered should be set to false") if assert.NotNil(t, dc.MaxRetransmits(), "should not be nil") { assert.Equal(t, maxRetransmits, *dc.MaxRetransmits(), "should match") } answerPC.OnDataChannel(func(d *DataChannel) { // Make sure this is the data channel we were looking for. (Not the one // created in signalPair). if d.Label() != expectedLabel { return } // Check if parameters are correctly set assert.False(t, d.Ordered(), "Ordered should be set to false") if assert.NotNil(t, d.MaxRetransmits(), "should not be nil") { assert.Equal(t, maxRetransmits, *d.MaxRetransmits(), "should match") } done <- true }) closeReliabilityParamTest(t, offerPC, answerPC, done) }) t.Run("Protocol exchange", func(t *testing.T) { protocol := "json" options := &DataChannelInit{ Protocol: &protocol, } offerPC, answerPC, dc, done := setUpDataChannelParametersTest(t, options) // Check if parameters are correctly set assert.Equal(t, protocol, dc.Protocol(), "Protocol should match DataChannelInit") answerPC.OnDataChannel(func(d *DataChannel) { // Make sure this is the data channel we were looking for. (Not the one // created in signalPair). if d.Label() != expectedLabel { return } // Check if parameters are correctly set assert.Equal(t, protocol, d.Protocol(), "Protocol should match what channel creator declared") done <- true }) closeReliabilityParamTest(t, offerPC, answerPC, done) }) t.Run("Negotiated exchange", func(t *testing.T) { const expectedMessage = "Hello World" negotiated := true var id uint16 = 500 options := &DataChannelInit{ Negotiated: &negotiated, ID: &id, } offerPC, answerPC, offerDatachannel, done := setUpDataChannelParametersTest(t, options) answerDatachannel, err := answerPC.CreateDataChannel(expectedLabel, options) assert.NoError(t, err) answerPC.OnDataChannel(func(d *DataChannel) { // Ignore our default channel, exists to force ICE candidates. See signalPair for more info if d.Label() == "initial_data_channel" { return } t.Fatal("OnDataChannel must not be fired when negotiated == true") }) offerPC.OnDataChannel(func(d *DataChannel) { t.Fatal("OnDataChannel must not be fired when negotiated == true") }) seenAnswerMessage := &atomicBool{} seenOfferMessage := &atomicBool{} answerDatachannel.OnMessage(func(msg DataChannelMessage) { if msg.IsString && string(msg.Data) == expectedMessage { seenAnswerMessage.set(true) } }) offerDatachannel.OnMessage(func(msg DataChannelMessage) { if msg.IsString && string(msg.Data) == expectedMessage { seenOfferMessage.set(true) } }) go func() { for { if seenAnswerMessage.get() && seenOfferMessage.get() { break } if offerDatachannel.ReadyState() == DataChannelStateOpen { assert.NoError(t, offerDatachannel.SendText(expectedMessage)) } if answerDatachannel.ReadyState() == DataChannelStateOpen { assert.NoError(t, answerDatachannel.SendText(expectedMessage)) } time.Sleep(500 * time.Millisecond) } done <- true }() closeReliabilityParamTest(t, offerPC, answerPC, done) }) } webrtc-3.1.56/datachannelinit.go000066400000000000000000000025051437620512100165540ustar00rootroot00000000000000package webrtc // DataChannelInit can be used to configure properties of the underlying // channel such as data reliability. type DataChannelInit struct { // Ordered indicates if data is allowed to be delivered out of order. The // default value of true, guarantees that data will be delivered in order. Ordered *bool // MaxPacketLifeTime limits the time (in milliseconds) during which the // channel will transmit or retransmit data if not acknowledged. This value // may be clamped if it exceeds the maximum value supported. MaxPacketLifeTime *uint16 // MaxRetransmits limits the number of times a channel will retransmit data // if not successfully delivered. This value may be clamped if it exceeds // the maximum value supported. MaxRetransmits *uint16 // Protocol describes the subprotocol name used for this channel. Protocol *string // Negotiated describes if the data channel is created by the local peer or // the remote peer. The default value of false tells the user agent to // announce the channel in-band and instruct the other peer to dispatch a // corresponding DataChannel. If set to true, it is up to the application // to negotiate the channel and create an DataChannel with the same id // at the other peer. Negotiated *bool // ID overrides the default selection of ID for this channel. ID *uint16 } webrtc-3.1.56/datachannelmessage.go000066400000000000000000000004361437620512100172360ustar00rootroot00000000000000package webrtc // DataChannelMessage represents a message received from the // data channel. IsString will be set to true if the incoming // message is of the string type. Otherwise the message is of // a binary type. type DataChannelMessage struct { IsString bool Data []byte } webrtc-3.1.56/datachannelparameters.go000066400000000000000000000007011437620512100177500ustar00rootroot00000000000000package webrtc // DataChannelParameters describes the configuration of the DataChannel. type DataChannelParameters struct { Label string `json:"label"` Protocol string `json:"protocol"` ID *uint16 `json:"id"` Ordered bool `json:"ordered"` MaxPacketLifeTime *uint16 `json:"maxPacketLifeTime"` MaxRetransmits *uint16 `json:"maxRetransmits"` Negotiated bool `json:"negotiated"` } webrtc-3.1.56/datachannelstate.go000066400000000000000000000034211437620512100167270ustar00rootroot00000000000000package webrtc // DataChannelState indicates the state of a data channel. type DataChannelState int const ( // DataChannelStateConnecting indicates that the data channel is being // established. This is the initial state of DataChannel, whether created // with CreateDataChannel, or dispatched as a part of an DataChannelEvent. DataChannelStateConnecting DataChannelState = iota + 1 // DataChannelStateOpen indicates that the underlying data transport is // established and communication is possible. DataChannelStateOpen // DataChannelStateClosing indicates that the procedure to close down the // underlying data transport has started. DataChannelStateClosing // DataChannelStateClosed indicates that the underlying data transport // has been closed or could not be established. DataChannelStateClosed ) // This is done this way because of a linter. const ( dataChannelStateConnectingStr = "connecting" dataChannelStateOpenStr = "open" dataChannelStateClosingStr = "closing" dataChannelStateClosedStr = "closed" ) func newDataChannelState(raw string) DataChannelState { switch raw { case dataChannelStateConnectingStr: return DataChannelStateConnecting case dataChannelStateOpenStr: return DataChannelStateOpen case dataChannelStateClosingStr: return DataChannelStateClosing case dataChannelStateClosedStr: return DataChannelStateClosed default: return DataChannelState(Unknown) } } func (t DataChannelState) String() string { switch t { case DataChannelStateConnecting: return dataChannelStateConnectingStr case DataChannelStateOpen: return dataChannelStateOpenStr case DataChannelStateClosing: return dataChannelStateClosingStr case DataChannelStateClosed: return dataChannelStateClosedStr default: return ErrUnknownType.Error() } } webrtc-3.1.56/datachannelstate_test.go000066400000000000000000000020741437620512100177710ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewDataChannelState(t *testing.T) { testCases := []struct { stateString string expectedState DataChannelState }{ {unknownStr, DataChannelState(Unknown)}, {"connecting", DataChannelStateConnecting}, {"open", DataChannelStateOpen}, {"closing", DataChannelStateClosing}, {"closed", DataChannelStateClosed}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedState, newDataChannelState(testCase.stateString), "testCase: %d %v", i, testCase, ) } } func TestDataChannelState_String(t *testing.T) { testCases := []struct { state DataChannelState expectedString string }{ {DataChannelState(Unknown), unknownStr}, {DataChannelStateConnecting, "connecting"}, {DataChannelStateOpen, "open"}, {DataChannelStateClosing, "closing"}, {DataChannelStateClosed, "closed"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.state.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/dtlsfingerprint.go000066400000000000000000000010751437620512100166450ustar00rootroot00000000000000package webrtc // DTLSFingerprint specifies the hash function algorithm and certificate // fingerprint as described in https://tools.ietf.org/html/rfc4572. type DTLSFingerprint struct { // Algorithm specifies one of the the hash function algorithms defined in // the 'Hash function Textual Names' registry. Algorithm string `json:"algorithm"` // Value specifies the value of the certificate fingerprint in lowercase // hex string as expressed utilizing the syntax of 'fingerprint' in // https://tools.ietf.org/html/rfc4572#section-5. Value string `json:"value"` } webrtc-3.1.56/dtlsparameters.go000066400000000000000000000003271437620512100164600ustar00rootroot00000000000000package webrtc // DTLSParameters holds information relating to DTLS configuration. type DTLSParameters struct { Role DTLSRole `json:"role"` Fingerprints []DTLSFingerprint `json:"fingerprints"` } webrtc-3.1.56/dtlsrole.go000066400000000000000000000045671437620512100152700ustar00rootroot00000000000000package webrtc import ( "github.com/pion/sdp/v3" ) // DTLSRole indicates the role of the DTLS transport. type DTLSRole byte const ( // DTLSRoleAuto defines the DTLS role is determined based on // the resolved ICE role: the ICE controlled role acts as the DTLS // client and the ICE controlling role acts as the DTLS server. DTLSRoleAuto DTLSRole = iota + 1 // DTLSRoleClient defines the DTLS client role. DTLSRoleClient // DTLSRoleServer defines the DTLS server role. DTLSRoleServer ) const ( // https://tools.ietf.org/html/rfc5763 /* The answerer MUST use either a setup attribute value of setup:active or setup:passive. Note that if the answerer uses setup:passive, then the DTLS handshake will not begin until the answerer is received, which adds additional latency. setup:active allows the answer and the DTLS handshake to occur in parallel. Thus, setup:active is RECOMMENDED. */ defaultDtlsRoleAnswer = DTLSRoleClient /* The endpoint that is the offerer MUST use the setup attribute value of setup:actpass and be prepared to receive a client_hello before it receives the answer. */ defaultDtlsRoleOffer = DTLSRoleAuto ) func (r DTLSRole) String() string { switch r { case DTLSRoleAuto: return "auto" case DTLSRoleClient: return "client" case DTLSRoleServer: return "server" default: return unknownStr } } // Iterate a SessionDescription from a remote to determine if an explicit // role can been determined from it. The decision is made from the first role we we parse. // If no role can be found we return DTLSRoleAuto func dtlsRoleFromRemoteSDP(sessionDescription *sdp.SessionDescription) DTLSRole { if sessionDescription == nil { return DTLSRoleAuto } for _, mediaSection := range sessionDescription.MediaDescriptions { for _, attribute := range mediaSection.Attributes { if attribute.Key == "setup" { switch attribute.Value { case sdp.ConnectionRoleActive.String(): return DTLSRoleClient case sdp.ConnectionRolePassive.String(): return DTLSRoleServer default: return DTLSRoleAuto } } } } return DTLSRoleAuto } func connectionRoleFromDtlsRole(d DTLSRole) sdp.ConnectionRole { switch d { case DTLSRoleClient: return sdp.ConnectionRoleActive case DTLSRoleServer: return sdp.ConnectionRolePassive case DTLSRoleAuto: return sdp.ConnectionRoleActpass default: return sdp.ConnectionRole(0) } } webrtc-3.1.56/dtlsrole_test.go000066400000000000000000000036461437620512100163240ustar00rootroot00000000000000package webrtc import ( "fmt" "testing" "github.com/pion/sdp/v3" "github.com/stretchr/testify/assert" ) func TestDTLSRole_String(t *testing.T) { testCases := []struct { role DTLSRole expectedString string }{ {DTLSRole(Unknown), unknownStr}, {DTLSRoleAuto, "auto"}, {DTLSRoleClient, "client"}, {DTLSRoleServer, "server"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.role.String(), "testCase: %d %v", i, testCase, ) } } func TestDTLSRoleFromRemoteSDP(t *testing.T) { parseSDP := func(raw string) *sdp.SessionDescription { parsed := &sdp.SessionDescription{} if err := parsed.Unmarshal([]byte(raw)); err != nil { panic(err) } return parsed } const noMedia = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 ` const mediaNoSetup = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=application 47299 DTLS/SCTP 5000 c=IN IP4 192.168.20.129 ` const mediaSetupDeclared = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=application 47299 DTLS/SCTP 5000 c=IN IP4 192.168.20.129 a=setup:%s ` testCases := []struct { test string sessionDescription *sdp.SessionDescription expectedRole DTLSRole }{ {"nil SessionDescription", nil, DTLSRoleAuto}, {"No MediaDescriptions", parseSDP(noMedia), DTLSRoleAuto}, {"MediaDescription, no setup", parseSDP(mediaNoSetup), DTLSRoleAuto}, {"MediaDescription, setup:actpass", parseSDP(fmt.Sprintf(mediaSetupDeclared, "actpass")), DTLSRoleAuto}, {"MediaDescription, setup:passive", parseSDP(fmt.Sprintf(mediaSetupDeclared, "passive")), DTLSRoleServer}, {"MediaDescription, setup:active", parseSDP(fmt.Sprintf(mediaSetupDeclared, "active")), DTLSRoleClient}, } for _, testCase := range testCases { assert.Equal(t, testCase.expectedRole, dtlsRoleFromRemoteSDP(testCase.sessionDescription), "TestDTLSRoleFromSDP (%s)", testCase.test, ) } } webrtc-3.1.56/dtlstransport.go000066400000000000000000000325511437620512100163550ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "errors" "fmt" "strings" "sync" "sync/atomic" "time" "github.com/pion/dtls/v2" "github.com/pion/dtls/v2/pkg/crypto/fingerprint" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/srtp/v2" "github.com/pion/webrtc/v3/internal/mux" "github.com/pion/webrtc/v3/internal/util" "github.com/pion/webrtc/v3/pkg/rtcerr" ) // DTLSTransport allows an application access to information about the DTLS // transport over which RTP and RTCP packets are sent and received by // RTPSender and RTPReceiver, as well other data such as SCTP packets sent // and received by data channels. type DTLSTransport struct { lock sync.RWMutex iceTransport *ICETransport certificates []Certificate remoteParameters DTLSParameters remoteCertificate []byte state DTLSTransportState srtpProtectionProfile srtp.ProtectionProfile onStateChangeHandler func(DTLSTransportState) conn *dtls.Conn srtpSession, srtcpSession atomic.Value srtpEndpoint, srtcpEndpoint *mux.Endpoint simulcastStreams []*srtp.ReadStreamSRTP srtpReady chan struct{} dtlsMatcher mux.MatchFunc api *API log logging.LeveledLogger } // NewDTLSTransport creates a new DTLSTransport. // This constructor is part of the ORTC API. It is not // meant to be used together with the basic WebRTC API. func (api *API) NewDTLSTransport(transport *ICETransport, certificates []Certificate) (*DTLSTransport, error) { t := &DTLSTransport{ iceTransport: transport, api: api, state: DTLSTransportStateNew, dtlsMatcher: mux.MatchDTLS, srtpReady: make(chan struct{}), log: api.settingEngine.LoggerFactory.NewLogger("DTLSTransport"), } if len(certificates) > 0 { now := time.Now() for _, x509Cert := range certificates { if !x509Cert.Expires().IsZero() && now.After(x509Cert.Expires()) { return nil, &rtcerr.InvalidAccessError{Err: ErrCertificateExpired} } t.certificates = append(t.certificates, x509Cert) } } else { sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, &rtcerr.UnknownError{Err: err} } certificate, err := GenerateCertificate(sk) if err != nil { return nil, err } t.certificates = []Certificate{*certificate} } return t, nil } // ICETransport returns the currently-configured *ICETransport or nil // if one has not been configured func (t *DTLSTransport) ICETransport() *ICETransport { t.lock.RLock() defer t.lock.RUnlock() return t.iceTransport } // onStateChange requires the caller holds the lock func (t *DTLSTransport) onStateChange(state DTLSTransportState) { t.state = state handler := t.onStateChangeHandler if handler != nil { handler(state) } } // OnStateChange sets a handler that is fired when the DTLS // connection state changes. func (t *DTLSTransport) OnStateChange(f func(DTLSTransportState)) { t.lock.Lock() defer t.lock.Unlock() t.onStateChangeHandler = f } // State returns the current dtls transport state. func (t *DTLSTransport) State() DTLSTransportState { t.lock.RLock() defer t.lock.RUnlock() return t.state } // WriteRTCP sends a user provided RTCP packet to the connected peer. If no peer is connected the // packet is discarded. func (t *DTLSTransport) WriteRTCP(pkts []rtcp.Packet) (int, error) { raw, err := rtcp.Marshal(pkts) if err != nil { return 0, err } srtcpSession, err := t.getSRTCPSession() if err != nil { return 0, err } writeStream, err := srtcpSession.OpenWriteStream() if err != nil { return 0, fmt.Errorf("%w: %v", errPeerConnWriteRTCPOpenWriteStream, err) } if n, err := writeStream.Write(raw); err != nil { return n, err } return 0, nil } // GetLocalParameters returns the DTLS parameters of the local DTLSTransport upon construction. func (t *DTLSTransport) GetLocalParameters() (DTLSParameters, error) { fingerprints := []DTLSFingerprint{} for _, c := range t.certificates { prints, err := c.GetFingerprints() if err != nil { return DTLSParameters{}, err } fingerprints = append(fingerprints, prints...) } return DTLSParameters{ Role: DTLSRoleAuto, // always returns the default role Fingerprints: fingerprints, }, nil } // GetRemoteCertificate returns the certificate chain in use by the remote side // returns an empty list prior to selection of the remote certificate func (t *DTLSTransport) GetRemoteCertificate() []byte { t.lock.RLock() defer t.lock.RUnlock() return t.remoteCertificate } func (t *DTLSTransport) startSRTP() error { srtpConfig := &srtp.Config{ Profile: t.srtpProtectionProfile, BufferFactory: t.api.settingEngine.BufferFactory, LoggerFactory: t.api.settingEngine.LoggerFactory, } if t.api.settingEngine.replayProtection.SRTP != nil { srtpConfig.RemoteOptions = append( srtpConfig.RemoteOptions, srtp.SRTPReplayProtection(*t.api.settingEngine.replayProtection.SRTP), ) } if t.api.settingEngine.disableSRTPReplayProtection { srtpConfig.RemoteOptions = append( srtpConfig.RemoteOptions, srtp.SRTPNoReplayProtection(), ) } if t.api.settingEngine.replayProtection.SRTCP != nil { srtpConfig.RemoteOptions = append( srtpConfig.RemoteOptions, srtp.SRTCPReplayProtection(*t.api.settingEngine.replayProtection.SRTCP), ) } if t.api.settingEngine.disableSRTCPReplayProtection { srtpConfig.RemoteOptions = append( srtpConfig.RemoteOptions, srtp.SRTCPNoReplayProtection(), ) } connState := t.conn.ConnectionState() err := srtpConfig.ExtractSessionKeysFromDTLS(&connState, t.role() == DTLSRoleClient) if err != nil { return fmt.Errorf("%w: %v", errDtlsKeyExtractionFailed, err) } srtpSession, err := srtp.NewSessionSRTP(t.srtpEndpoint, srtpConfig) if err != nil { return fmt.Errorf("%w: %v", errFailedToStartSRTP, err) } srtcpSession, err := srtp.NewSessionSRTCP(t.srtcpEndpoint, srtpConfig) if err != nil { return fmt.Errorf("%w: %v", errFailedToStartSRTCP, err) } t.srtpSession.Store(srtpSession) t.srtcpSession.Store(srtcpSession) close(t.srtpReady) return nil } func (t *DTLSTransport) getSRTPSession() (*srtp.SessionSRTP, error) { if value, ok := t.srtpSession.Load().(*srtp.SessionSRTP); ok { return value, nil } return nil, errDtlsTransportNotStarted } func (t *DTLSTransport) getSRTCPSession() (*srtp.SessionSRTCP, error) { if value, ok := t.srtcpSession.Load().(*srtp.SessionSRTCP); ok { return value, nil } return nil, errDtlsTransportNotStarted } func (t *DTLSTransport) role() DTLSRole { // If remote has an explicit role use the inverse switch t.remoteParameters.Role { case DTLSRoleClient: return DTLSRoleServer case DTLSRoleServer: return DTLSRoleClient default: } // If SettingEngine has an explicit role switch t.api.settingEngine.answeringDTLSRole { case DTLSRoleServer: return DTLSRoleServer case DTLSRoleClient: return DTLSRoleClient default: } // Remote was auto and no explicit role was configured via SettingEngine if t.iceTransport.Role() == ICERoleControlling { return DTLSRoleServer } return defaultDtlsRoleAnswer } // Start DTLS transport negotiation with the parameters of the remote DTLS transport func (t *DTLSTransport) Start(remoteParameters DTLSParameters) error { // Take lock and prepare connection, we must not hold the lock // when connecting prepareTransport := func() (DTLSRole, *dtls.Config, error) { t.lock.Lock() defer t.lock.Unlock() if err := t.ensureICEConn(); err != nil { return DTLSRole(0), nil, err } if t.state != DTLSTransportStateNew { return DTLSRole(0), nil, &rtcerr.InvalidStateError{Err: fmt.Errorf("%w: %s", errInvalidDTLSStart, t.state)} } t.srtpEndpoint = t.iceTransport.newEndpoint(mux.MatchSRTP) t.srtcpEndpoint = t.iceTransport.newEndpoint(mux.MatchSRTCP) t.remoteParameters = remoteParameters cert := t.certificates[0] t.onStateChange(DTLSTransportStateConnecting) return t.role(), &dtls.Config{ Certificates: []tls.Certificate{ { Certificate: [][]byte{cert.x509Cert.Raw}, PrivateKey: cert.privateKey, }, }, SRTPProtectionProfiles: func() []dtls.SRTPProtectionProfile { if len(t.api.settingEngine.srtpProtectionProfiles) > 0 { return t.api.settingEngine.srtpProtectionProfiles } return defaultSrtpProtectionProfiles() }(), ClientAuth: dtls.RequireAnyClientCert, LoggerFactory: t.api.settingEngine.LoggerFactory, InsecureSkipVerify: true, }, nil } var dtlsConn *dtls.Conn dtlsEndpoint := t.iceTransport.newEndpoint(mux.MatchDTLS) role, dtlsConfig, err := prepareTransport() if err != nil { return err } if t.api.settingEngine.replayProtection.DTLS != nil { dtlsConfig.ReplayProtectionWindow = int(*t.api.settingEngine.replayProtection.DTLS) } if t.api.settingEngine.dtls.retransmissionInterval != 0 { dtlsConfig.FlightInterval = t.api.settingEngine.dtls.retransmissionInterval } // Connect as DTLS Client/Server, function is blocking and we // must not hold the DTLSTransport lock if role == DTLSRoleClient { dtlsConn, err = dtls.Client(dtlsEndpoint, dtlsConfig) } else { dtlsConn, err = dtls.Server(dtlsEndpoint, dtlsConfig) } // Re-take the lock, nothing beyond here is blocking t.lock.Lock() defer t.lock.Unlock() if err != nil { t.onStateChange(DTLSTransportStateFailed) return err } srtpProfile, ok := dtlsConn.SelectedSRTPProtectionProfile() if !ok { t.onStateChange(DTLSTransportStateFailed) return ErrNoSRTPProtectionProfile } switch srtpProfile { case dtls.SRTP_AEAD_AES_128_GCM: t.srtpProtectionProfile = srtp.ProtectionProfileAeadAes128Gcm case dtls.SRTP_AES128_CM_HMAC_SHA1_80: t.srtpProtectionProfile = srtp.ProtectionProfileAes128CmHmacSha1_80 default: t.onStateChange(DTLSTransportStateFailed) return ErrNoSRTPProtectionProfile } // Check the fingerprint if a certificate was exchanged remoteCerts := dtlsConn.ConnectionState().PeerCertificates if len(remoteCerts) == 0 { t.onStateChange(DTLSTransportStateFailed) return errNoRemoteCertificate } t.remoteCertificate = remoteCerts[0] if !t.api.settingEngine.disableCertificateFingerprintVerification { parsedRemoteCert, err := x509.ParseCertificate(t.remoteCertificate) if err != nil { if closeErr := dtlsConn.Close(); closeErr != nil { t.log.Error(err.Error()) } t.onStateChange(DTLSTransportStateFailed) return err } if err = t.validateFingerPrint(parsedRemoteCert); err != nil { if closeErr := dtlsConn.Close(); closeErr != nil { t.log.Error(err.Error()) } t.onStateChange(DTLSTransportStateFailed) return err } } t.conn = dtlsConn t.onStateChange(DTLSTransportStateConnected) return t.startSRTP() } // Stop stops and closes the DTLSTransport object. func (t *DTLSTransport) Stop() error { t.lock.Lock() defer t.lock.Unlock() // Try closing everything and collect the errors var closeErrs []error if srtpSession, err := t.getSRTPSession(); err == nil && srtpSession != nil { closeErrs = append(closeErrs, srtpSession.Close()) } if srtcpSession, err := t.getSRTCPSession(); err == nil && srtcpSession != nil { closeErrs = append(closeErrs, srtcpSession.Close()) } for i := range t.simulcastStreams { closeErrs = append(closeErrs, t.simulcastStreams[i].Close()) } if t.conn != nil { // dtls connection may be closed on sctp close. if err := t.conn.Close(); err != nil && !errors.Is(err, dtls.ErrConnClosed) { closeErrs = append(closeErrs, err) } } t.onStateChange(DTLSTransportStateClosed) return util.FlattenErrs(closeErrs) } func (t *DTLSTransport) validateFingerPrint(remoteCert *x509.Certificate) error { for _, fp := range t.remoteParameters.Fingerprints { hashAlgo, err := fingerprint.HashFromString(fp.Algorithm) if err != nil { return err } remoteValue, err := fingerprint.Fingerprint(remoteCert, hashAlgo) if err != nil { return err } if strings.EqualFold(remoteValue, fp.Value) { return nil } } return errNoMatchingCertificateFingerprint } func (t *DTLSTransport) ensureICEConn() error { if t.iceTransport == nil { return errICEConnectionNotStarted } return nil } func (t *DTLSTransport) storeSimulcastStream(s *srtp.ReadStreamSRTP) { t.lock.Lock() defer t.lock.Unlock() t.simulcastStreams = append(t.simulcastStreams, s) } func (t *DTLSTransport) streamsForSSRC(ssrc SSRC, streamInfo interceptor.StreamInfo) (*srtp.ReadStreamSRTP, interceptor.RTPReader, *srtp.ReadStreamSRTCP, interceptor.RTCPReader, error) { srtpSession, err := t.getSRTPSession() if err != nil { return nil, nil, nil, nil, err } rtpReadStream, err := srtpSession.OpenReadStream(uint32(ssrc)) if err != nil { return nil, nil, nil, nil, err } rtpInterceptor := t.api.interceptor.BindRemoteStream(&streamInfo, interceptor.RTPReaderFunc(func(in []byte, a interceptor.Attributes) (n int, attributes interceptor.Attributes, err error) { n, err = rtpReadStream.Read(in) return n, a, err })) srtcpSession, err := t.getSRTCPSession() if err != nil { return nil, nil, nil, nil, err } rtcpReadStream, err := srtcpSession.OpenReadStream(uint32(ssrc)) if err != nil { return nil, nil, nil, nil, err } rtcpInterceptor := t.api.interceptor.BindRTCPReader(interceptor.RTPReaderFunc(func(in []byte, a interceptor.Attributes) (n int, attributes interceptor.Attributes, err error) { n, err = rtcpReadStream.Read(in) return n, a, err })) return rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor, nil } webrtc-3.1.56/dtlstransport_js.go000066400000000000000000000014141437620512100170430ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import "syscall/js" // DTLSTransport allows an application access to information about the DTLS // transport over which RTP and RTCP packets are sent and received by // RTPSender and RTPReceiver, as well other data such as SCTP packets sent // and received by data channels. type DTLSTransport struct { // Pointer to the underlying JavaScript DTLSTransport object. underlying js.Value } // ICETransport returns the currently-configured *ICETransport or nil // if one has not been configured func (r *DTLSTransport) ICETransport() *ICETransport { underlying := r.underlying.Get("iceTransport") if underlying.IsNull() || underlying.IsUndefined() { return nil } return &ICETransport{ underlying: underlying, } } webrtc-3.1.56/dtlstransport_test.go000066400000000000000000000065071437620512100174160ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "regexp" "testing" "time" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) // An invalid fingerprint MUST cause PeerConnectionState to go to PeerConnectionStateFailed func TestInvalidFingerprintCausesFailed(t *testing.T) { lim := test.TimeOut(time.Second * 40) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, err := NewPeerConnection(Configuration{}) if err != nil { t.Fatal(err) } pcAnswer, err := NewPeerConnection(Configuration{}) if err != nil { t.Fatal(err) } pcAnswer.OnDataChannel(func(_ *DataChannel) { t.Fatal("A DataChannel must not be created when Fingerprint verification fails") }) defer closePairNow(t, pcOffer, pcAnswer) offerChan := make(chan SessionDescription) pcOffer.OnICECandidate(func(candidate *ICECandidate) { if candidate == nil { offerChan <- *pcOffer.PendingLocalDescription() } }) offerConnectionHasFailed := untilConnectionState(PeerConnectionStateFailed, pcOffer) answerConnectionHasFailed := untilConnectionState(PeerConnectionStateFailed, pcAnswer) if _, err = pcOffer.CreateDataChannel("unusedDataChannel", nil); err != nil { t.Fatal(err) } offer, err := pcOffer.CreateOffer(nil) if err != nil { t.Fatal(err) } else if err := pcOffer.SetLocalDescription(offer); err != nil { t.Fatal(err) } select { case offer := <-offerChan: // Replace with invalid fingerprint re := regexp.MustCompile(`sha-256 (.*?)\r`) offer.SDP = re.ReplaceAllString(offer.SDP, "sha-256 AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA\r") if err := pcAnswer.SetRemoteDescription(offer); err != nil { t.Fatal(err) } answer, err := pcAnswer.CreateAnswer(nil) if err != nil { t.Fatal(err) } if err = pcAnswer.SetLocalDescription(answer); err != nil { t.Fatal(err) } answer.SDP = re.ReplaceAllString(answer.SDP, "sha-256 AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA\r") err = pcOffer.SetRemoteDescription(answer) if err != nil { t.Fatal(err) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting to receive offer") } offerConnectionHasFailed.Wait() answerConnectionHasFailed.Wait() assert.Equal(t, pcOffer.SCTP().Transport().State(), DTLSTransportStateFailed) assert.Nil(t, pcOffer.SCTP().Transport().conn) assert.Equal(t, pcAnswer.SCTP().Transport().State(), DTLSTransportStateFailed) assert.Nil(t, pcAnswer.SCTP().Transport().conn) } func TestPeerConnection_DTLSRoleSettingEngine(t *testing.T) { runTest := func(r DTLSRole) { s := SettingEngine{} assert.NoError(t, s.SetAnsweringDTLSRole(r)) offerPC, err := NewAPI(WithSettingEngine(s)).NewPeerConnection(Configuration{}) if err != nil { t.Fatal(err) } answerPC, err := NewAPI(WithSettingEngine(s)).NewPeerConnection(Configuration{}) if err != nil { t.Fatal(err) } if err = signalPair(offerPC, answerPC); err != nil { t.Fatal(err) } connectionComplete := untilConnectionState(PeerConnectionStateConnected, answerPC) connectionComplete.Wait() closePairNow(t, offerPC, answerPC) } report := test.CheckRoutines(t) defer report() t.Run("Server", func(t *testing.T) { runTest(DTLSRoleServer) }) t.Run("Client", func(t *testing.T) { runTest(DTLSRoleClient) }) } webrtc-3.1.56/dtlstransportstate.go000066400000000000000000000043001437620512100174050ustar00rootroot00000000000000package webrtc // DTLSTransportState indicates the DTLS transport establishment state. type DTLSTransportState int const ( // DTLSTransportStateNew indicates that DTLS has not started negotiating // yet. DTLSTransportStateNew DTLSTransportState = iota + 1 // DTLSTransportStateConnecting indicates that DTLS is in the process of // negotiating a secure connection and verifying the remote fingerprint. DTLSTransportStateConnecting // DTLSTransportStateConnected indicates that DTLS has completed // negotiation of a secure connection and verified the remote fingerprint. DTLSTransportStateConnected // DTLSTransportStateClosed indicates that the transport has been closed // intentionally as the result of receipt of a close_notify alert, or // calling close(). DTLSTransportStateClosed // DTLSTransportStateFailed indicates that the transport has failed as // the result of an error (such as receipt of an error alert or failure to // validate the remote fingerprint). DTLSTransportStateFailed ) // This is done this way because of a linter. const ( dtlsTransportStateNewStr = "new" dtlsTransportStateConnectingStr = "connecting" dtlsTransportStateConnectedStr = "connected" dtlsTransportStateClosedStr = "closed" dtlsTransportStateFailedStr = "failed" ) func newDTLSTransportState(raw string) DTLSTransportState { switch raw { case dtlsTransportStateNewStr: return DTLSTransportStateNew case dtlsTransportStateConnectingStr: return DTLSTransportStateConnecting case dtlsTransportStateConnectedStr: return DTLSTransportStateConnected case dtlsTransportStateClosedStr: return DTLSTransportStateClosed case dtlsTransportStateFailedStr: return DTLSTransportStateFailed default: return DTLSTransportState(Unknown) } } func (t DTLSTransportState) String() string { switch t { case DTLSTransportStateNew: return dtlsTransportStateNewStr case DTLSTransportStateConnecting: return dtlsTransportStateConnectingStr case DTLSTransportStateConnected: return dtlsTransportStateConnectedStr case DTLSTransportStateClosed: return dtlsTransportStateClosedStr case DTLSTransportStateFailed: return dtlsTransportStateFailedStr default: return ErrUnknownType.Error() } } webrtc-3.1.56/dtlstransportstate_test.go000066400000000000000000000022561437620512100204540ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewDTLSTransportState(t *testing.T) { testCases := []struct { stateString string expectedState DTLSTransportState }{ {unknownStr, DTLSTransportState(Unknown)}, {"new", DTLSTransportStateNew}, {"connecting", DTLSTransportStateConnecting}, {"connected", DTLSTransportStateConnected}, {"closed", DTLSTransportStateClosed}, {"failed", DTLSTransportStateFailed}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedState, newDTLSTransportState(testCase.stateString), "testCase: %d %v", i, testCase, ) } } func TestDTLSTransportState_String(t *testing.T) { testCases := []struct { state DTLSTransportState expectedString string }{ {DTLSTransportState(Unknown), unknownStr}, {DTLSTransportStateNew, "new"}, {DTLSTransportStateConnecting, "connecting"}, {DTLSTransportStateConnected, "connected"}, {DTLSTransportStateClosed, "closed"}, {DTLSTransportStateFailed, "failed"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.state.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/e2e/000077500000000000000000000000001437620512100135505ustar00rootroot00000000000000webrtc-3.1.56/e2e/Dockerfile000066400000000000000000000003541437620512100155440ustar00rootroot00000000000000FROM golang:1.17-alpine3.13 RUN apk add --no-cache \ chromium \ chromium-chromedriver ENV CGO_ENABLED=0 COPY . /go/src/github.com/pion/webrtc WORKDIR /go/src/github.com/pion/webrtc/e2e CMD ["go", "test", "-tags=e2e", "-v", "."] webrtc-3.1.56/e2e/e2e_test.go000066400000000000000000000210611437620512100156110ustar00rootroot00000000000000//go:build e2e // +build e2e package main import ( "context" "encoding/json" "fmt" "os" "strconv" "strings" "testing" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/pkg/media" "github.com/sclevine/agouti" ) var silentOpusFrame = []byte{0xf8, 0xff, 0xfe} // 20ms, 8kHz, mono var drivers = map[string]func() *agouti.WebDriver{ "Chrome": func() *agouti.WebDriver { return agouti.ChromeDriver( agouti.ChromeOptions("args", []string{ "--headless", "--disable-gpu", "--no-sandbox", }), agouti.Desired(agouti.Capabilities{ "loggingPrefs": map[string]string{ "browser": "INFO", }, }), ) }, } func TestE2E_Audio(t *testing.T) { for name, d := range drivers { driver := d() t.Run(name, func(t *testing.T) { if err := driver.Start(); err != nil { t.Fatalf("Failed to start WebDriver: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer func() { cancel() time.Sleep(50 * time.Millisecond) _ = driver.Stop() }() page, errPage := driver.NewPage() if errPage != nil { t.Fatalf("Failed to open page: %v", errPage) } if err := page.SetPageLoad(1000); err != nil { t.Fatalf("Failed to load page: %v", err) } if err := page.SetImplicitWait(1000); err != nil { t.Fatalf("Failed to set wait: %v", err) } chStarted := make(chan struct{}) chSDP := make(chan *webrtc.SessionDescription) chStats := make(chan stats) go logParseLoop(ctx, t, page, chStarted, chSDP, chStats) pwd, errPwd := os.Getwd() if errPwd != nil { t.Fatalf("Failed to get working directory: %v", errPwd) } if err := page.Navigate( fmt.Sprintf("file://%s/test.html", pwd), ); err != nil { t.Fatalf("Failed to navigate: %v", err) } sdp := <-chSDP pc, answer, track, errTrack := createTrack(*sdp) if errTrack != nil { t.Fatalf("Failed to create track: %v", errTrack) } defer func() { _ = pc.Close() }() answerBytes, errAnsSDP := json.Marshal(answer) if errAnsSDP != nil { t.Fatalf("Failed to marshal SDP: %v", errAnsSDP) } var result string if err := page.RunScript( "pc.setRemoteDescription(JSON.parse(answer))", map[string]interface{}{"answer": string(answerBytes)}, &result, ); err != nil { t.Fatalf("Failed to run script to set SDP: %v", err) } go func() { for { if err := track.WriteSample( media.Sample{Data: silentOpusFrame, Duration: time.Millisecond * 20}, ); err != nil { t.Errorf("Failed to WriteSample: %v", err) return } select { case <-time.After(20 * time.Millisecond): case <-ctx.Done(): return } } }() select { case <-chStarted: case <-time.After(5 * time.Second): t.Fatal("Timeout") } <-chStats var packetReceived [2]int for i := 0; i < 2; i++ { select { case stat := <-chStats: for _, s := range stat { if s.Type != "inbound-rtp" { continue } if s.Kind != "audio" { t.Errorf("Unused track stat received: %+v", s) continue } packetReceived[i] = s.PacketsReceived } case <-time.After(5 * time.Second): t.Fatal("Timeout") } } packetsPerSecond := packetReceived[1] - packetReceived[0] if packetsPerSecond < 45 || 55 < packetsPerSecond { t.Errorf("Number of OPUS packets is expected to be: 50/second, got: %d/second", packetsPerSecond) } }) } } func TestE2E_DataChannel(t *testing.T) { for name, d := range drivers { driver := d() t.Run(name, func(t *testing.T) { if err := driver.Start(); err != nil { t.Fatalf("Failed to start WebDriver: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer func() { cancel() time.Sleep(50 * time.Millisecond) _ = driver.Stop() }() page, errPage := driver.NewPage() if errPage != nil { t.Fatalf("Failed to open page: %v", errPage) } if err := page.SetPageLoad(1000); err != nil { t.Fatalf("Failed to load page: %v", err) } if err := page.SetImplicitWait(1000); err != nil { t.Fatalf("Failed to set wait: %v", err) } chStarted := make(chan struct{}) chSDP := make(chan *webrtc.SessionDescription) go logParseLoop(ctx, t, page, chStarted, chSDP, nil) pwd, errPwd := os.Getwd() if errPwd != nil { t.Fatalf("Failed to get working directory: %v", errPwd) } if err := page.Navigate( fmt.Sprintf("file://%s/test.html", pwd), ); err != nil { t.Fatalf("Failed to navigate: %v", err) } sdp := <-chSDP pc, errPc := webrtc.NewPeerConnection(webrtc.Configuration{}) if errPc != nil { t.Fatalf("Failed to create peer connection: %v", errPc) } defer func() { _ = pc.Close() }() chValid := make(chan struct{}) pc.OnDataChannel(func(dc *webrtc.DataChannel) { dc.OnOpen(func() { // Ping if err := dc.SendText("hello world"); err != nil { t.Errorf("Failed to send data: %v", err) } }) dc.OnMessage(func(msg webrtc.DataChannelMessage) { // Pong if string(msg.Data) != "HELLO WORLD" { t.Errorf("expected message from browser: HELLO WORLD, got: %s", string(msg.Data)) } else { chValid <- struct{}{} } }) }) if err := pc.SetRemoteDescription(*sdp); err != nil { t.Fatalf("Failed to set remote description: %v", err) } answer, errAns := pc.CreateAnswer(nil) if errAns != nil { t.Fatalf("Failed to create answer: %v", errAns) } if err := pc.SetLocalDescription(answer); err != nil { t.Fatalf("Failed to set local description: %v", err) } answerBytes, errAnsSDP := json.Marshal(answer) if errAnsSDP != nil { t.Fatalf("Failed to marshal SDP: %v", errAnsSDP) } var result string if err := page.RunScript( "pc.setRemoteDescription(JSON.parse(answer))", map[string]interface{}{"answer": string(answerBytes)}, &result, ); err != nil { t.Fatalf("Failed to run script to set SDP: %v", err) } select { case <-chStarted: case <-time.After(5 * time.Second): t.Fatal("Timeout") } select { case <-chValid: case <-time.After(5 * time.Second): t.Fatal("Timeout") } }) } } type stats []struct { Kind string `json:"kind"` Type string `json:"type"` PacketsReceived int `json:"packetsReceived"` } func logParseLoop(ctx context.Context, t *testing.T, page *agouti.Page, chStarted chan struct{}, chSDP chan *webrtc.SessionDescription, chStats chan stats) { for { select { case <-time.After(time.Second): case <-ctx.Done(): return } logs, errLog := page.ReadNewLogs("browser") if errLog != nil { t.Errorf("Failed to read log: %v", errLog) return } for _, log := range logs { k, v, ok := parseLog(log) if !ok { t.Log(log.Message) continue } switch k { case "connection": switch v { case "connected": close(chStarted) case "failed": t.Error("Browser reported connection failed") return } case "sdp": sdp := &webrtc.SessionDescription{} if err := json.Unmarshal([]byte(v), sdp); err != nil { t.Errorf("Failed to unmarshal SDP: %v", err) return } chSDP <- sdp case "stats": if chStats == nil { break } s := &stats{} if err := json.Unmarshal([]byte(v), &s); err != nil { t.Errorf("Failed to parse log: %v", err) break } select { case chStats <- *s: case <-time.After(10 * time.Millisecond): } default: t.Log(log.Message) } } } } func parseLog(log agouti.Log) (string, string, bool) { l := strings.SplitN(log.Message, " ", 4) if len(l) != 4 { return "", "", false } k, err1 := strconv.Unquote(l[2]) if err1 != nil { return "", "", false } v, err2 := strconv.Unquote(l[3]) if err2 != nil { return "", "", false } return k, v, true } func createTrack(offer webrtc.SessionDescription) (*webrtc.PeerConnection, *webrtc.SessionDescription, *webrtc.TrackLocalStaticSample, error) { pc, errPc := webrtc.NewPeerConnection(webrtc.Configuration{}) if errPc != nil { return nil, nil, nil, errPc } track, errTrack := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion") if errTrack != nil { return nil, nil, nil, errTrack } if _, err := pc.AddTrack(track); err != nil { return nil, nil, nil, err } if err := pc.SetRemoteDescription(offer); err != nil { return nil, nil, nil, err } answer, errAns := pc.CreateAnswer(nil) if errAns != nil { return nil, nil, nil, errAns } if err := pc.SetLocalDescription(answer); err != nil { return nil, nil, nil, err } return pc, &answer, track, nil } webrtc-3.1.56/e2e/test.html000066400000000000000000000020451437620512100154160ustar00rootroot00000000000000
webrtc-3.1.56/errors.go000066400000000000000000000364221437620512100147470ustar00rootroot00000000000000package webrtc import ( "errors" ) var ( // ErrUnknownType indicates an error with Unknown info. ErrUnknownType = errors.New("unknown") // ErrConnectionClosed indicates an operation executed after connection // has already been closed. ErrConnectionClosed = errors.New("connection closed") // ErrDataChannelNotOpen indicates an operation executed when the data // channel is not (yet) open. ErrDataChannelNotOpen = errors.New("data channel not open") // ErrCertificateExpired indicates that an x509 certificate has expired. ErrCertificateExpired = errors.New("x509Cert expired") // ErrNoTurnCredentials indicates that a TURN server URL was provided // without required credentials. ErrNoTurnCredentials = errors.New("turn server credentials required") // ErrTurnCredentials indicates that provided TURN credentials are partial // or malformed. ErrTurnCredentials = errors.New("invalid turn server credentials") // ErrExistingTrack indicates that a track already exists. ErrExistingTrack = errors.New("track already exists") // ErrPrivateKeyType indicates that a particular private key encryption // chosen to generate a certificate is not supported. ErrPrivateKeyType = errors.New("private key type not supported") // ErrModifyingPeerIdentity indicates that an attempt to modify // PeerIdentity was made after PeerConnection has been initialized. ErrModifyingPeerIdentity = errors.New("peerIdentity cannot be modified") // ErrModifyingCertificates indicates that an attempt to modify // Certificates was made after PeerConnection has been initialized. ErrModifyingCertificates = errors.New("certificates cannot be modified") // ErrModifyingBundlePolicy indicates that an attempt to modify // BundlePolicy was made after PeerConnection has been initialized. ErrModifyingBundlePolicy = errors.New("bundle policy cannot be modified") // ErrModifyingRTCPMuxPolicy indicates that an attempt to modify // RTCPMuxPolicy was made after PeerConnection has been initialized. ErrModifyingRTCPMuxPolicy = errors.New("rtcp mux policy cannot be modified") // ErrModifyingICECandidatePoolSize indicates that an attempt to modify // ICECandidatePoolSize was made after PeerConnection has been initialized. ErrModifyingICECandidatePoolSize = errors.New("ice candidate pool size cannot be modified") // ErrStringSizeLimit indicates that the character size limit of string is // exceeded. The limit is hardcoded to 65535 according to specifications. ErrStringSizeLimit = errors.New("data channel label exceeds size limit") // ErrMaxDataChannelID indicates that the maximum number ID that could be // specified for a data channel has been exceeded. ErrMaxDataChannelID = errors.New("maximum number ID for datachannel specified") // ErrNegotiatedWithoutID indicates that an attempt to create a data channel // was made while setting the negotiated option to true without providing // the negotiated channel ID. ErrNegotiatedWithoutID = errors.New("negotiated set without channel id") // ErrRetransmitsOrPacketLifeTime indicates that an attempt to create a data // channel was made with both options MaxPacketLifeTime and MaxRetransmits // set together. Such configuration is not supported by the specification // and is mutually exclusive. ErrRetransmitsOrPacketLifeTime = errors.New("both MaxPacketLifeTime and MaxRetransmits was set") // ErrCodecNotFound is returned when a codec search to the Media Engine fails ErrCodecNotFound = errors.New("codec not found") // ErrNoRemoteDescription indicates that an operation was rejected because // the remote description is not set ErrNoRemoteDescription = errors.New("remote description is not set") // ErrIncorrectSDPSemantics indicates that the PeerConnection was configured to // generate SDP Answers with different SDP Semantics than the received Offer ErrIncorrectSDPSemantics = errors.New("remote SessionDescription semantics does not match configuration") // ErrIncorrectSignalingState indicates that the signaling state of PeerConnection is not correct ErrIncorrectSignalingState = errors.New("operation can not be run in current signaling state") // ErrProtocolTooLarge indicates that value given for a DataChannelInit protocol is // longer then 65535 bytes ErrProtocolTooLarge = errors.New("protocol is larger then 65535 bytes") // ErrSenderNotCreatedByConnection indicates RemoveTrack was called with a RtpSender not created // by this PeerConnection ErrSenderNotCreatedByConnection = errors.New("RtpSender not created by this PeerConnection") // ErrSessionDescriptionNoFingerprint indicates SetRemoteDescription was called with a SessionDescription that has no // fingerprint ErrSessionDescriptionNoFingerprint = errors.New("SetRemoteDescription called with no fingerprint") // ErrSessionDescriptionInvalidFingerprint indicates SetRemoteDescription was called with a SessionDescription that // has an invalid fingerprint ErrSessionDescriptionInvalidFingerprint = errors.New("SetRemoteDescription called with an invalid fingerprint") // ErrSessionDescriptionConflictingFingerprints indicates SetRemoteDescription was called with a SessionDescription that // has an conflicting fingerprints ErrSessionDescriptionConflictingFingerprints = errors.New("SetRemoteDescription called with multiple conflicting fingerprint") // ErrSessionDescriptionMissingIceUfrag indicates SetRemoteDescription was called with a SessionDescription that // is missing an ice-ufrag value ErrSessionDescriptionMissingIceUfrag = errors.New("SetRemoteDescription called with no ice-ufrag") // ErrSessionDescriptionMissingIcePwd indicates SetRemoteDescription was called with a SessionDescription that // is missing an ice-pwd value ErrSessionDescriptionMissingIcePwd = errors.New("SetRemoteDescription called with no ice-pwd") // ErrSessionDescriptionConflictingIceUfrag indicates SetRemoteDescription was called with a SessionDescription that // contains multiple conflicting ice-ufrag values ErrSessionDescriptionConflictingIceUfrag = errors.New("SetRemoteDescription called with multiple conflicting ice-ufrag values") // ErrSessionDescriptionConflictingIcePwd indicates SetRemoteDescription was called with a SessionDescription that // contains multiple conflicting ice-pwd values ErrSessionDescriptionConflictingIcePwd = errors.New("SetRemoteDescription called with multiple conflicting ice-pwd values") // ErrNoSRTPProtectionProfile indicates that the DTLS handshake completed and no SRTP Protection Profile was chosen ErrNoSRTPProtectionProfile = errors.New("DTLS Handshake completed and no SRTP Protection Profile was chosen") // ErrFailedToGenerateCertificateFingerprint indicates that we failed to generate the fingerprint used for comparing certificates ErrFailedToGenerateCertificateFingerprint = errors.New("failed to generate certificate fingerprint") // ErrNoCodecsAvailable indicates that operation isn't possible because the MediaEngine has no codecs available ErrNoCodecsAvailable = errors.New("operation failed no codecs are available") // ErrUnsupportedCodec indicates the remote peer doesn't support the requested codec ErrUnsupportedCodec = errors.New("unable to start track, codec is not supported by remote") // ErrSenderWithNoCodecs indicates that a RTPSender was created without any codecs. To send media the MediaEngine needs at // least one configured codec. ErrSenderWithNoCodecs = errors.New("unable to populate media section, RTPSender created with no codecs") // ErrRTPSenderNewTrackHasIncorrectKind indicates that the new track is of a different kind than the previous/original ErrRTPSenderNewTrackHasIncorrectKind = errors.New("new track must be of the same kind as previous") // ErrRTPSenderNewTrackHasIncorrectEnvelope indicates that the new track has a different envelope than the previous/original ErrRTPSenderNewTrackHasIncorrectEnvelope = errors.New("new track must have the same envelope as previous") // ErrUnbindFailed indicates that a TrackLocal was not able to be unbind ErrUnbindFailed = errors.New("failed to unbind TrackLocal from PeerConnection") // ErrNoPayloaderForCodec indicates that the requested codec does not have a payloader ErrNoPayloaderForCodec = errors.New("the requested codec does not have a payloader") // ErrRegisterHeaderExtensionInvalidDirection indicates that a extension was registered with a direction besides `sendonly` or `recvonly` ErrRegisterHeaderExtensionInvalidDirection = errors.New("a header extension must be registered as 'recvonly', 'sendonly' or both") // ErrSimulcastProbeOverflow indicates that too many Simulcast probe streams are in flight and the requested SSRC was ignored ErrSimulcastProbeOverflow = errors.New("simulcast probe limit has been reached, new SSRC has been discarded") errDetachNotEnabled = errors.New("enable detaching by calling webrtc.DetachDataChannels()") errDetachBeforeOpened = errors.New("datachannel not opened yet, try calling Detach from OnOpen") errDtlsTransportNotStarted = errors.New("the DTLS transport has not started yet") errDtlsKeyExtractionFailed = errors.New("failed extracting keys from DTLS for SRTP") errFailedToStartSRTP = errors.New("failed to start SRTP") errFailedToStartSRTCP = errors.New("failed to start SRTCP") errInvalidDTLSStart = errors.New("attempted to start DTLSTransport that is not in new state") errNoRemoteCertificate = errors.New("peer didn't provide certificate via DTLS") errIdentityProviderNotImplemented = errors.New("identity provider is not implemented") errNoMatchingCertificateFingerprint = errors.New("remote certificate does not match any fingerprint") errICEConnectionNotStarted = errors.New("ICE connection not started") errICECandidateTypeUnknown = errors.New("unknown candidate type") errICEInvalidConvertCandidateType = errors.New("cannot convert ice.CandidateType into webrtc.ICECandidateType, invalid type") errICEAgentNotExist = errors.New("ICEAgent does not exist") errICECandiatesCoversionFailed = errors.New("unable to convert ICE candidates to ICECandidates") errICERoleUnknown = errors.New("unknown ICE Role") errICEProtocolUnknown = errors.New("unknown protocol") errICEGathererNotStarted = errors.New("gatherer not started") errNetworkTypeUnknown = errors.New("unknown network type") errSDPDoesNotMatchOffer = errors.New("new sdp does not match previous offer") errSDPDoesNotMatchAnswer = errors.New("new sdp does not match previous answer") errPeerConnSDPTypeInvalidValue = errors.New("provided value is not a valid enum value of type SDPType") errPeerConnStateChangeInvalid = errors.New("invalid state change op") errPeerConnStateChangeUnhandled = errors.New("unhandled state change op") errPeerConnSDPTypeInvalidValueSetLocalDescription = errors.New("invalid SDP type supplied to SetLocalDescription()") errPeerConnRemoteDescriptionWithoutMidValue = errors.New("remoteDescription contained media section without mid value") errPeerConnRemoteDescriptionNil = errors.New("remoteDescription has not been set yet") errPeerConnSingleMediaSectionHasExplicitSSRC = errors.New("single media section has an explicit SSRC") errPeerConnRemoteSSRCAddTransceiver = errors.New("could not add transceiver for remote SSRC") errPeerConnSimulcastMidRTPExtensionRequired = errors.New("mid RTP Extensions required for Simulcast") errPeerConnSimulcastStreamIDRTPExtensionRequired = errors.New("stream id RTP Extensions required for Simulcast") errPeerConnSimulcastIncomingSSRCFailed = errors.New("incoming SSRC failed Simulcast probing") errPeerConnAddTransceiverFromKindOnlyAcceptsOne = errors.New("AddTransceiverFromKind only accepts one RTPTransceiverInit") errPeerConnAddTransceiverFromTrackOnlyAcceptsOne = errors.New("AddTransceiverFromTrack only accepts one RTPTransceiverInit") errPeerConnAddTransceiverFromKindSupport = errors.New("AddTransceiverFromKind currently only supports recvonly") errPeerConnAddTransceiverFromTrackSupport = errors.New("AddTransceiverFromTrack currently only supports sendonly and sendrecv") errPeerConnSetIdentityProviderNotImplemented = errors.New("TODO SetIdentityProvider") errPeerConnWriteRTCPOpenWriteStream = errors.New("WriteRTCP failed to open WriteStream") errPeerConnTranscieverMidNil = errors.New("cannot find transceiver with mid") errRTPReceiverDTLSTransportNil = errors.New("DTLSTransport must not be nil") errRTPReceiverReceiveAlreadyCalled = errors.New("Receive has already been called") errRTPReceiverWithSSRCTrackStreamNotFound = errors.New("unable to find stream for Track with SSRC") errRTPReceiverForRIDTrackStreamNotFound = errors.New("no trackStreams found for RID") errRTPSenderTrackNil = errors.New("Track must not be nil") errRTPSenderDTLSTransportNil = errors.New("DTLSTransport must not be nil") errRTPSenderSendAlreadyCalled = errors.New("Send has already been called") errRTPSenderStopped = errors.New("Sender has already been stopped") errRTPSenderTrackRemoved = errors.New("Sender Track has been removed or replaced to nil") errRTPSenderRidNil = errors.New("Sender cannot add encoding as rid is empty") errRTPSenderNoBaseEncoding = errors.New("Sender cannot add encoding as there is no base track") errRTPSenderBaseEncodingMismatch = errors.New("Sender cannot add encoding as provided track does not match base track") errRTPSenderRIDCollision = errors.New("Sender cannot encoding due to RID collision") errRTPSenderNoTrackForRID = errors.New("Sender does not have track for RID") errRTPTransceiverCannotChangeMid = errors.New("errRTPSenderTrackNil") errRTPTransceiverSetSendingInvalidState = errors.New("invalid state change in RTPTransceiver.setSending") errRTPTransceiverCodecUnsupported = errors.New("unsupported codec type by this transceiver") errSCTPTransportDTLS = errors.New("DTLS not established") errSDPZeroTransceivers = errors.New("addTransceiverSDP() called with 0 transceivers") errSDPMediaSectionMediaDataChanInvalid = errors.New("invalid Media Section. Media + DataChannel both enabled") errSDPMediaSectionMultipleTrackInvalid = errors.New("invalid Media Section. Can not have multiple tracks in one MediaSection in UnifiedPlan") errSettingEngineSetAnsweringDTLSRole = errors.New("SetAnsweringDTLSRole must DTLSRoleClient or DTLSRoleServer") errSignalingStateCannotRollback = errors.New("can't rollback from stable state") errSignalingStateProposedTransitionInvalid = errors.New("invalid proposed signaling state transition") errStatsICECandidateStateInvalid = errors.New("cannot convert to StatsICECandidatePairStateSucceeded invalid ice candidate state") errInvalidICECredentialTypeString = errors.New("invalid ICECredentialType") errInvalidICEServer = errors.New("invalid ICEServer") errICETransportNotInNew = errors.New("ICETransport can only be called in ICETransportStateNew") errCertificatePEMFormatError = errors.New("bad Certificate PEM format") errRTPTooShort = errors.New("not long enough to be a RTP Packet") errExcessiveRetries = errors.New("excessive retries in CreateOffer") ) webrtc-3.1.56/examples/000077500000000000000000000000001437620512100147135ustar00rootroot00000000000000webrtc-3.1.56/examples/README.md000066400000000000000000000136721437620512100162030ustar00rootroot00000000000000

Examples

We've built an extensive collection of examples covering common use-cases. You can modify and extend these examples to get started quickly. For more full featured examples that use 3rd party libraries see our **[example-webrtc-applications](https://github.com/pion/example-webrtc-applications)** repo. ### Overview #### Media API * [Reflect](reflect): The reflect example demonstrates how to have Pion send back to the user exactly what it receives using the same PeerConnection. * [Play from Disk](play-from-disk): The play-from-disk example demonstrates how to send video to your browser from a file saved to disk. * [Play from Disk Renegotation](play-from-disk-renegotation): The play-from-disk-renegotation example is an extension of the play-from-disk example, but demonstrates how you can add/remove video tracks from an already negotiated PeerConnection. * [Insertable Streams](insertable-streams): The insertable-streams example demonstrates how Pion can be used to send E2E encrypted video and decrypt via insertable streams in the browser. * [Save to Disk](save-to-disk): The save-to-disk example shows how to record your webcam and save the footage to disk on the server side. * [Broadcast](broadcast): The broadcast example demonstrates how to broadcast a video to multiple peers. A broadcaster uploads the video once and the server forwards it to all other peers. * [RTP Forwarder](rtp-forwarder): The rtp-forwarder example demonstrates how to forward your audio/video streams using RTP. * [RTP to WebRTC](rtp-to-webrtc): The rtp-to-webrtc example demonstrates how to take RTP packets sent to a Pion process into your browser. * [Simulcast](simulcast): The simulcast example demonstrates how to accept and demux 1 Track that contains 3 Simulcast streams. It then returns the media as 3 independent Tracks back to the sender. * [Swap Tracks](swap-tracks): The swap-tracks example demonstrates deeper usage of the Pion Media API. The server accepts 3 media streams, and then dynamically routes them back as a single stream to the user. * [RTCP Processing](rtcp-processing) The rtcp-processing example demonstrates Pion's RTCP APIs. This allow access to media statistics and control information. #### Data Channel API * [Data Channels](data-channels): The data-channels example shows how you can send/recv DataChannel messages from a web browser. * [Data Channels Detach](data-channels-detach): The data-channels-detach example shows how you can send/recv DataChannel messages using the underlying DataChannel implementation directly. This provides a more idiomatic way of interacting with Data Channels. * [Data Channels Flow Control](data-channels-flow-control): Example data-channels-flow-control shows how to use the DataChannel API efficiently. You can measure the amount the rate at which the remote peer is receiving data, and structure your application accordingly. * [ORTC](ortc): Example ortc shows how you an use the ORTC API for DataChannel communication. * [Pion to Pion](pion-to-pion): Example pion-to-pion is an example of two pion instances communicating directly! It therefore has no corresponding web page. #### Miscellaneous * [Custom Logger](custom-logger) The custom-logger demonstrates how the user can override the logging and process messages instead of printing to stdout. It has no corresponding web page. * [ICE Restart](ice-restart) Example ice-restart demonstrates how a WebRTC connection can roam between networks. This example restarts ICE in a loop and prints the new addresses it uses each time. * [ICE Single Port](ice-single-port) Example ice-single-port demonstrates how multiple WebRTC connections can be served from a single port. By default Pion listens on a new port for every PeerConnection. Pion can be configured to use a single port for multiple connections. * [ICE TCP](ice-tcp) Example ice-tcp demonstrates how a WebRTC connection can be made over TCP instead of UDP. By default Pion only does UDP. Pion can be configured to use a TCP port, and this TCP port can be used for many connections. * [Trickle ICE](trickle-ice) Example trickle-ice example demonstrates Pion WebRTC's Trickle ICE APIs. This is important to use since it allows ICE Gathering and Connecting to happen concurrently. * [VNet](vnet) Example vnet demonstrates Pion's network virtualisation library. This example connects two PeerConnections over a virtual network and prints statistics about the data traveling over it. ### Usage We've made it easy to run the browser based examples on your local machine. 1. Build and run the example server: ``` sh GO111MODULE=on go get github.com/pion/webrtc/v3 git clone https://github.com/pion/webrtc.git $GOPATH/src/github.com/pion/webrtc cd $GOPATH/src/github.com/pion/webrtc/examples go run examples.go ``` 2. Browse to [localhost](http://localhost) to browse through the examples. Note that you can change the port of the server using the ``--address`` flag: ``` sh go run examples.go --address localhost:8080 go run examples.go --address :8080 # listen on all available interfaces ``` ### WebAssembly Pion WebRTC can be used when compiled to WebAssembly, also known as WASM. In this case the library will act as a wrapper around the JavaScript WebRTC API. This allows you to use WebRTC from Go in both server and browser side code with little to no changes Some of our examples have support for WebAssembly. The same examples server documented above can be used to run the WebAssembly examples. However, you have to compile them first. This is done as follows: 1. If the example supports WebAssembly it will contain a `main.go` file under the `jsfiddle` folder. 2. Build this `main.go` file as follows: ``` GOOS=js GOARCH=wasm go build -o demo.wasm ``` 3. Start the example server. Refer to the [usage](#usage) section for how you can build the example server. 4. Browse to [localhost](http://localhost). The page should now give you the option to run the example using the WebAssembly binary. webrtc-3.1.56/examples/bandwidth-estimation-from-disk/000077500000000000000000000000001437620512100227225ustar00rootroot00000000000000webrtc-3.1.56/examples/bandwidth-estimation-from-disk/README.md000066400000000000000000000040621437620512100242030ustar00rootroot00000000000000# bandwidth-estimation-from-disk bandwidth-estimation-from-disk demonstrates how to use Pion's Bandwidth Estimation APIs. Pion provides multiple Bandwidth Estimators, but they all satisfy one interface. This interface emits an int for how much bandwidth is available to send. It is then up to the sender to meet that number. ## Instructions ### Create IVF files named `high.ivf` `med.ivf` and `low.ivf` ``` ffmpeg -i $INPUT_FILE -g 30 -b:v .3M -s 320x240 low.ivf ffmpeg -i $INPUT_FILE -g 30 -b:v 1M -s 858x480 med.ivf ffmpeg -i $INPUT_FILE -g 30 -b:v 2.5M -s 1280x720 high.ivf ``` ### Download bandwidth-estimation-from-disk ``` go get github.com/pion/webrtc/v3/examples/bandwidth-estimation-from-disk ``` ### Open bandwidth-estimation-from-disk example page [jsfiddle.net](https://jsfiddle.net/a1cz42op/) you should see two text-areas, 'Start Session' button and 'Copy browser SessionDescription to clipboard' ### Run bandwidth-estimation-from-disk with your browsers Session Description as stdin The `output.ivf` you created should be in the same directory as `bandwidth-estimation-from-disk`. In the jsfiddle press 'Copy browser Session Description to clipboard' or copy the base64 string manually. Now use this value you just copied as the input to `bandwidth-estimation-from-disk` #### Linux/macOS Run `echo $BROWSER_SDP | bandwidth-estimation-from-disk` #### Windows 1. Paste the SessionDescription into a file. 1. Run `bandwidth-estimation-from-disk < my_file` ### Input bandwidth-estimation-from-disk's Session Description into your browser Copy the text that `bandwidth-estimation-from-disk` just emitted and copy into the second text area in the jsfiddle ### Hit 'Start Session' in jsfiddle, enjoy your video! A video should start playing in your browser above the input boxes. When `bandwidth-estimation-from-disk` switches quality levels it will print the old and new file like so. ``` Switching from low.ivf to med.ivf Switching from med.ivf to high.ivf Switching from high.ivf to med.ivf ``` Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/bandwidth-estimation-from-disk/main.go000066400000000000000000000163271437620512100242060ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "errors" "fmt" "io" "os" "time" "github.com/pion/interceptor" "github.com/pion/interceptor/pkg/cc" "github.com/pion/interceptor/pkg/gcc" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/ivfreader" ) const ( lowFile = "low.ivf" lowBitrate = 300_000 medFile = "med.ivf" medBitrate = 1_000_000 highFile = "high.ivf" highBitrate = 2_500_000 ivfHeaderSize = 32 ) func main() { qualityLevels := []struct { fileName string bitrate int }{ {lowFile, lowBitrate}, {medFile, medBitrate}, {highFile, highBitrate}, } currentQuality := 0 for _, level := range qualityLevels { _, err := os.Stat(level.fileName) if os.IsNotExist(err) { panic(fmt.Sprintf("File %s was not found", level.fileName)) } } i := &interceptor.Registry{} m := &webrtc.MediaEngine{} if err := m.RegisterDefaultCodecs(); err != nil { panic(err) } // Create a Congestion Controller. This analyzes inbound and outbound data and provides // suggestions on how much we should be sending. // // Passing `nil` means we use the default Estimation Algorithm which is Google Congestion Control. // You can use the other ones that Pion provides, or write your own! congestionController, err := cc.NewInterceptor(func() (cc.BandwidthEstimator, error) { return gcc.NewSendSideBWE(gcc.SendSideBWEInitialBitrate(lowBitrate)) }) if err != nil { panic(err) } estimatorChan := make(chan cc.BandwidthEstimator, 1) congestionController.OnNewPeerConnection(func(id string, estimator cc.BandwidthEstimator) { estimatorChan <- estimator }) i.Add(congestionController) if err = webrtc.ConfigureTWCCHeaderExtensionSender(m, i); err != nil { panic(err) } if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil { panic(err) } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewAPI(webrtc.WithInterceptorRegistry(i), webrtc.WithMediaEngine(m)).NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, }) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Wait until our Bandwidth Estimator has been created estimator := <-estimatorChan // Create a video track videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion") if err != nil { panic(err) } rtpSender, err := peerConnection.AddTrack(videoTrack) if err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Open a IVF file and start reading using our IVFReader file, err := os.Open(qualityLevels[currentQuality].fileName) if err != nil { panic(err) } ivf, header, err := ivfreader.NewWith(file) if err != nil { panic(err) } // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. // // It is important to use a time.Ticker instead of time.Sleep because // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)) frame := []byte{} frameHeader := &ivfreader.IVFFrameHeader{} currentTimestamp := uint64(0) switchQualityLevel := func(newQualityLevel int) { fmt.Printf("Switching from %s to %s \n", qualityLevels[currentQuality].fileName, qualityLevels[newQualityLevel].fileName) currentQuality = newQualityLevel ivf.ResetReader(setReaderFile(qualityLevels[currentQuality].fileName)) for { if frame, frameHeader, err = ivf.ParseNextFrame(); err != nil { break } else if frameHeader.Timestamp >= currentTimestamp && frame[0]&0x1 == 0 { break } } } for ; true; <-ticker.C { targetBitrate := estimator.GetTargetBitrate() switch { // If current quality level is below target bitrate drop to level below case currentQuality != 0 && targetBitrate < qualityLevels[currentQuality].bitrate: switchQualityLevel(currentQuality - 1) // If next quality level is above target bitrate move to next level case len(qualityLevels) > (currentQuality+1) && targetBitrate > qualityLevels[currentQuality+1].bitrate: switchQualityLevel(currentQuality + 1) // Adjust outbound bandwidth for probing default: frame, _, err = ivf.ParseNextFrame() } switch { // If we have reached the end of the file start again case errors.Is(err, io.EOF): ivf.ResetReader(setReaderFile(qualityLevels[currentQuality].fileName)) // No error write the video frame case err == nil: currentTimestamp = frameHeader.Timestamp if err = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil { panic(err) } // Error besides io.EOF that we dont know how to handle default: panic(err) } } } func setReaderFile(filename string) func(_ int64) io.Reader { return func(_ int64) io.Reader { file, err := os.Open(filename) // nolint if err != nil { panic(err) } if _, err = file.Seek(ivfHeaderSize, io.SeekStart); err != nil { panic(err) } return file } } webrtc-3.1.56/examples/broadcast/000077500000000000000000000000001437620512100166555ustar00rootroot00000000000000webrtc-3.1.56/examples/broadcast/README.md000066400000000000000000000036571437620512100201470ustar00rootroot00000000000000# broadcast broadcast is a Pion WebRTC application that demonstrates how to broadcast a video to many peers, while only requiring the broadcaster to upload once. This could serve as the building block to building conferencing software, and other applications where publishers are bandwidth constrained. ## Instructions ### Download broadcast ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/broadcast ``` ### Open broadcast example page [jsfiddle.net](https://jsfiddle.net/us4h58jx/) You should see two buttons `Publish a Broadcast` and `Join a Broadcast` ### Run Broadcast #### Linux/macOS Run `broadcast` OR run `main.go` in `github.com/pion/webrtc/examples/broadcast` ### Start a publisher * Click `Publish a Broadcast` * Press `Copy browser SDP to clipboard` or copy the `Browser base64 Session Description` string manually * Run `curl localhost:8080/sdp -d "$BROWSER_OFFER"`. `$BROWSER_OFFER` is the value you copied in the last step. * The `broadcast` terminal application will respond with an answer, paste this into the second input field in your browser. * Press `Start Session` * The connection state will be printed in the terminal and under `logs` in the browser. ### Join the broadcast * Click `Join a Broadcast` * Copy the string in the first input labelled `Browser base64 Session Description` * Run `curl localhost:8080/sdp -d "$BROWSER_OFFER"`. `$BROWSER_OFFER` is the value you copied in the last step. * The `broadcast` terminal application will respond with an answer, paste this into the second input field in your browser. * Press `Start Session` * The connection state will be printed in the terminal and under `logs` in the browser. You can change the listening port using `-port 8011` You can `Join the broadcast` as many times as you want. The `broadcast` Golang application is relaying all traffic, so your browser only has to upload once. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/broadcast/jsfiddle/000077500000000000000000000000001437620512100204415ustar00rootroot00000000000000webrtc-3.1.56/examples/broadcast/jsfiddle/demo.css000066400000000000000000000000641437620512100220770ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/broadcast/jsfiddle/demo.details000066400000000000000000000001471437620512100227360ustar00rootroot00000000000000--- name: broadcast description: Example of a broadcast using Pion WebRTC authors: - Sean DuBois webrtc-3.1.56/examples/broadcast/jsfiddle/demo.html000066400000000000000000000014471437620512100222610ustar00rootroot00000000000000
Video



Logs
webrtc-3.1.56/examples/broadcast/jsfiddle/demo.js000066400000000000000000000041011437620512100217170ustar00rootroot00000000000000/* eslint-env browser */ const log = msg => { document.getElementById('logs').innerHTML += msg + '
' } window.createSession = isPublisher => { const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }) pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } if (isPublisher) { navigator.mediaDevices.getUserMedia({ video: true, audio: false }) .then(stream => { stream.getTracks().forEach(track => pc.addTrack(track, stream)) document.getElementById('video1').srcObject = stream pc.createOffer() .then(d => pc.setLocalDescription(d)) .catch(log) }).catch(log) } else { pc.addTransceiver('video') pc.createOffer() .then(d => pc.setLocalDescription(d)) .catch(log) pc.ontrack = function (event) { const el = document.getElementById('video1') el.srcObject = event.streams[0] el.autoplay = true el.controls = true } } window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySDP = () => { const browserSDP = document.getElementById('localSessionDescription') browserSDP.focus() browserSDP.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' log('Copying SDP was ' + msg) } catch (err) { log('Unable to copy SDP ' + err) } } const btns = document.getElementsByClassName('createSessionButton') for (let i = 0; i < btns.length; i++) { btns[i].style = 'display: none' } document.getElementById('signalingContainer').style = 'display: block' } webrtc-3.1.56/examples/broadcast/main.go000066400000000000000000000116141437620512100201330ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "errors" "fmt" "io" "time" "github.com/pion/rtcp" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) const ( rtcpPLIInterval = time.Second * 3 ) func main() { // nolint:gocognit sdpChan := signal.HTTPSDPServer() // Everything below is the Pion WebRTC API, thanks for using it ❤️. offer := webrtc.SessionDescription{} signal.Decode(<-sdpChan, &offer) fmt.Println("") peerConnectionConfig := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfig) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Allow us to receive 1 video track if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil { panic(err) } localTrackChan := make(chan *webrtc.TrackLocalStaticRTP) // Set a handler for when a new remote track starts, this just distributes all our packets // to connected peers peerConnection.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval // This can be less wasteful by processing incoming RTCP events, then we would emit a NACK/PLI when a viewer requests it go func() { ticker := time.NewTicker(rtcpPLIInterval) for range ticker.C { if rtcpSendErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remoteTrack.SSRC())}}); rtcpSendErr != nil { fmt.Println(rtcpSendErr) } } }() // Create a local track, all our SFU clients will be fed via this track localTrack, newTrackErr := webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, "video", "pion") if newTrackErr != nil { panic(newTrackErr) } localTrackChan <- localTrack rtpBuf := make([]byte, 1400) for { i, _, readErr := remoteTrack.Read(rtpBuf) if readErr != nil { panic(readErr) } // ErrClosedPipe means we don't have any subscribers, this is ok if no peers have connected yet if _, err = localTrack.Write(rtpBuf[:i]); err != nil && !errors.Is(err, io.ErrClosedPipe) { panic(err) } } }) // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(offer) if err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Get the LocalDescription and take it to base64 so we can paste in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) localTrack := <-localTrackChan for { fmt.Println("") fmt.Println("Curl an base64 SDP to start sendonly peer connection") recvOnlyOffer := webrtc.SessionDescription{} signal.Decode(<-sdpChan, &recvOnlyOffer) // Create a new PeerConnection peerConnection, err := webrtc.NewPeerConnection(peerConnectionConfig) if err != nil { panic(err) } rtpSender, err := peerConnection.AddTrack(localTrack) if err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(recvOnlyOffer) if err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete = webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Get the LocalDescription and take it to base64 so we can paste in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) } } webrtc-3.1.56/examples/custom-logger/000077500000000000000000000000001437620512100175025ustar00rootroot00000000000000webrtc-3.1.56/examples/custom-logger/README.md000066400000000000000000000007441437620512100207660ustar00rootroot00000000000000# custom-logger custom-logger is an example of how the Pion API provides an customizable logging API. By default all Pion projects log to stdout, but we also allow users to override this and process messages however they want. ## Instructions ### Download custom-logger ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/custom-logger ``` ### Run custom-logger `custom-logger` You should see messages from our customLogger, as two PeerConnections start a session webrtc-3.1.56/examples/custom-logger/main.go000066400000000000000000000132541437620512100207620ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "fmt" "os" "github.com/pion/logging" "github.com/pion/webrtc/v3" ) // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // customLogger satisfies the interface logging.LeveledLogger // a logger is created per subsystem in Pion, so you can have custom // behavior per subsystem (ICE, DTLS, SCTP...) type customLogger struct{} // Print all messages except trace func (c customLogger) Trace(msg string) {} func (c customLogger) Tracef(format string, args ...interface{}) {} func (c customLogger) Debug(msg string) { fmt.Printf("customLogger Debug: %s\n", msg) } func (c customLogger) Debugf(format string, args ...interface{}) { c.Debug(fmt.Sprintf(format, args...)) } func (c customLogger) Info(msg string) { fmt.Printf("customLogger Info: %s\n", msg) } func (c customLogger) Infof(format string, args ...interface{}) { c.Trace(fmt.Sprintf(format, args...)) } func (c customLogger) Warn(msg string) { fmt.Printf("customLogger Warn: %s\n", msg) } func (c customLogger) Warnf(format string, args ...interface{}) { c.Warn(fmt.Sprintf(format, args...)) } func (c customLogger) Error(msg string) { fmt.Printf("customLogger Error: %s\n", msg) } func (c customLogger) Errorf(format string, args ...interface{}) { c.Error(fmt.Sprintf(format, args...)) } // customLoggerFactory satisfies the interface logging.LoggerFactory // This allows us to create different loggers per subsystem. So we can // add custom behavior type customLoggerFactory struct{} func (c customLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { fmt.Printf("Creating logger for %s \n", subsystem) return customLogger{} } func main() { // Create a new API with a custom logger // This SettingEngine allows non-standard WebRTC behavior s := webrtc.SettingEngine{ LoggerFactory: customLoggerFactory{}, } api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) // Create a new RTCPeerConnection offerPeerConnection, err := api.NewPeerConnection(webrtc.Configuration{}) if err != nil { panic(err) } defer func() { if cErr := offerPeerConnection.Close(); cErr != nil { fmt.Printf("cannot close offerPeerConnection: %v\n", cErr) } }() // We need a DataChannel so we can have ICE Candidates if _, err = offerPeerConnection.CreateDataChannel("custom-logger", nil); err != nil { panic(err) } // Create a new RTCPeerConnection answerPeerConnection, err := api.NewPeerConnection(webrtc.Configuration{}) if err != nil { panic(err) } defer func() { if cErr := answerPeerConnection.Close(); cErr != nil { fmt.Printf("cannot close answerPeerConnection: %v\n", cErr) } }() // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected offerPeerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s (offerer)\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected answerPeerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s (answerer)\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate // send it to the other peer answerPeerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { if i != nil { if iceErr := offerPeerConnection.AddICECandidate(i.ToJSON()); iceErr != nil { panic(iceErr) } } }) // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate // send it to the other peer offerPeerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { if i != nil { if iceErr := answerPeerConnection.AddICECandidate(i.ToJSON()); iceErr != nil { panic(iceErr) } } }) // Create an offer for the other PeerConnection offer, err := offerPeerConnection.CreateOffer(nil) if err != nil { panic(err) } // SetLocalDescription, needed before remote gets offer if err = offerPeerConnection.SetLocalDescription(offer); err != nil { panic(err) } // Take offer from remote, answerPeerConnection is now able to contact // the other PeerConnection if err = answerPeerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create an Answer to send back to our originating PeerConnection answer, err := answerPeerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Set the answerer's LocalDescription if err = answerPeerConnection.SetLocalDescription(answer); err != nil { panic(err) } // SetRemoteDescription on original PeerConnection, this finishes our signaling // bother PeerConnections should be able to communicate with each other now if err = offerPeerConnection.SetRemoteDescription(answer); err != nil { panic(err) } // Block forever select {} } webrtc-3.1.56/examples/data-channels-detach/000077500000000000000000000000001437620512100206435ustar00rootroot00000000000000webrtc-3.1.56/examples/data-channels-detach/README.md000066400000000000000000000013421437620512100221220ustar00rootroot00000000000000# data-channels-detach data-channels-detach is an example that shows how you can detach a data channel. This allows direct access the the underlying [pion/datachannel](https://github.com/pion/datachannel). This allows you to interact with the data channel using a more idiomatic API based on the `io.ReadWriteCloser` interface. The example mirrors the data-channels example. ## Install ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/data-channels-detach ``` ## Usage The example can be used in the same way as the data-channel example or can be paired with the data-channels-detach-create example. In the latter case; run both example and exchange the offer/answer text by copy-pasting them on the other terminal. webrtc-3.1.56/examples/data-channels-detach/jsfiddle/000077500000000000000000000000001437620512100224275ustar00rootroot00000000000000webrtc-3.1.56/examples/data-channels-detach/jsfiddle/demo.css000066400000000000000000000000641437620512100240650ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/data-channels-detach/jsfiddle/demo.html000066400000000000000000000007541437620512100242470ustar00rootroot00000000000000Browser base64 Session Description

Golang base64 Session Description




Logs
webrtc-3.1.56/examples/data-channels-detach/jsfiddle/main.go000066400000000000000000000106011437620512100237000ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package main import ( "fmt" "io" "syscall/js" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) const messageSize = 15 func main() { // Since this behavior diverges from the WebRTC API it has to be // enabled using a settings engine. Mixing both detached and the // OnMessage DataChannel API is not supported. // Create a SettingEngine and enable Detach s := webrtc.SettingEngine{} s.DetachDataChannels() // Create an API object with the engine api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection using the API object peerConnection, err := api.NewPeerConnection(config) if err != nil { handleError(err) } // Create a datachannel with label 'data' dataChannel, err := peerConnection.CreateDataChannel("data", nil) if err != nil { handleError(err) } // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { log(fmt.Sprintf("ICE Connection State has changed: %s\n", connectionState.String())) }) // Register channel opening handling dataChannel.OnOpen(func() { log(fmt.Sprintf("Data channel '%s'-'%d' open.\n", dataChannel.Label(), dataChannel.ID())) // Detach the data channel raw, dErr := dataChannel.Detach() if dErr != nil { handleError(dErr) } // Handle reading from the data channel go ReadLoop(raw) // Handle writing to the data channel go WriteLoop(raw) }) // Create an offer to send to the browser offer, err := peerConnection.CreateOffer(nil) if err != nil { handleError(err) } // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(offer) if err != nil { handleError(err) } // Add handlers for setting up the connection. peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { log(fmt.Sprint(state)) }) peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) { if candidate != nil { encodedDescr := signal.Encode(peerConnection.LocalDescription()) el := getElementByID("localSessionDescription") el.Set("value", encodedDescr) } }) // Set up global callbacks which will be triggered on button clicks. /*js.Global().Set("sendMessage", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} { go func() { el := getElementByID("message") message := el.Get("value").String() if message == "" { js.Global().Call("alert", "Message must not be empty") return } if err := sendChannel.SendText(message); err != nil { handleError(err) } }() return js.Undefined() }))*/ js.Global().Set("startSession", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} { go func() { el := getElementByID("remoteSessionDescription") sd := el.Get("value").String() if sd == "" { js.Global().Call("alert", "Session Description must not be empty") return } descr := webrtc.SessionDescription{} signal.Decode(sd, &descr) if err := peerConnection.SetRemoteDescription(descr); err != nil { handleError(err) } }() return js.Undefined() })) // Block forever select {} } // ReadLoop shows how to read from the datachannel directly func ReadLoop(d io.Reader) { for { buffer := make([]byte, messageSize) n, err := d.Read(buffer) if err != nil { log(fmt.Sprintf("Datachannel closed; Exit the readloop: %v", err)) return } log(fmt.Sprintf("Message from DataChannel: %s\n", string(buffer[:n]))) } } // WriteLoop shows how to write to the datachannel directly func WriteLoop(d io.Writer) { for range time.NewTicker(5 * time.Second).C { message := signal.RandSeq(messageSize) log(fmt.Sprintf("Sending %s \n", message)) _, err := d.Write([]byte(message)) if err != nil { handleError(err) } } } func log(msg string) { el := getElementByID("logs") el.Set("innerHTML", el.Get("innerHTML").String()+msg+"
") } func handleError(err error) { log("Unexpected error. Check console.") panic(err) } func getElementByID(id string) js.Value { return js.Global().Get("document").Call("getElementById", id) } webrtc-3.1.56/examples/data-channels-detach/main.go000066400000000000000000000075571437620512100221340ustar00rootroot00000000000000package main import ( "fmt" "io" "os" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) const messageSize = 15 func main() { // Since this behavior diverges from the WebRTC API it has to be // enabled using a settings engine. Mixing both detached and the // OnMessage DataChannel API is not supported. // Create a SettingEngine and enable Detach s := webrtc.SettingEngine{} s.DetachDataChannels() // Create an API object with the engine api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection using the API object peerConnection, err := api.NewPeerConnection(config) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Register data channel creation handling peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID()) // Register channel opening handling d.OnOpen(func() { fmt.Printf("Data channel '%s'-'%d' open.\n", d.Label(), d.ID()) // Detach the data channel raw, dErr := d.Detach() if dErr != nil { panic(dErr) } // Handle reading from the data channel go ReadLoop(raw) // Handle writing to the data channel go WriteLoop(raw) }) }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(offer) if err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } // ReadLoop shows how to read from the datachannel directly func ReadLoop(d io.Reader) { for { buffer := make([]byte, messageSize) n, err := d.Read(buffer) if err != nil { fmt.Println("Datachannel closed; Exit the readloop:", err) return } fmt.Printf("Message from DataChannel: %s\n", string(buffer[:n])) } } // WriteLoop shows how to write to the datachannel directly func WriteLoop(d io.Writer) { for range time.NewTicker(5 * time.Second).C { message := signal.RandSeq(messageSize) fmt.Printf("Sending %s \n", message) _, err := d.Write([]byte(message)) if err != nil { panic(err) } } } webrtc-3.1.56/examples/data-channels-flow-control/000077500000000000000000000000001437620512100220405ustar00rootroot00000000000000webrtc-3.1.56/examples/data-channels-flow-control/README.md000066400000000000000000000052641437620512100233260ustar00rootroot00000000000000# data-channels-flow-control This example demonstrates how to use the following property / methods. * func (d *DataChannel) BufferedAmount() uint64 * func (d *DataChannel) SetBufferedAmountLowThreshold(th uint64) * func (d *DataChannel) BufferedAmountLowThreshold() uint64 * func (d *DataChannel) OnBufferedAmountLow(f func()) These methods are equivalent to that of JavaScript WebRTC API. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel for more details. ## When do we need it? Send or SendText methods are called on DataChannel to send data to the connected peer. The methods return immediately, but it does not mean the data was actually sent onto the wire. Instead, it is queued in a buffer until it actually gets sent out to the wire. When you have a large amount of data to send, it is an application's responsibility to control the buffered amount in order not to indefinitely grow the buffer size to eventually exhaust the memory. The rate you wish to send data might be much higher than the rate the data channel can actually send to the peer over the Internet. The above properties/methods help your application to pace the amount of data to be pushed into the data channel. ## How to run the example code The demo code (main.go) implements two endpoints (offerPC and answerPC) in it. ``` signaling messages +----------------------------------------+ | | v v +---------------+ +---------------+ | | data | | | offerPC |----------------------->| answerPC | |:PeerConnection| |:PeerConnection| +---------------+ +---------------+ ``` First offerPC and answerPC will exchange signaling message to establish a peer-to-peer connection, and data channel (label: "data"). Once the data channel is successfully opened, offerPC will start sending a series of 1024-byte packets to answerPC as fast as it can, until you kill the process by Ctrl-c. Here's how to run the code. At the root of the example, `pion/webrtc/examples/data-channels-flow-control/`: ``` $ go run main.go 2019/08/31 14:56:41 OnOpen: data-824635025728. Start sending a series of 1024-byte packets as fast as it can 2019/08/31 14:56:41 OnOpen: data-824637171120. Start receiving data 2019/08/31 14:56:42 Throughput: 179.118 Mbps 2019/08/31 14:56:43 Throughput: 203.545 Mbps 2019/08/31 14:56:44 Throughput: 211.516 Mbps 2019/08/31 14:56:45 Throughput: 216.292 Mbps 2019/08/31 14:56:46 Throughput: 217.961 Mbps 2019/08/31 14:56:47 Throughput: 218.342 Mbps : ``` webrtc-3.1.56/examples/data-channels-flow-control/main.go000066400000000000000000000124631437620512100233210ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "log" "os" "sync/atomic" "time" "github.com/pion/webrtc/v3" ) const ( bufferedAmountLowThreshold uint64 = 512 * 1024 // 512 KB maxBufferedAmount uint64 = 1024 * 1024 // 1 MB ) func check(err error) { if err != nil { panic(err) } } func setRemoteDescription(pc *webrtc.PeerConnection, sdp []byte) { var desc webrtc.SessionDescription err := json.Unmarshal(sdp, &desc) check(err) // Apply the desc as the remote description err = pc.SetRemoteDescription(desc) check(err) } func createOfferer() *webrtc.PeerConnection { // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{}, } // Create a new PeerConnection pc, err := webrtc.NewPeerConnection(config) check(err) buf := make([]byte, 1024) ordered := false maxRetransmits := uint16(0) options := &webrtc.DataChannelInit{ Ordered: &ordered, MaxRetransmits: &maxRetransmits, } sendMoreCh := make(chan struct{}) // Create a datachannel with label 'data' dc, err := pc.CreateDataChannel("data", options) check(err) // Register channel opening handling dc.OnOpen(func() { log.Printf("OnOpen: %s-%d. Start sending a series of 1024-byte packets as fast as it can\n", dc.Label(), dc.ID()) for { err2 := dc.Send(buf) check(err2) if dc.BufferedAmount()+uint64(len(buf)) > maxBufferedAmount { // Wait until the bufferedAmount becomes lower than the threshold <-sendMoreCh } } }) // Set bufferedAmountLowThreshold so that we can get notified when // we can send more dc.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold) // This callback is made when the current bufferedAmount becomes lower than the threshold dc.OnBufferedAmountLow(func() { sendMoreCh <- struct{}{} }) return pc } func createAnswerer() *webrtc.PeerConnection { // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{}, } // Create a new PeerConnection pc, err := webrtc.NewPeerConnection(config) check(err) pc.OnDataChannel(func(dc *webrtc.DataChannel) { var totalBytesReceived uint64 // Register channel opening handling dc.OnOpen(func() { log.Printf("OnOpen: %s-%d. Start receiving data", dc.Label(), dc.ID()) since := time.Now() // Start printing out the observed throughput for range time.NewTicker(1000 * time.Millisecond).C { bps := float64(atomic.LoadUint64(&totalBytesReceived)*8) / time.Since(since).Seconds() log.Printf("Throughput: %.03f Mbps", bps/1024/1024) } }) // Register the OnMessage to handle incoming messages dc.OnMessage(func(dcMsg webrtc.DataChannelMessage) { n := len(dcMsg.Data) atomic.AddUint64(&totalBytesReceived, uint64(n)) }) }) return pc } func main() { offerPC := createOfferer() defer func() { if err := offerPC.Close(); err != nil { fmt.Printf("cannot close offerPC: %v\n", err) } }() answerPC := createAnswerer() defer func() { if err := answerPC.Close(); err != nil { fmt.Printf("cannot close answerPC: %v\n", err) } }() // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate // send it to the other peer answerPC.OnICECandidate(func(i *webrtc.ICECandidate) { if i != nil { check(offerPC.AddICECandidate(i.ToJSON())) } }) // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate // send it to the other peer offerPC.OnICECandidate(func(i *webrtc.ICECandidate) { if i != nil { check(answerPC.AddICECandidate(i.ToJSON())) } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected offerPC.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s (offerer)\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected answerPC.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s (answerer)\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Now, create an offer offer, err := offerPC.CreateOffer(nil) check(err) check(offerPC.SetLocalDescription(offer)) desc, err := json.Marshal(offer) check(err) setRemoteDescription(answerPC, desc) answer, err := answerPC.CreateAnswer(nil) check(err) check(answerPC.SetLocalDescription(answer)) desc2, err := json.Marshal(answer) check(err) setRemoteDescription(offerPC, desc2) // Block forever select {} } webrtc-3.1.56/examples/data-channels/000077500000000000000000000000001437620512100174155ustar00rootroot00000000000000webrtc-3.1.56/examples/data-channels/README.md000066400000000000000000000046571437620512100207100ustar00rootroot00000000000000# data-channels data-channels is a Pion WebRTC application that shows how you can send/recv DataChannel messages from a web browser ## Instructions ### Download data-channels ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/data-channels ``` ### Open data-channels example page [jsfiddle.net](https://jsfiddle.net/e41tgovp/) ### Run data-channels, with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser's session description, press `Copy browser SDP to clipboard` or copy the base64 string manually and: #### Linux/macOS Run `echo $BROWSER_SDP | data-channels` #### Windows 1. Paste the SessionDescription into a file. 1. Run `data-channels < my_file` ### Input data-channels's SessionDescription into your browser Copy the text that `data-channels` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle Under Start Session you should see 'Checking' as it starts connecting. If everything worked you should see `New DataChannel foo 1` Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal! Pion WebRTC will send random messages every 5 seconds that will appear in your browser. Congrats, you have used Pion WebRTC! Now start building something cool ## Architecture ```mermaid flowchart TB Browser--Copy Offer from TextArea-->Pion Pion--Copy Text Print to Console-->Browser subgraph Pion[Go Peer] p1[Create PeerConnection] p2[OnConnectionState Handler] p3[Print Connection State] p2-->p3 p4[OnDataChannel Handler] p5[OnDataChannel Open] p6[Send Random Message every 5 seconds to DataChannel] p4-->p5-->p6 p7[OnDataChannel Message] p8[Log Incoming Message to Console] p4-->p7-->p8 p9[Read Session Description from Standard Input] p10[SetRemoteDescription with Session Description from Standard Input] p11[Create Answer] p12[Block until ICE Gathering is Complete] p13[Print Answer with ICE Candidatens included to Standard Output] end subgraph Browser[Browser Peer] b1[Create PeerConnection] b2[Create DataChannel 'foo'] b3[OnDataChannel Message] b4[Log Incoming Message to Console] b3-->b4 b5[Create Offer] b6[SetLocalDescription with Offer] b7[Print Offer with ICE Candidates included] end ``` webrtc-3.1.56/examples/data-channels/jsfiddle/000077500000000000000000000000001437620512100212015ustar00rootroot00000000000000webrtc-3.1.56/examples/data-channels/jsfiddle/demo.css000066400000000000000000000000641437620512100226370ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/data-channels/jsfiddle/demo.details000066400000000000000000000002411437620512100234710ustar00rootroot00000000000000--- name: data-channels description: Example of using Pion WebRTC to communicate with a web browser using bi-direction DataChannels authors: - Sean DuBois webrtc-3.1.56/examples/data-channels/jsfiddle/demo.html000066400000000000000000000011001437620512100230030ustar00rootroot00000000000000Browser base64 Session Description



Golang base64 Session Description



Message



Logs
webrtc-3.1.56/examples/data-channels/jsfiddle/demo.js000066400000000000000000000032111437620512100224600ustar00rootroot00000000000000/* eslint-env browser */ const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }) const log = msg => { document.getElementById('logs').innerHTML += msg + '
' } const sendChannel = pc.createDataChannel('foo') sendChannel.onclose = () => console.log('sendChannel has closed') sendChannel.onopen = () => console.log('sendChannel has opened') sendChannel.onmessage = e => log(`Message from DataChannel '${sendChannel.label}' payload '${e.data}'`) pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } pc.onnegotiationneeded = e => pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) window.sendMessage = () => { const message = document.getElementById('message').value if (message === '') { return alert('Message must not be empty') } sendChannel.send(message) } window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySDP = () => { const browserSDP = document.getElementById('localSessionDescription') browserSDP.focus() browserSDP.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' log('Copying SDP was ' + msg) } catch (err) { log('Unable to copy SDP ' + err) } } webrtc-3.1.56/examples/data-channels/jsfiddle/main.go000066400000000000000000000067121437620512100224620ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package main import ( "fmt" "syscall/js" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func main() { // Configure and create a new PeerConnection. config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } pc, err := webrtc.NewPeerConnection(config) if err != nil { handleError(err) } // Create DataChannel. sendChannel, err := pc.CreateDataChannel("foo", nil) if err != nil { handleError(err) } sendChannel.OnClose(func() { fmt.Println("sendChannel has closed") }) sendChannel.OnOpen(func() { fmt.Println("sendChannel has opened") candidatePair, err := pc.SCTP().Transport().ICETransport().GetSelectedCandidatePair() fmt.Println(candidatePair) fmt.Println(err) }) sendChannel.OnMessage(func(msg webrtc.DataChannelMessage) { log(fmt.Sprintf("Message from DataChannel %s payload %s", sendChannel.Label(), string(msg.Data))) }) // Create offer offer, err := pc.CreateOffer(nil) if err != nil { handleError(err) } if err := pc.SetLocalDescription(offer); err != nil { handleError(err) } // Add handlers for setting up the connection. pc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { log(fmt.Sprint(state)) }) pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { if candidate != nil { encodedDescr := signal.Encode(pc.LocalDescription()) el := getElementByID("localSessionDescription") el.Set("value", encodedDescr) } }) // Set up global callbacks which will be triggered on button clicks. js.Global().Set("sendMessage", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} { go func() { el := getElementByID("message") message := el.Get("value").String() if message == "" { js.Global().Call("alert", "Message must not be empty") return } if err := sendChannel.SendText(message); err != nil { handleError(err) } }() return js.Undefined() })) js.Global().Set("startSession", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} { go func() { el := getElementByID("remoteSessionDescription") sd := el.Get("value").String() if sd == "" { js.Global().Call("alert", "Session Description must not be empty") return } descr := webrtc.SessionDescription{} signal.Decode(sd, &descr) if err := pc.SetRemoteDescription(descr); err != nil { handleError(err) } }() return js.Undefined() })) js.Global().Set("copySDP", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} { go func() { defer func() { if e := recover(); e != nil { switch e := e.(type) { case error: handleError(e) default: handleError(fmt.Errorf("recovered with non-error value: (%T) %s", e, e)) } } }() browserSDP := getElementByID("localSessionDescription") browserSDP.Call("focus") browserSDP.Call("select") copyStatus := js.Global().Get("document").Call("execCommand", "copy") if copyStatus.Bool() { log("Copying SDP was successful") } else { log("Copying SDP was unsuccessful") } }() return js.Undefined() })) // Stay alive select {} } func log(msg string) { el := getElementByID("logs") el.Set("innerHTML", el.Get("innerHTML").String()+msg+"
") } func handleError(err error) { log("Unexpected error. Check console.") panic(err) } func getElementByID(id string) js.Value { return js.Global().Get("document").Call("getElementById", id) } webrtc-3.1.56/examples/data-channels/main.go000066400000000000000000000061631437620512100206760ustar00rootroot00000000000000package main import ( "fmt" "os" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func main() { // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewPeerConnection(config) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Register data channel creation handling peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID()) // Register channel opening handling d.OnOpen(func() { fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", d.Label(), d.ID()) for range time.NewTicker(5 * time.Second).C { message := signal.RandSeq(15) fmt.Printf("Sending '%s'\n", message) // Send the message as text sendErr := d.SendText(message) if sendErr != nil { panic(sendErr) } } }) // Register text message handling d.OnMessage(func(msg webrtc.DataChannelMessage) { fmt.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data)) }) }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(offer) if err != nil { panic(err) } // Create an answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/example.html000066400000000000000000000016211437620512100172340ustar00rootroot00000000000000 {{ .Title }} | Pion

{{ .Title }}

< Home

{{ template "demo.html" }}
{{ if .JS }} {{ else }} {{ end }} webrtc-3.1.56/examples/examples.go000066400000000000000000000065361437620512100170720ustar00rootroot00000000000000package main import ( "encoding/json" "errors" "flag" "fmt" "go/build" "html/template" "log" "net/http" "os" "path/filepath" "strings" ) // Examples represents the examples loaded from examples.json. type Examples []*Example var ( errListExamples = errors.New("failed to list examples (please run in the examples folder)") errParseExamples = errors.New("failed to parse examples") ) // Example represents an example loaded from examples.json. type Example struct { Title string `json:"title"` Link string `json:"link"` Description string `json:"description"` Type string `json:"type"` IsJS bool IsWASM bool } func main() { addr := flag.String("address", ":80", "Address to host the HTTP server on.") flag.Parse() log.Println("Listening on", *addr) err := serve(*addr) if err != nil { log.Fatalf("Failed to serve: %v", err) } } func serve(addr string) error { // Load the examples examples, err := getExamples() if err != nil { return err } // Load the templates homeTemplate := template.Must(template.ParseFiles("index.html")) // Serve the required pages // DIY 'mux' to avoid additional dependencies http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { url := r.URL.Path if url == "/wasm_exec.js" { http.FileServer(http.Dir(filepath.Join(build.Default.GOROOT, "misc/wasm/"))).ServeHTTP(w, r) return } // Split up the URL. Expected parts: // 1: Base url // 2: "example" // 3: Example type: js or wasm // 4: Example folder, e.g.: data-channels // 5: Static file as part of the example parts := strings.Split(url, "/") if len(parts) > 4 && parts[1] == "example" { exampleType := parts[2] exampleLink := parts[3] for _, example := range *examples { if example.Link != exampleLink { continue } fiddle := filepath.Join(exampleLink, "jsfiddle") if len(parts[4]) != 0 { http.StripPrefix("/example/"+exampleType+"/"+exampleLink+"/", http.FileServer(http.Dir(fiddle))).ServeHTTP(w, r) return } temp := template.Must(template.ParseFiles("example.html")) _, err = temp.ParseFiles(filepath.Join(fiddle, "demo.html")) if err != nil { panic(err) } data := struct { *Example JS bool }{ example, exampleType == "js", } err = temp.Execute(w, data) if err != nil { panic(err) } return } } // Serve the main page err = homeTemplate.Execute(w, examples) if err != nil { panic(err) } }) // Start the server return http.ListenAndServe(addr, nil) } // getExamples loads the examples from the examples.json file. func getExamples() (*Examples, error) { file, err := os.Open("./examples.json") if err != nil { return nil, fmt.Errorf("%w: %v", errListExamples, err) } defer func() { closeErr := file.Close() if closeErr != nil { panic(closeErr) } }() var examples Examples err = json.NewDecoder(file).Decode(&examples) if err != nil { return nil, fmt.Errorf("%w: %v", errParseExamples, err) } for _, example := range examples { fiddle := filepath.Join(example.Link, "jsfiddle") js := filepath.Join(fiddle, "demo.js") if _, err := os.Stat(js); !os.IsNotExist(err) { example.IsJS = true } wasm := filepath.Join(fiddle, "demo.wasm") if _, err := os.Stat(wasm); !os.IsNotExist(err) { example.IsWASM = true } } return &examples, nil } webrtc-3.1.56/examples/examples.json000066400000000000000000000120211437620512100174200ustar00rootroot00000000000000[ { "title": "Data Channels", "link": "data-channels", "description": "The data-channels example shows how you can send/recv DataChannel messages from a web browser.", "type": "browser" }, { "title": "Data Channels Detach", "link": "data-channels-detach", "description": "The data-channels-detach is an example that shows how you can detach a data channel.", "type": "browser" }, { "title": "Data Channels Flow Control", "link": "data-channels-flow-control", "description": "The data-channels-detach data-channels-flow-control shows how to use the DataChannel API efficiently. You can measure the amount the rate at which the remote peer is receiving data, and structure your application accordingly", "type": "browser" }, { "title": "Reflect", "link": "reflect", "description": "The reflect example demonstrates how to have Pion send back to the user exactly what it receives using the same PeerConnection.", "type": "browser" }, { "title": "Pion to Pion", "link": "#", "description": "Example pion-to-pion is an example of two pion instances communicating directly! It therefore has no corresponding web page.", "type": "browser" }, { "title": "Play from Disk", "link": "play-from-disk", "description": "The play-from-disk example demonstrates how to send video to your browser from a file saved to disk.", "type": "browser" }, { "title": "Play from Disk Renegotation", "link": "play-from-disk", "description": "The play-from-disk-renegotation example is an extension of the play-from-disk example, but demonstrates how you can add/remove video tracks from an already negotiated PeerConnection.", "type": "browser" }, { "title": "Insertable Streams", "link": "insertable-streams", "description": "The insertable-streams example demonstrates how Pion can be used to send E2E encrypted video and decrypt via insertable streams in the browser.", "type": "browser" }, { "title": "Save to Disk", "link": "save-to-disk", "description": "The save-to-disk example shows how to record your webcam and save the footage to disk on the server side.", "type": "browser" }, { "title": "Broadcast", "link": "broadcast", "description": "The broadcast example demonstrates how to broadcast a video to multiple peers. A broadcaster uploads the video once and the server forwards it to all other peers.", "type": "browser" }, { "title": "RTP Forwarder", "link": "rtp-forwarder", "description": "The rtp-forwarder example demonstrates how to forward your audio/video streams using RTP.", "type": "browser" }, { "title": "RTP to WebRTC", "link": "rtp-to-webrtc", "description": "The rtp-to-webrtc example demonstrates how to take RTP packets sent to a Pion process into your browser.", "type": "browser" }, { "title": "Custom Logger", "link": "#", "description": "Example custom-logger demonstrates how the user can override the logging and process messages instead of printing to stdout. It has no corresponding web page.", "type": "browser" }, { "title": "Simulcast", "link": "simulcast", "description": "Example simulcast demonstrates how to accept and demux 1 Track that contains 3 Simulcast streams. It then returns the media as 3 independent Tracks back to the sender.", "type": "browser" }, { "title": "ICE Restart", "link": "#", "description": "Example ice-restart demonstrates how a WebRTC connection can roam between networks. This example restarts ICE in a loop and prints the new addresses it uses each time.", "type": "browser" }, { "title": "ICE Single Port", "link": "#", "description": "Example ice-single-port demonstrates how multiple WebRTC connections can be served from a single port. By default Pion listens on a new port for every PeerConnection. Pion can be configured to use a single port for multiple connections.", "type": "browser" }, { "title": "ICE TCP", "link": "#", "description": "Example ice-tcp demonstrates how a WebRTC connection can be made over TCP instead of UDP. By default Pion only does UDP. Pion can be configured to use a TCP port, and this TCP port can be used for many connections.", "type": "browser" }, { "title": "Swap Tracks", "link": "swap-tracks", "description": "The swap-tracks example demonstrates deeper usage of the Pion Media API. The server accepts 3 media streams, and then dynamically routes them back as a single stream to the user.", "type": "browser" }, { "title": "VNet", "link": "#", "description": "The vnet example demonstrates Pion's network virtualisation library. This example connects two PeerConnections over a virtual network and prints statistics about the data traveling over it.", "type": "browser" }, { "title": "rtcp-processing", "link": "rtcp-processing", "description": "The rtcp-processing example demonstrates Pion's RTCP APIs. This allow access to media statistics and control information.", "type": "browser" }, { "title": "trickle-ice", "link": "#", "description": "The trickle-ice example demonstrates Pion WebRTC's Trickle ICE APIs.", "type": "browser" } ] webrtc-3.1.56/examples/ice-restart/000077500000000000000000000000001437620512100171355ustar00rootroot00000000000000webrtc-3.1.56/examples/ice-restart/README.md000066400000000000000000000021501437620512100204120ustar00rootroot00000000000000# ice-restart ice-restart demonstrates Pion WebRTC's ICE Restart abilities. ## Instructions ### Download ice-restart This example requires you to clone the repo since it is serving static HTML. ``` mkdir -p $GOPATH/src/github.com/pion cd $GOPATH/src/github.com/pion git clone https://github.com/pion/webrtc.git cd webrtc/examples/ice-restart ``` ### Run ice-restart Execute `go run *.go` ### Open the Web UI Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. This page will now prints stats about the PeerConnection and allow you to do an ICE Restart at anytime. * `ICE Restart` is the button that causes a new offer to be made wih `iceRestart: true`. * `ICE Connection States` will contain all the connection states the PeerConnection moves through. * `ICE Selected Pairs` will print the selected pair every 3 seconds. Note how the uFrag/uPwd/Port change everytime you start the Restart process. * `Inbound DataChannel Messages` containing the current time sent by the Pion process every 3 seconds. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/ice-restart/index.html000066400000000000000000000041731437620512100211370ustar00rootroot00000000000000 ice-restart

ICE Connection States


ICE Selected Pairs


Inbound DataChannel Messages

webrtc-3.1.56/examples/ice-restart/main.go000066400000000000000000000041431437620512100204120ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "net/http" "time" "github.com/pion/webrtc/v3" ) var peerConnection *webrtc.PeerConnection //nolint func doSignaling(w http.ResponseWriter, r *http.Request) { var err error if peerConnection == nil { if peerConnection, err = webrtc.NewPeerConnection(webrtc.Configuration{}); err != nil { panic(err) } // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String()) }) // Send the current time via a DataChannel to the remote peer every 3 seconds peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { d.OnOpen(func() { for range time.Tick(time.Second * 3) { if err = d.SendText(time.Now().String()); err != nil { panic(err) } } }) }) } var offer webrtc.SessionDescription if err = json.NewDecoder(r.Body).Decode(&offer); err != nil { panic(err) } if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } else if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete response, err := json.Marshal(*peerConnection.LocalDescription()) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(response); err != nil { panic(err) } } func main() { http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/doSignaling", doSignaling) fmt.Println("Open http://localhost:8080 to access this demo") panic(http.ListenAndServe(":8080", nil)) } webrtc-3.1.56/examples/ice-single-port/000077500000000000000000000000001437620512100177145ustar00rootroot00000000000000webrtc-3.1.56/examples/ice-single-port/README.md000066400000000000000000000020761437620512100212000ustar00rootroot00000000000000# ice-single-port ice-single-port demonstrates Pion WebRTC's ability to serve many PeerConnections on a single port. Pion WebRTC has no global state, so by default ports can't be shared between two PeerConnections. Using the SettingEngine, a developer can manually share state between many PeerConnections to allow multiple PeerConnections to use the same port. ## Instructions ### Download ice-single-port This example requires you to clone the repo since it is serving static HTML. ``` mkdir -p $GOPATH/src/github.com/pion cd $GOPATH/src/github.com/pion git clone https://github.com/pion/webrtc.git cd webrtc/examples/ice-single-port ``` ### Run ice-single-port Execute `go run *.go` ### Open the Web UI Open [http://localhost:8080](http://localhost:8080). This will automatically open 10 PeerConnections. This page will print a Local/Remote line for each PeerConnection. Note that all 10 PeerConnections have different ports for their Local port. However for the remote they all will be using port 8443. Congrats, you have used Pion WebRTC! Now start building something cool. webrtc-3.1.56/examples/ice-single-port/index.html000066400000000000000000000025041437620512100217120ustar00rootroot00000000000000 ice-single-port

ICE Selected Pairs


webrtc-3.1.56/examples/ice-single-port/main.go000066400000000000000000000054741437620512100212010ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "encoding/json" "fmt" "net/http" "time" "github.com/pion/ice/v2" "github.com/pion/webrtc/v3" ) var api *webrtc.API //nolint // Everything below is the Pion WebRTC API! Thanks for using it ❤️. func doSignaling(w http.ResponseWriter, r *http.Request) { peerConnection, err := api.NewPeerConnection(webrtc.Configuration{}) if err != nil { panic(err) } // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String()) }) // Send the current time via a DataChannel to the remote peer every 3 seconds peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { d.OnOpen(func() { for range time.Tick(time.Second * 3) { if err = d.SendText(time.Now().String()); err != nil { panic(err) } } }) }) var offer webrtc.SessionDescription if err = json.NewDecoder(r.Body).Decode(&offer); err != nil { panic(err) } if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } else if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete response, err := json.Marshal(*peerConnection.LocalDescription()) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(response); err != nil { panic(err) } } func main() { // Create a SettingEngine, this allows non-standard WebRTC behavior settingEngine := webrtc.SettingEngine{} // Configure our SettingEngine to use our UDPMux. By default a PeerConnection has // no global state. The API+SettingEngine allows the user to share state between them. // In this case we are sharing our listening port across many. // Listen on UDP Port 8443, will be used for all WebRTC traffic mux, err := ice.NewMultiUDPMuxFromPort(8443) if err != nil { panic(err) } fmt.Printf("Listening for WebRTC traffic at %d\n", 8443) settingEngine.SetICEUDPMux(mux) // Create a new API using our SettingEngine api = webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)) http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/doSignaling", doSignaling) fmt.Println("Open http://localhost:8080 to access this demo") panic(http.ListenAndServe(":8080", nil)) } webrtc-3.1.56/examples/ice-tcp/000077500000000000000000000000001437620512100162375ustar00rootroot00000000000000webrtc-3.1.56/examples/ice-tcp/README.md000066400000000000000000000012421437620512100175150ustar00rootroot00000000000000# ice-tcp ice-tcp demonstrates Pion WebRTC's ICE TCP abilities. ## Instructions ### Download ice-tcp This example requires you to clone the repo since it is serving static HTML. ``` mkdir -p $GOPATH/src/github.com/pion cd $GOPATH/src/github.com/pion git clone https://github.com/pion/webrtc.git cd webrtc/examples/ice-tcp ``` ### Run ice-tcp Execute `go run *.go` ### Open the Web UI Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. This page will now prints stats about the PeerConnection. The UDP candidates will be filtered out from the SDP. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/ice-tcp/index.html000066400000000000000000000024231437620512100202350ustar00rootroot00000000000000 ice-tcp

ICE TCP

ICE Connection States


Inbound DataChannel Messages

webrtc-3.1.56/examples/ice-tcp/main.go000066400000000000000000000052501437620512100175140ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "encoding/json" "errors" "fmt" "io" "net" "net/http" "time" "github.com/pion/webrtc/v3" ) var api *webrtc.API //nolint func doSignaling(w http.ResponseWriter, r *http.Request) { peerConnection, err := api.NewPeerConnection(webrtc.Configuration{}) if err != nil { panic(err) } // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String()) }) // Send the current time via a DataChannel to the remote peer every 3 seconds peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { d.OnOpen(func() { for range time.Tick(time.Second * 3) { if err = d.SendText(time.Now().String()); err != nil { if errors.Is(io.ErrClosedPipe, err) { return } panic(err) } } }) }) var offer webrtc.SessionDescription if err = json.NewDecoder(r.Body).Decode(&offer); err != nil { panic(err) } if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } else if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete response, err := json.Marshal(*peerConnection.LocalDescription()) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(response); err != nil { panic(err) } } func main() { settingEngine := webrtc.SettingEngine{} // Enable support only for TCP ICE candidates. settingEngine.SetNetworkTypes([]webrtc.NetworkType{ webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6, }) tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{ IP: net.IP{0, 0, 0, 0}, Port: 8443, }) if err != nil { panic(err) } fmt.Printf("Listening for ICE TCP at %s\n", tcpListener.Addr()) tcpMux := webrtc.NewICETCPMux(nil, tcpListener, 8) settingEngine.SetICETCPMux(tcpMux) api = webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)) http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/doSignaling", doSignaling) fmt.Println("Open http://localhost:8080 to access this demo") panic(http.ListenAndServe(":8080", nil)) } webrtc-3.1.56/examples/index.html000066400000000000000000000025111437620512100167070ustar00rootroot00000000000000 WebRTC examples! | Pion

Pion WebRTC examples

{{range .}}

{{ .Title }}

{{ .Description }}

{{ if .IsJS}}

Run JavaScript

{{ end }} {{ if .IsWASM}}

Run WASM

{{ end }}
{{else}}
  • No examples found!
  • {{end}}
    webrtc-3.1.56/examples/insertable-streams/000077500000000000000000000000001437620512100205175ustar00rootroot00000000000000webrtc-3.1.56/examples/insertable-streams/README.md000066400000000000000000000034101437620512100217740ustar00rootroot00000000000000# insertable-streams insertable-streams demonstrates how to use insertable streams with Pion. This example modifies the video with a single-byte XOR cipher before sending, and then decrypts in Javascript. insertable-streams allows the browser to process encoded video. You could implement E2E encyption, add metadata or insert a completely different video feed! ## Instructions ### Create IVF named `output.ivf` that contains a VP8 track ``` ffmpeg -i $INPUT_FILE -g 30 output.ivf ``` ### Download insertable-streams ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/insertable-streams ``` ### Open insertable-streams example page [jsfiddle.net](https://jsfiddle.net/t5xoaryc/) you should see two text-areas and a 'Start Session' button. You will also have a 'Decrypt' checkbox. When unchecked the browser will not decrypt the incoming video stream, so it will stop playing or display certificates. ### Run insertable-streams with your browsers SessionDescription as stdin The `output.ivf` you created should be in the same directory as `insertable-streams`. In the jsfiddle the top textarea is your browser, copy that and: #### Linux/macOS Run `echo $BROWSER_SDP | insertable-streams` #### Windows 1. Paste the SessionDescription into a file. 1. Run `insertable-streams < my_file` ### Input insertable-streams's SessionDescription into your browser Copy the text that `insertable-streams` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle, enjoy your video! A video should start playing in your browser above the input boxes. `insertable-streams` will exit when the file reaches the end. To stop decrypting the stream uncheck the box and the video will not be viewable. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/insertable-streams/jsfiddle/000077500000000000000000000000001437620512100223035ustar00rootroot00000000000000webrtc-3.1.56/examples/insertable-streams/jsfiddle/demo.css000066400000000000000000000000641437620512100237410ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/insertable-streams/jsfiddle/demo.details000066400000000000000000000002341437620512100245750ustar00rootroot00000000000000--- name: play-from-disk description: play-from-disk demonstrates how to send video to your browser from a file saved to disk. authors: - Sean DuBois webrtc-3.1.56/examples/insertable-streams/jsfiddle/demo.html000066400000000000000000000012251437620512100241150ustar00rootroot00000000000000

    Browser does not support insertable streams

    Browser base64 Session Description

    Golang base64 Session Description

    Decrypt Video

    Video

    Logs
    webrtc-3.1.56/examples/insertable-streams/jsfiddle/demo.js000066400000000000000000000055621437620512100235750ustar00rootroot00000000000000/* eslint-env browser */ // cipherKey that video is encrypted with const cipherKey = 0xAA const pc = new RTCPeerConnection({ encodedInsertableStreams: true, forceEncodedVideoInsertableStreams: true }) const log = msg => { document.getElementById('div').innerHTML += msg + '
    ' } // Offer to receive 1 video const transceiver = pc.addTransceiver('video') // The API has seen two iterations, support both // In the future this will just be `createEncodedStreams` const receiverStreams = getInsertableStream(transceiver) // boolean controlled by checkbox to enable/disable encryption let applyDecryption = true window.toggleDecryption = () => { applyDecryption = !applyDecryption } // Loop that is called for each video frame const reader = receiverStreams.readable.getReader() const writer = receiverStreams.writable.getWriter() reader.read().then(function processVideo ({ done, value }) { const decrypted = new DataView(value.data) if (applyDecryption) { for (let i = 0; i < decrypted.buffer.byteLength; i++) { decrypted.setInt8(i, decrypted.getInt8(i) ^ cipherKey) } } value.data = decrypted.buffer writer.write(value) return reader.read().then(processVideo) }) // Fire when remote video arrives pc.ontrack = function (event) { document.getElementById('remote-video').srcObject = event.streams[0] document.getElementById('remote-video').style = '' } // Populate SDP field when finished gathering pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } // DOM code to show banner if insertable streams not supported let insertableStreamsSupported = true const updateSupportBanner = () => { const el = document.getElementById('no-support-banner') if (insertableStreamsSupported && el) { el.style = 'display: none' } } document.addEventListener('DOMContentLoaded', updateSupportBanner) // Shim to support both versions of API function getInsertableStream (transceiver) { let insertableStreams = null if (transceiver.receiver.createEncodedVideoStreams) { insertableStreams = transceiver.receiver.createEncodedVideoStreams() } else if (transceiver.receiver.createEncodedStreams) { insertableStreams = transceiver.receiver.createEncodedStreams() } if (!insertableStreams) { insertableStreamsSupported = false updateSupportBanner() throw new Error('Insertable Streams are not supported') } return insertableStreams } webrtc-3.1.56/examples/insertable-streams/main.go000066400000000000000000000107111437620512100217720ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "context" "errors" "fmt" "io" "os" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/ivfreader" ) const cipherKey = 0xAA func main() { peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, }) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Create a video track videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion") if err != nil { panic(err) } rtpSender, err := peerConnection.AddTrack(videoTrack) if err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background()) go func() { // Open a IVF file and start reading using our IVFReader file, ivfErr := os.Open("output.ivf") if ivfErr != nil { panic(ivfErr) } ivf, header, ivfErr := ivfreader.NewWith(file) if ivfErr != nil { panic(ivfErr) } // Wait for connection established <-iceConnectedCtx.Done() // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. sleepTime := time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000) for { frame, _, ivfErr := ivf.ParseNextFrame() if errors.Is(ivfErr, io.EOF) { fmt.Printf("All frames parsed and sent") os.Exit(0) } if ivfErr != nil { panic(ivfErr) } // Encrypt video using XOR Cipher for i := range frame { frame[i] ^= cipherKey } time.Sleep(sleepTime) if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil { panic(ivfErr) } } }() // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) if connectionState == webrtc.ICEConnectionStateConnected { iceConnectedCtxCancel() } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/internal/000077500000000000000000000000001437620512100165275ustar00rootroot00000000000000webrtc-3.1.56/examples/internal/signal/000077500000000000000000000000001437620512100200045ustar00rootroot00000000000000webrtc-3.1.56/examples/internal/signal/http.go000066400000000000000000000010641437620512100213130ustar00rootroot00000000000000package signal import ( "flag" "fmt" "io/ioutil" "net/http" "strconv" ) // HTTPSDPServer starts a HTTP Server that consumes SDPs func HTTPSDPServer() chan string { port := flag.Int("port", 8080, "http server port") flag.Parse() sdpChan := make(chan string) http.HandleFunc("/sdp", func(w http.ResponseWriter, r *http.Request) { body, _ := ioutil.ReadAll(r.Body) fmt.Fprintf(w, "done") sdpChan <- string(body) }) go func() { err := http.ListenAndServe(":"+strconv.Itoa(*port), nil) if err != nil { panic(err) } }() return sdpChan } webrtc-3.1.56/examples/internal/signal/rand.go000066400000000000000000000006571437620512100212670ustar00rootroot00000000000000package signal import "github.com/pion/randutil" // RandSeq generates a random string to serve as dummy data // // It returns a deterministic sequence of values each time a program is run. // Use rand.Seed() function in your real applications. func RandSeq(n int) string { val, err := randutil.GenerateCryptoRandomString(n, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") if err != nil { panic(err) } return val } webrtc-3.1.56/examples/internal/signal/signal.go000066400000000000000000000033521437620512100216130ustar00rootroot00000000000000// Package signal contains helpers to exchange the SDP session // description between examples. package signal import ( "bufio" "bytes" "compress/gzip" "encoding/base64" "encoding/json" "fmt" "io" "io/ioutil" "os" "strings" ) // Allows compressing offer/answer to bypass terminal input limits. const compress = false // MustReadStdin blocks until input is received from stdin func MustReadStdin() string { r := bufio.NewReader(os.Stdin) var in string for { var err error in, err = r.ReadString('\n') if err != io.EOF { if err != nil { panic(err) } } in = strings.TrimSpace(in) if len(in) > 0 { break } } fmt.Println("") return in } // Encode encodes the input in base64 // It can optionally zip the input before encoding func Encode(obj interface{}) string { b, err := json.Marshal(obj) if err != nil { panic(err) } if compress { b = zip(b) } return base64.StdEncoding.EncodeToString(b) } // Decode decodes the input from base64 // It can optionally unzip the input after decoding func Decode(in string, obj interface{}) { b, err := base64.StdEncoding.DecodeString(in) if err != nil { panic(err) } if compress { b = unzip(b) } err = json.Unmarshal(b, obj) if err != nil { panic(err) } } func zip(in []byte) []byte { var b bytes.Buffer gz := gzip.NewWriter(&b) _, err := gz.Write(in) if err != nil { panic(err) } err = gz.Flush() if err != nil { panic(err) } err = gz.Close() if err != nil { panic(err) } return b.Bytes() } func unzip(in []byte) []byte { var b bytes.Buffer _, err := b.Write(in) if err != nil { panic(err) } r, err := gzip.NewReader(&b) if err != nil { panic(err) } res, err := ioutil.ReadAll(r) if err != nil { panic(err) } return res } webrtc-3.1.56/examples/ortc/000077500000000000000000000000001437620512100156625ustar00rootroot00000000000000webrtc-3.1.56/examples/ortc/README.md000066400000000000000000000021431437620512100171410ustar00rootroot00000000000000# ortc ortc demonstrates Pion WebRTC's [ORTC](https://ortc.org/) capabilities. Instead of using the Session Description Protocol to configure and communicate ORTC provides APIs. Users then can implement signaling with whatever protocol they wish. ORTC can then be used to implement WebRTC. A ORTC implementation can parse/emit Session Description and act as a WebRTC implementation. In this example we have defined a simple JSON based signaling protocol. ## Instructions ### Download ortc ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/ortc ``` ### Run first client as offerer `ortc -offer` this will emit a base64 message. Copy this message to your clipboard. ## Run the second client as answerer Run the second client. This should be launched with the message you copied in the previous step as stdin. `echo BASE64_MESSAGE_YOU_COPIED | ortc` ### Enjoy If everything worked you will see `Data channel 'Foo'-'' open.` in each terminal. Each client will send random messages every 5 seconds that will appear in the terminal Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/ortc/main.go000066400000000000000000000101131437620512100171310ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "flag" "fmt" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func main() { isOffer := flag.Bool("offer", false, "Act as the offerer if set") flag.Parse() // Everything below is the Pion WebRTC (ORTC) API! Thanks for using it ❤️. // Prepare ICE gathering options iceOptions := webrtc.ICEGatherOptions{ ICEServers: []webrtc.ICEServer{ {URLs: []string{"stun:stun.l.google.com:19302"}}, }, } // Create an API object api := webrtc.NewAPI() // Create the ICE gatherer gatherer, err := api.NewICEGatherer(iceOptions) if err != nil { panic(err) } // Construct the ICE transport ice := api.NewICETransport(gatherer) // Construct the DTLS transport dtls, err := api.NewDTLSTransport(ice, nil) if err != nil { panic(err) } // Construct the SCTP transport sctp := api.NewSCTPTransport(dtls) // Handle incoming data channels sctp.OnDataChannel(func(channel *webrtc.DataChannel) { fmt.Printf("New DataChannel %s %d\n", channel.Label(), channel.ID()) // Register the handlers channel.OnOpen(handleOnOpen(channel)) channel.OnMessage(func(msg webrtc.DataChannelMessage) { fmt.Printf("Message from DataChannel '%s': '%s'\n", channel.Label(), string(msg.Data)) }) }) gatherFinished := make(chan struct{}) gatherer.OnLocalCandidate(func(i *webrtc.ICECandidate) { if i == nil { close(gatherFinished) } }) // Gather candidates err = gatherer.Gather() if err != nil { panic(err) } <-gatherFinished iceCandidates, err := gatherer.GetLocalCandidates() if err != nil { panic(err) } iceParams, err := gatherer.GetLocalParameters() if err != nil { panic(err) } dtlsParams, err := dtls.GetLocalParameters() if err != nil { panic(err) } sctpCapabilities := sctp.GetCapabilities() s := Signal{ ICECandidates: iceCandidates, ICEParameters: iceParams, DTLSParameters: dtlsParams, SCTPCapabilities: sctpCapabilities, } // Exchange the information fmt.Println(signal.Encode(s)) remoteSignal := Signal{} signal.Decode(signal.MustReadStdin(), &remoteSignal) iceRole := webrtc.ICERoleControlled if *isOffer { iceRole = webrtc.ICERoleControlling } err = ice.SetRemoteCandidates(remoteSignal.ICECandidates) if err != nil { panic(err) } // Start the ICE transport err = ice.Start(nil, remoteSignal.ICEParameters, &iceRole) if err != nil { panic(err) } // Start the DTLS transport err = dtls.Start(remoteSignal.DTLSParameters) if err != nil { panic(err) } // Start the SCTP transport err = sctp.Start(remoteSignal.SCTPCapabilities) if err != nil { panic(err) } // Construct the data channel as the offerer if *isOffer { var id uint16 = 1 dcParams := &webrtc.DataChannelParameters{ Label: "Foo", ID: &id, } var channel *webrtc.DataChannel channel, err = api.NewDataChannel(sctp, dcParams) if err != nil { panic(err) } // Register the handlers // channel.OnOpen(handleOnOpen(channel)) // TODO: OnOpen on handle ChannelAck go handleOnOpen(channel)() // Temporary alternative channel.OnMessage(func(msg webrtc.DataChannelMessage) { fmt.Printf("Message from DataChannel '%s': '%s'\n", channel.Label(), string(msg.Data)) }) } select {} } // Signal is used to exchange signaling info. // This is not part of the ORTC spec. You are free // to exchange this information any way you want. type Signal struct { ICECandidates []webrtc.ICECandidate `json:"iceCandidates"` ICEParameters webrtc.ICEParameters `json:"iceParameters"` DTLSParameters webrtc.DTLSParameters `json:"dtlsParameters"` SCTPCapabilities webrtc.SCTPCapabilities `json:"sctpCapabilities"` } func handleOnOpen(channel *webrtc.DataChannel) func() { return func() { fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", channel.Label(), channel.ID()) for range time.NewTicker(5 * time.Second).C { message := signal.RandSeq(15) fmt.Printf("Sending '%s' \n", message) err := channel.SendText(message) if err != nil { panic(err) } } } } webrtc-3.1.56/examples/pion-to-pion/000077500000000000000000000000001437620512100172435ustar00rootroot00000000000000webrtc-3.1.56/examples/pion-to-pion/README.md000066400000000000000000000012451437620512100205240ustar00rootroot00000000000000# pion-to-pion pion-to-pion is an example of two pion instances communicating directly! The SDP offer and answer are exchanged automatically over HTTP. The `answer` side acts like a HTTP server and should therefore be ran first. ## Instructions First run `answer`: ```sh export GO111MODULE=on go install github.com/pion/webrtc/v3/examples/pion-to-pion/answer answer ``` Next, run `offer`: ```sh go install github.com/pion/webrtc/v3/examples/pion-to-pion/offer offer ``` You should see them connect and start to exchange messages. ## You can use Docker-compose to start this example: ```sh docker-compose up -d ``` Now, you can see message exchanging, using `docker logs`. webrtc-3.1.56/examples/pion-to-pion/answer/000077500000000000000000000000001437620512100205425ustar00rootroot00000000000000webrtc-3.1.56/examples/pion-to-pion/answer/Dockerfile000066400000000000000000000002201437620512100225260ustar00rootroot00000000000000FROM golang:1.20 ENV GO111MODULE=on RUN go install github.com/pion/webrtc/v3/examples/pion-to-pion/answer@latest CMD ["answer"] EXPOSE 50000 webrtc-3.1.56/examples/pion-to-pion/answer/main.go000066400000000000000000000125521437620512100220220ustar00rootroot00000000000000package main import ( "bytes" "encoding/json" "flag" "fmt" "io/ioutil" "net/http" "os" "sync" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func signalCandidate(addr string, c *webrtc.ICECandidate) error { payload := []byte(c.ToJSON().Candidate) resp, err := http.Post(fmt.Sprintf("http://%s/candidate", addr), // nolint:noctx "application/json; charset=utf-8", bytes.NewReader(payload)) if err != nil { return err } if closeErr := resp.Body.Close(); closeErr != nil { return closeErr } return nil } func main() { // nolint:gocognit offerAddr := flag.String("offer-address", "localhost:50000", "Address that the Offer HTTP server is hosted on.") answerAddr := flag.String("answer-address", ":60000", "Address that the Answer HTTP server is hosted on.") flag.Parse() var candidatesMux sync.Mutex pendingCandidates := make([]*webrtc.ICECandidate, 0) // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewPeerConnection(config) if err != nil { panic(err) } defer func() { if err := peerConnection.Close(); err != nil { fmt.Printf("cannot close peerConnection: %v\n", err) } }() // When an ICE candidate is available send to the other Pion instance // the other Pion instance will add this candidate by calling AddICECandidate peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { if c == nil { return } candidatesMux.Lock() defer candidatesMux.Unlock() desc := peerConnection.RemoteDescription() if desc == nil { pendingCandidates = append(pendingCandidates, c) } else if onICECandidateErr := signalCandidate(*offerAddr, c); onICECandidateErr != nil { panic(onICECandidateErr) } }) // A HTTP handler that allows the other Pion instance to send us ICE candidates // This allows us to add ICE candidates faster, we don't have to wait for STUN or TURN // candidates which may be slower http.HandleFunc("/candidate", func(w http.ResponseWriter, r *http.Request) { candidate, candidateErr := ioutil.ReadAll(r.Body) if candidateErr != nil { panic(candidateErr) } if candidateErr := peerConnection.AddICECandidate(webrtc.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil { panic(candidateErr) } }) // A HTTP handler that processes a SessionDescription given to us from the other Pion process http.HandleFunc("/sdp", func(w http.ResponseWriter, r *http.Request) { sdp := webrtc.SessionDescription{} if err := json.NewDecoder(r.Body).Decode(&sdp); err != nil { panic(err) } if err := peerConnection.SetRemoteDescription(sdp); err != nil { panic(err) } // Create an answer to send to the other process answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Send our answer to the HTTP server listening in the other process payload, err := json.Marshal(answer) if err != nil { panic(err) } resp, err := http.Post(fmt.Sprintf("http://%s/sdp", *offerAddr), "application/json; charset=utf-8", bytes.NewReader(payload)) // nolint:noctx if err != nil { panic(err) } else if closeErr := resp.Body.Close(); closeErr != nil { panic(closeErr) } // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } candidatesMux.Lock() for _, c := range pendingCandidates { onICECandidateErr := signalCandidate(*offerAddr, c) if onICECandidateErr != nil { panic(onICECandidateErr) } } candidatesMux.Unlock() }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Register data channel creation handling peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID()) // Register channel opening handling d.OnOpen(func() { fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", d.Label(), d.ID()) for range time.NewTicker(5 * time.Second).C { message := signal.RandSeq(15) fmt.Printf("Sending '%s'\n", message) // Send the message as text sendTextErr := d.SendText(message) if sendTextErr != nil { panic(sendTextErr) } } }) // Register text message handling d.OnMessage(func(msg webrtc.DataChannelMessage) { fmt.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data)) }) }) // Start HTTP server that accepts requests from the offer process to exchange SDP and Candidates panic(http.ListenAndServe(*answerAddr, nil)) } webrtc-3.1.56/examples/pion-to-pion/docker-compose.yml000066400000000000000000000004051437620512100226770ustar00rootroot00000000000000version: '3' services: answer: container_name: answer build: ./answer command: answer -offer-address offer:50000 offer: container_name: offer depends_on: - answer build: ./offer command: offer -answer-address answer:60000 webrtc-3.1.56/examples/pion-to-pion/offer/000077500000000000000000000000001437620512100203445ustar00rootroot00000000000000webrtc-3.1.56/examples/pion-to-pion/offer/Dockerfile000066400000000000000000000002001437620512100223260ustar00rootroot00000000000000FROM golang:1.20 ENV GO111MODULE=on RUN go install github.com/pion/webrtc/v3/examples/pion-to-pion/offer@latest CMD ["offer"] webrtc-3.1.56/examples/pion-to-pion/offer/main.go000066400000000000000000000126651437620512100216310ustar00rootroot00000000000000package main import ( "bytes" "encoding/json" "flag" "fmt" "io/ioutil" "net/http" "os" "sync" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func signalCandidate(addr string, c *webrtc.ICECandidate) error { payload := []byte(c.ToJSON().Candidate) resp, err := http.Post(fmt.Sprintf("http://%s/candidate", addr), "application/json; charset=utf-8", bytes.NewReader(payload)) //nolint:noctx if err != nil { return err } if closeErr := resp.Body.Close(); closeErr != nil { return closeErr } return nil } func main() { //nolint:gocognit offerAddr := flag.String("offer-address", ":50000", "Address that the Offer HTTP server is hosted on.") answerAddr := flag.String("answer-address", "127.0.0.1:60000", "Address that the Answer HTTP server is hosted on.") flag.Parse() var candidatesMux sync.Mutex pendingCandidates := make([]*webrtc.ICECandidate, 0) // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewPeerConnection(config) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // When an ICE candidate is available send to the other Pion instance // the other Pion instance will add this candidate by calling AddICECandidate peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { if c == nil { return } candidatesMux.Lock() defer candidatesMux.Unlock() desc := peerConnection.RemoteDescription() if desc == nil { pendingCandidates = append(pendingCandidates, c) } else if onICECandidateErr := signalCandidate(*answerAddr, c); onICECandidateErr != nil { panic(onICECandidateErr) } }) // A HTTP handler that allows the other Pion instance to send us ICE candidates // This allows us to add ICE candidates faster, we don't have to wait for STUN or TURN // candidates which may be slower http.HandleFunc("/candidate", func(w http.ResponseWriter, r *http.Request) { candidate, candidateErr := ioutil.ReadAll(r.Body) if candidateErr != nil { panic(candidateErr) } if candidateErr := peerConnection.AddICECandidate(webrtc.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil { panic(candidateErr) } }) // A HTTP handler that processes a SessionDescription given to us from the other Pion process http.HandleFunc("/sdp", func(w http.ResponseWriter, r *http.Request) { sdp := webrtc.SessionDescription{} if sdpErr := json.NewDecoder(r.Body).Decode(&sdp); sdpErr != nil { panic(sdpErr) } if sdpErr := peerConnection.SetRemoteDescription(sdp); sdpErr != nil { panic(sdpErr) } candidatesMux.Lock() defer candidatesMux.Unlock() for _, c := range pendingCandidates { if onICECandidateErr := signalCandidate(*answerAddr, c); onICECandidateErr != nil { panic(onICECandidateErr) } } }) // Start HTTP server that accepts requests from the answer process go func() { panic(http.ListenAndServe(*offerAddr, nil)) }() // Create a datachannel with label 'data' dataChannel, err := peerConnection.CreateDataChannel("data", nil) if err != nil { panic(err) } // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Register channel opening handling dataChannel.OnOpen(func() { fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", dataChannel.Label(), dataChannel.ID()) for range time.NewTicker(5 * time.Second).C { message := signal.RandSeq(15) fmt.Printf("Sending '%s'\n", message) // Send the message as text sendTextErr := dataChannel.SendText(message) if sendTextErr != nil { panic(sendTextErr) } } }) // Register text message handling dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { fmt.Printf("Message from DataChannel '%s': '%s'\n", dataChannel.Label(), string(msg.Data)) }) // Create an offer to send to the other process offer, err := peerConnection.CreateOffer(nil) if err != nil { panic(err) } // Sets the LocalDescription, and starts our UDP listeners // Note: this will start the gathering of ICE candidates if err = peerConnection.SetLocalDescription(offer); err != nil { panic(err) } // Send our offer to the HTTP server listening in the other process payload, err := json.Marshal(offer) if err != nil { panic(err) } resp, err := http.Post(fmt.Sprintf("http://%s/sdp", *answerAddr), "application/json; charset=utf-8", bytes.NewReader(payload)) // nolint:noctx if err != nil { panic(err) } else if err := resp.Body.Close(); err != nil { panic(err) } // Block forever select {} } webrtc-3.1.56/examples/pion-to-pion/test.sh000077500000000000000000000004701437620512100205620ustar00rootroot00000000000000#!/bin/bash -eu docker compose up -d function on_exit { docker compose logs docker compose rm -fsv } trap on_exit EXIT TIMEOUT=10 timeout $TIMEOUT docker compose logs -f | grep -q "answer | Message from DataChannel" timeout $TIMEOUT docker compose logs -f | grep -q "offer | Message from DataChannel" webrtc-3.1.56/examples/play-from-disk-renegotation/000077500000000000000000000000001437620512100222455ustar00rootroot00000000000000webrtc-3.1.56/examples/play-from-disk-renegotation/README.md000066400000000000000000000027411437620512100235300ustar00rootroot00000000000000# play-from-disk-renegotiation play-from-disk-renegotiation demonstrates Pion WebRTC's renegotiation abilities. For a simpler example of playing a file from disk we also have [examples/play-from-disk](/examples/play-from-disk) ## Instructions ### Download play-from-disk-renegotiation This example requires you to clone the repo since it is serving static HTML. ``` mkdir -p $GOPATH/src/github.com/pion cd $GOPATH/src/github.com/pion git clone https://github.com/pion/webrtc.git cd webrtc/examples/play-from-disk-renegotiation ``` ### Create IVF named `output.ivf` that contains a VP8 track ``` ffmpeg -i $INPUT_FILE -g 30 -b:v 2M output.ivf ``` **Note**: In the `ffmpeg` command, the argument `-b:v 2M` specifies the video bitrate to be 2 megabits per second. We provide this default value to produce decent video quality, but if you experience problems with this configuration (such as dropped frames etc.), you can decrease this. See the [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html#Options) for more information on the format of the value. ### Run play-from-disk-renegotiation The `output.ivf` you created should be in the same directory as `play-from-disk-renegotiation`. Execute `go run *.go` ### Open the Web UI Open [http://localhost:8080](http://localhost:8080) and you should have a `Add Track` and `Remove Track` button. Press these to add as many tracks as you want, or to remove as many as you wish. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/play-from-disk-renegotation/index.html000066400000000000000000000035111437620512100242420ustar00rootroot00000000000000 play-from-disk-renegotation

    Video


    Logs

    webrtc-3.1.56/examples/play-from-disk-renegotation/main.go000066400000000000000000000131211437620512100235160ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "encoding/json" "fmt" "math/rand" "net/http" "os" "time" "github.com/pion/randutil" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/ivfreader" ) var peerConnection *webrtc.PeerConnection //nolint // doSignaling exchanges all state of the local PeerConnection and is called // every time a video is added or removed func doSignaling(w http.ResponseWriter, r *http.Request) { var offer webrtc.SessionDescription if err := json.NewDecoder(r.Body).Decode(&offer); err != nil { panic(err) } if err := peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } else if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete response, err := json.Marshal(*peerConnection.LocalDescription()) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(response); err != nil { panic(err) } } // Add a single video track func createPeerConnection(w http.ResponseWriter, r *http.Request) { if peerConnection.ConnectionState() != webrtc.PeerConnectionStateNew { panic(fmt.Sprintf("createPeerConnection called in non-new state (%s)", peerConnection.ConnectionState())) } doSignaling(w, r) fmt.Println("PeerConnection has been created") } // Add a single video track func addVideo(w http.ResponseWriter, r *http.Request) { videoTrack, err := webrtc.NewTrackLocalStaticSample( webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), ) if err != nil { panic(err) } rtpSender, err := peerConnection.AddTrack(videoTrack) if err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() go writeVideoToTrack(videoTrack) doSignaling(w, r) fmt.Println("Video track has been added") } // Remove a single sender func removeVideo(w http.ResponseWriter, r *http.Request) { if senders := peerConnection.GetSenders(); len(senders) != 0 { if err := peerConnection.RemoveTrack(senders[0]); err != nil { panic(err) } } doSignaling(w, r) fmt.Println("Video track has been removed") } func main() { rand.Seed(time.Now().UTC().UnixNano()) var err error if peerConnection, err = webrtc.NewPeerConnection(webrtc.Configuration{}); err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/createPeerConnection", createPeerConnection) http.HandleFunc("/addVideo", addVideo) http.HandleFunc("/removeVideo", removeVideo) go func() { fmt.Println("Open http://localhost:8080 to access this demo") panic(http.ListenAndServe(":8080", nil)) }() // Block forever select {} } // Read a video file from disk and write it to a webrtc.Track // When the video has been completely read this exits without error func writeVideoToTrack(t *webrtc.TrackLocalStaticSample) { // Open a IVF file and start reading using our IVFReader file, err := os.Open("output.ivf") if err != nil { panic(err) } ivf, header, err := ivfreader.NewWith(file) if err != nil { panic(err) } // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. // // It is important to use a time.Ticker instead of time.Sleep because // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)) for ; true; <-ticker.C { frame, _, err := ivf.ParseNextFrame() if err != nil { fmt.Printf("Finish writing video track: %s ", err) return } if err = t.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil { fmt.Printf("Finish writing video track: %s ", err) return } } } webrtc-3.1.56/examples/play-from-disk/000077500000000000000000000000001437620512100175515ustar00rootroot00000000000000webrtc-3.1.56/examples/play-from-disk/README.md000066400000000000000000000041641437620512100210350ustar00rootroot00000000000000# play-from-disk play-from-disk demonstrates how to send video and/or audio to your browser from files saved to disk. For an example of playing H264 from disk see [play-from-disk-h264](https://github.com/pion/example-webrtc-applications/tree/master/play-from-disk-h264) ## Instructions ### Create IVF named `output.ivf` that contains a VP8 track and/or `output.ogg` that contains a Opus track ``` ffmpeg -i $INPUT_FILE -g 30 -b:v 2M output.ivf ffmpeg -i $INPUT_FILE -c:a libopus -page_duration 20000 -vn output.ogg ``` **Note**: In the `ffmpeg` command which produces the .ivf file, the argument `-b:v 2M` specifies the video bitrate to be 2 megabits per second. We provide this default value to produce decent video quality, but if you experience problems with this configuration (such as dropped frames etc.), you can decrease this. See the [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html#Options) for more information on the format of the value. ### Download play-from-disk ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/play-from-disk ``` ### Open play-from-disk example page [jsfiddle.net](https://jsfiddle.net/8kup9mvn/) you should see two text-areas, 'Start Session' button and 'Copy browser SessionDescription to clipboard' ### Run play-from-disk with your browsers Session Description as stdin The `output.ivf` you created should be in the same directory as `play-from-disk`. In the jsfiddle press 'Copy browser Session Description to clipboard' or copy the base64 string manually. Now use this value you just copied as the input to `play-from-disk` #### Linux/macOS Run `echo $BROWSER_SDP | play-from-disk` #### Windows 1. Paste the SessionDescription into a file. 1. Run `play-from-disk < my_file` ### Input play-from-disk's Session Description into your browser Copy the text that `play-from-disk` just emitted and copy into the second text area in the jsfiddle ### Hit 'Start Session' in jsfiddle, enjoy your video! A video should start playing in your browser above the input boxes. `play-from-disk` will exit when the file reaches the end Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/play-from-disk/jsfiddle/000077500000000000000000000000001437620512100213355ustar00rootroot00000000000000webrtc-3.1.56/examples/play-from-disk/jsfiddle/demo.css000066400000000000000000000000641437620512100227730ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/play-from-disk/jsfiddle/demo.details000066400000000000000000000002341437620512100236270ustar00rootroot00000000000000--- name: play-from-disk description: play-from-disk demonstrates how to send video to your browser from a file saved to disk. authors: - Sean DuBois webrtc-3.1.56/examples/play-from-disk/jsfiddle/demo.html000066400000000000000000000007421437620512100231520ustar00rootroot00000000000000Browser Session Description




    Remote Session Description



    Video

    Logs
    webrtc-3.1.56/examples/play-from-disk/jsfiddle/demo.js000066400000000000000000000031361437620512100226220ustar00rootroot00000000000000/* eslint-env browser */ const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }) const log = msg => { document.getElementById('div').innerHTML += msg + '
    ' } pc.ontrack = function (event) { const el = document.createElement(event.track.kind) el.srcObject = event.streams[0] el.autoplay = true el.controls = true document.getElementById('remoteVideos').appendChild(el) } pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } // Offer to receive 1 audio, and 1 video track pc.addTransceiver('video', { direction: 'sendrecv' }) pc.addTransceiver('audio', { direction: 'sendrecv' }) pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySessionDescription = () => { const browserSessionDescription = document.getElementById('localSessionDescription') browserSessionDescription.focus() browserSessionDescription.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' log('Copying SessionDescription was ' + msg) } catch (err) { log('Oops, unable to copy SessionDescription ' + err) } } webrtc-3.1.56/examples/play-from-disk/main.go000066400000000000000000000165571437620512100210420ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "context" "errors" "fmt" "io" "os" "time" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/ivfreader" "github.com/pion/webrtc/v3/pkg/media/oggreader" ) const ( audioFileName = "output.ogg" videoFileName = "output.ivf" oggPageDuration = time.Millisecond * 20 ) func main() { // Assert that we have an audio or video file _, err := os.Stat(videoFileName) haveVideoFile := !os.IsNotExist(err) _, err = os.Stat(audioFileName) haveAudioFile := !os.IsNotExist(err) if !haveAudioFile && !haveVideoFile { panic("Could not find `" + audioFileName + "` or `" + videoFileName + "`") } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, }) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background()) if haveVideoFile { // Create a video track videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion") if videoTrackErr != nil { panic(videoTrackErr) } rtpSender, videoTrackErr := peerConnection.AddTrack(videoTrack) if videoTrackErr != nil { panic(videoTrackErr) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() go func() { // Open a IVF file and start reading using our IVFReader file, ivfErr := os.Open(videoFileName) if ivfErr != nil { panic(ivfErr) } ivf, header, ivfErr := ivfreader.NewWith(file) if ivfErr != nil { panic(ivfErr) } // Wait for connection established <-iceConnectedCtx.Done() // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. // // It is important to use a time.Ticker instead of time.Sleep because // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)) for ; true; <-ticker.C { frame, _, ivfErr := ivf.ParseNextFrame() if errors.Is(ivfErr, io.EOF) { fmt.Printf("All video frames parsed and sent") os.Exit(0) } if ivfErr != nil { panic(ivfErr) } if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil { panic(ivfErr) } } }() } if haveAudioFile { // Create a audio track audioTrack, audioTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion") if audioTrackErr != nil { panic(audioTrackErr) } rtpSender, audioTrackErr := peerConnection.AddTrack(audioTrack) if audioTrackErr != nil { panic(audioTrackErr) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() go func() { // Open a OGG file and start reading using our OGGReader file, oggErr := os.Open(audioFileName) if oggErr != nil { panic(oggErr) } // Open on oggfile in non-checksum mode. ogg, _, oggErr := oggreader.NewWith(file) if oggErr != nil { panic(oggErr) } // Wait for connection established <-iceConnectedCtx.Done() // Keep track of last granule, the difference is the amount of samples in the buffer var lastGranule uint64 // It is important to use a time.Ticker instead of time.Sleep because // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) ticker := time.NewTicker(oggPageDuration) for ; true; <-ticker.C { pageData, pageHeader, oggErr := ogg.ParseNextPage() if errors.Is(oggErr, io.EOF) { fmt.Printf("All audio pages parsed and sent") os.Exit(0) } if oggErr != nil { panic(oggErr) } // The amount of samples is the difference between the last and current timestamp sampleCount := float64(pageHeader.GranulePosition - lastGranule) lastGranule = pageHeader.GranulePosition sampleDuration := time.Duration((sampleCount/48000)*1000) * time.Millisecond if oggErr = audioTrack.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); oggErr != nil { panic(oggErr) } } }() } // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) if connectionState == webrtc.ICEConnectionStateConnected { iceConnectedCtxCancel() } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/reflect/000077500000000000000000000000001437620512100163375ustar00rootroot00000000000000webrtc-3.1.56/examples/reflect/README.md000066400000000000000000000022311437620512100176140ustar00rootroot00000000000000# reflect reflect demonstrates how with one PeerConnection you can send video to Pion and have the packets sent back. This example could be easily extended to do server side processing. ## Instructions ### Download reflect ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/reflect ``` ### Open reflect example page [jsfiddle.net](https://jsfiddle.net/g643ft1k/) you should see two text-areas and a 'Start Session' button. ### Run reflect, with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually. We will use this value in the next step. #### Linux/macOS Run `echo $BROWSER_SDP | reflect` #### Windows 1. Paste the SessionDescription into a file. 1. Run `reflect < my_file` ### Input reflect's SessionDescription into your browser Copy the text that `reflect` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle, enjoy your video! Your browser should send video to Pion, and then it will be relayed right back to you. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/reflect/jsfiddle/000077500000000000000000000000001437620512100201235ustar00rootroot00000000000000webrtc-3.1.56/examples/reflect/jsfiddle/demo.css000066400000000000000000000000641437620512100215610ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/reflect/jsfiddle/demo.details000066400000000000000000000002461437620512100224200ustar00rootroot00000000000000--- name: reflect description: Example of how to have Pion send back to the user exactly what it receives using the same PeerConnection. authors: - Sean DuBois webrtc-3.1.56/examples/reflect/jsfiddle/demo.html000066400000000000000000000007241437620512100217400ustar00rootroot00000000000000Browser base64 Session Description



    Golang base64 Session Description



    Video

    Logs
    webrtc-3.1.56/examples/reflect/jsfiddle/demo.js000066400000000000000000000030301437620512100214010ustar00rootroot00000000000000/* eslint-env browser */ const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }) const log = msg => { document.getElementById('logs').innerHTML += msg + '
    ' } navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { stream.getTracks().forEach(track => pc.addTrack(track, stream)) pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) }).catch(log) pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } pc.ontrack = function (event) { const el = document.createElement(event.track.kind) el.srcObject = event.streams[0] el.autoplay = true el.controls = true document.getElementById('remoteVideos').appendChild(el) } window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySDP = () => { const browserSDP = document.getElementById('localSessionDescription') browserSDP.focus() browserSDP.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' log('Copying SDP was ' + msg) } catch (err) { log('Unable to copy SDP ' + err) } } webrtc-3.1.56/examples/reflect/main.go000066400000000000000000000121711437620512100176140ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "fmt" "os" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func main() { // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Create a MediaEngine object to configure the supported codec m := &webrtc.MediaEngine{} // Setup the codecs you want to use. // We'll use a VP8 and Opus but you can also define your own if err := m.RegisterCodec(webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, PayloadType: 96, }, webrtc.RTPCodecTypeVideo); err != nil { panic(err) } // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry // for each PeerConnection. i := &interceptor.Registry{} // Use the default set of Interceptors if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { panic(err) } // Create the API object with the MediaEngine api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)) // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := api.NewPeerConnection(config) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Create Track that we send video back to browser on outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion") if err != nil { panic(err) } // Add this newly created track to the PeerConnection rtpSender, err := peerConnection.AddTrack(outputTrack) if err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(offer) if err != nil { panic(err) } // Set a handler for when a new remote track starts, this handler copies inbound RTP packets, // replaces the SSRC and sends them back peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval // This is a temporary fix until we implement incoming RTCP events, then we would push a PLI only when a viewer requests it go func() { ticker := time.NewTicker(time.Second * 3) for range ticker.C { errSend := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}) if errSend != nil { fmt.Println(errSend) } } }() fmt.Printf("Track has started, of type %d: %s \n", track.PayloadType(), track.Codec().MimeType) for { // Read RTP packets being sent to Pion rtp, _, readErr := track.ReadRTP() if readErr != nil { panic(readErr) } if writeErr := outputTrack.WriteRTP(rtp); writeErr != nil { panic(writeErr) } } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Create an answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/rtcp-processing/000077500000000000000000000000001437620512100200355ustar00rootroot00000000000000webrtc-3.1.56/examples/rtcp-processing/README.md000066400000000000000000000031441437620512100213160ustar00rootroot00000000000000# rtcp-processing rtcp-processing demonstrates the Public API for processing RTCP packets in Pion WebRTC. This example is only processing messages for a RTPReceiver. A RTPReceiver is used for accepting media from a remote peer. These APIs also exist on the RTPSender when sending media to a remote peer. RTCP is used for statistics and control information for media in WebRTC. Using these messages you can get information about the quality of the media, round trip time and packet loss. You can also craft messages to influence the media quality. ## Instructions ### Download rtcp-processing ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/rtcp-processing ``` ### Open rtcp-processing example page [jsfiddle.net](https://jsfiddle.net/zurq6j7x/) you should see two text-areas, 'Start Session' button and 'Copy browser SessionDescription to clipboard' ### Run rtcp-processing with your browsers Session Description as stdin In the jsfiddle press 'Copy browser Session Description to clipboard' or copy the base64 string manually. Now use this value you just copied as the input to `rtcp-processing` #### Linux/macOS Run `echo $BROWSER_SDP | rtcp-processing` #### Windows 1. Paste the SessionDescription into a file. 1. Run `rtcp-processing < my_file` ### Input rtcp-processing's Session Description into your browser Copy the text that `rtcp-processing` just emitted and copy into the second text area in the jsfiddle ### Hit 'Start Session' in jsfiddle You will see console messages for each inbound RTCP message from the remote peer. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/rtcp-processing/jsfiddle/000077500000000000000000000000001437620512100216215ustar00rootroot00000000000000webrtc-3.1.56/examples/rtcp-processing/jsfiddle/demo.css000066400000000000000000000000641437620512100232570ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/rtcp-processing/jsfiddle/demo.details000066400000000000000000000002161437620512100241130ustar00rootroot00000000000000--- name: rtcp-processing description: play-from-disk demonstrates how to process RTCP messages from Pion WebRTC authors: - Sean DuBois webrtc-3.1.56/examples/rtcp-processing/jsfiddle/demo.html000066400000000000000000000010101437620512100234230ustar00rootroot00000000000000Browser Session Description




    Remote Session Description



    Video

    Logs
    webrtc-3.1.56/examples/rtcp-processing/jsfiddle/demo.js000066400000000000000000000032461437620512100231100ustar00rootroot00000000000000/* eslint-env browser */ const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }) const log = msg => { document.getElementById('div').innerHTML += msg + '
    ' } pc.ontrack = function (event) { const el = document.createElement(event.track.kind) el.srcObject = event.streams[0] el.autoplay = true el.controls = true document.getElementById('remoteVideos').appendChild(el) } pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { document.getElementById('video1').srcObject = stream stream.getTracks().forEach(track => pc.addTrack(track, stream)) pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) }).catch(log) window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySessionDescription = () => { const browserSessionDescription = document.getElementById('localSessionDescription') browserSessionDescription.focus() browserSessionDescription.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' log('Copying SessionDescription was ' + msg) } catch (err) { log('Oops, unable to copy SessionDescription ' + err) } } webrtc-3.1.56/examples/rtcp-processing/main.go000066400000000000000000000046771437620512100213260ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "fmt" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func main() { // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewPeerConnection(config) if err != nil { panic(err) } // Set a handler for when a new remote track starts peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { fmt.Printf("Track has started streamId(%s) id(%s) rid(%s) \n", track.StreamID(), track.ID(), track.RID()) for { // Read the RTCP packets as they become available for our new remote track rtcpPackets, _, rtcpErr := receiver.ReadRTCP() if rtcpErr != nil { panic(rtcpErr) } for _, r := range rtcpPackets { // Print a string description of the packets if stringer, canString := r.(fmt.Stringer); canString { fmt.Printf("Received RTCP Packet: %v", stringer.String()) } } } }) // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(offer) if err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/rtp-forwarder/000077500000000000000000000000001437620512100175115ustar00rootroot00000000000000webrtc-3.1.56/examples/rtp-forwarder/README.md000066400000000000000000000037651437620512100210030ustar00rootroot00000000000000# rtp-forwarder rtp-forwarder is a simple application that shows how to forward your webcam/microphone via RTP using Pion WebRTC. ## Instructions ### Download rtp-forwarder ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/rtp-forwarder ``` ### Open rtp-forwarder example page [jsfiddle.net](https://jsfiddle.net/fm7btvr3/) you should see your Webcam, two text-areas and `Copy browser SDP to clipboard`, `Start Session` buttons ### Run rtp-forwarder, with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually. We will use this value in the next step. #### Linux/macOS Run `echo $BROWSER_SDP | rtp-forwarder` #### Windows 1. Paste the SessionDescription into a file. 1. Run `rtp-forwarder < my_file` ### Input rtp-forwarder's SessionDescription into your browser Copy the text that `rtp-forwarder` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle and enjoy your RTP forwarded stream! You can run any of these commands at anytime. The media is live/stateless, you can switch commands without restarting Pion. #### VLC Open `rtp-forwarder.sdp` with VLC and enjoy your live video! #### ffmpeg/ffprobe Run `ffprobe -i rtp-forwarder.sdp -protocol_whitelist file,udp,rtp` to get more details about your streams Run `ffplay -i rtp-forwarder.sdp -protocol_whitelist file,udp,rtp` to play your streams You can add `-fflags nobuffer -flags low_delay -framedrop` to lower the latency. You will have worse playback in networks with jitter. Read about minimizing the delay on [Stackoverflow](https://stackoverflow.com/a/49273163/5472819). #### Twitch/RTMP `ffmpeg -protocol_whitelist file,udp,rtp -i rtp-forwarder.sdp -c:v libx264 -preset veryfast -b:v 3000k -maxrate 3000k -bufsize 6000k -pix_fmt yuv420p -g 50 -c:a aac -b:a 160k -ac 2 -ar 44100 -f flv rtmp://live.twitch.tv/app/$STREAM_KEY` Make sure to replace `$STREAM_KEY` at the end of the URL first. webrtc-3.1.56/examples/rtp-forwarder/jsfiddle/000077500000000000000000000000001437620512100212755ustar00rootroot00000000000000webrtc-3.1.56/examples/rtp-forwarder/jsfiddle/demo.css000066400000000000000000000000641437620512100227330ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/rtp-forwarder/jsfiddle/demo.details000066400000000000000000000002041437620512100235640ustar00rootroot00000000000000--- name: rtp-forwarder description: Example of using Pion WebRTC to forward WebRTC streams via RTP authors: - Quentin Renard webrtc-3.1.56/examples/rtp-forwarder/jsfiddle/demo.html000066400000000000000000000007721437620512100231150ustar00rootroot00000000000000Browser base64 Session Description



    Golang base64 Session Description



    Video

    Logs
    webrtc-3.1.56/examples/rtp-forwarder/jsfiddle/demo.js000066400000000000000000000025621437620512100225640ustar00rootroot00000000000000/* eslint-env browser */ const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }) const log = msg => { document.getElementById('logs').innerHTML += msg + '
    ' } navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { stream.getTracks().forEach(track => pc.addTrack(track, stream)) document.getElementById('video1').srcObject = stream pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) }).catch(log) pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySDP = () => { const browserSDP = document.getElementById('localSessionDescription') browserSDP.focus() browserSDP.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' log('Copying SDP was ' + msg) } catch (err) { log('Unable to copy SDP ' + err) } } webrtc-3.1.56/examples/rtp-forwarder/main.go000066400000000000000000000162021437620512100207650ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "errors" "fmt" "net" "os" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) type udpConn struct { conn *net.UDPConn port int payloadType uint8 } func main() { // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Create a MediaEngine object to configure the supported codec m := &webrtc.MediaEngine{} // Setup the codecs you want to use. // We'll use a VP8 and Opus but you can also define your own if err := m.RegisterCodec(webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, }, webrtc.RTPCodecTypeVideo); err != nil { panic(err) } if err := m.RegisterCodec(webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, }, webrtc.RTPCodecTypeAudio); err != nil { panic(err) } // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry // for each PeerConnection. i := &interceptor.Registry{} // Use the default set of Interceptors if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { panic(err) } // Create the API object with the MediaEngine api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)) // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := api.NewPeerConnection(config) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Allow us to receive 1 audio track, and 1 video track if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil { panic(err) } else if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil { panic(err) } // Create a local addr var laddr *net.UDPAddr if laddr, err = net.ResolveUDPAddr("udp", "127.0.0.1:"); err != nil { panic(err) } // Prepare udp conns // Also update incoming packets with expected PayloadType, the browser may use // a different value. We have to modify so our stream matches what rtp-forwarder.sdp expects udpConns := map[string]*udpConn{ "audio": {port: 4000, payloadType: 111}, "video": {port: 4002, payloadType: 96}, } for _, c := range udpConns { // Create remote addr var raddr *net.UDPAddr if raddr, err = net.ResolveUDPAddr("udp", fmt.Sprintf("127.0.0.1:%d", c.port)); err != nil { panic(err) } // Dial udp if c.conn, err = net.DialUDP("udp", laddr, raddr); err != nil { panic(err) } defer func(conn net.PacketConn) { if closeErr := conn.Close(); closeErr != nil { panic(closeErr) } }(c.conn) } // Set a handler for when a new remote track starts, this handler will forward data to // our UDP listeners. // In your application this is where you would handle/process audio/video peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { // Retrieve udp connection c, ok := udpConns[track.Kind().String()] if !ok { return } // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval go func() { ticker := time.NewTicker(time.Second * 2) for range ticker.C { if rtcpErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}); rtcpErr != nil { fmt.Println(rtcpErr) } } }() b := make([]byte, 1500) rtpPacket := &rtp.Packet{} for { // Read n, _, readErr := track.Read(b) if readErr != nil { panic(readErr) } // Unmarshal the packet and update the PayloadType if err = rtpPacket.Unmarshal(b[:n]); err != nil { panic(err) } rtpPacket.PayloadType = c.payloadType // Marshal into original buffer with updated PayloadType if n, err = rtpPacket.MarshalTo(b); err != nil { panic(err) } // Write if _, writeErr := c.conn.Write(b[:n]); writeErr != nil { // For this particular example, third party applications usually timeout after a short // amount of time during which the user doesn't have enough time to provide the answer // to the browser. // That's why, for this particular example, the user first needs to provide the answer // to the browser then open the third party application. Therefore we must not kill // the forward on "connection refused" errors var opError *net.OpError if errors.As(writeErr, &opError) && opError.Err.Error() == "write: connection refused" { continue } panic(err) } } }) // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) if connectionState == webrtc.ICEConnectionStateConnected { fmt.Println("Ctrl+C the remote client to stop the demo") } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Done forwarding") os.Exit(0) } }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/rtp-forwarder/rtp-forwarder.sdp000066400000000000000000000002441437620512100230170ustar00rootroot00000000000000v=0 o=- 0 0 IN IP4 127.0.0.1 s=Pion WebRTC c=IN IP4 127.0.0.1 t=0 0 m=audio 4000 RTP/AVP 111 a=rtpmap:111 OPUS/48000/2 m=video 4002 RTP/AVP 96 a=rtpmap:96 VP8/90000webrtc-3.1.56/examples/rtp-to-webrtc/000077500000000000000000000000001437620512100174245ustar00rootroot00000000000000webrtc-3.1.56/examples/rtp-to-webrtc/README.md000066400000000000000000000054221437620512100207060ustar00rootroot00000000000000# rtp-to-webrtc rtp-to-webrtc demonstrates how to consume a RTP stream video UDP, and then send to a WebRTC client. With this example we have pre-made GStreamer and ffmpeg pipelines, but you can use any tool you like! ## Instructions ### Download rtp-to-webrtc ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/rtp-to-webrtc ``` ### Open jsfiddle example page [jsfiddle.net](https://jsfiddle.net/z7ms3u5r/) you should see two text-areas and a 'Start Session' button ### Run rtp-to-webrtc with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser's SessionDescription, copy that and: #### Linux/macOS Run `echo $BROWSER_SDP | rtp-to-webrtc` #### Windows 1. Paste the SessionDescription into a file. 1. Run `rtp-to-webrtc < my_file` ### Send RTP to listening socket You can use any software to send VP8 packets to port 5004. We also have the pre made examples below #### GStreamer ``` gst-launch-1.0 videotestsrc ! video/x-raw,width=640,height=480,format=I420 ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! rtpvp8pay ! udpsink host=127.0.0.1 port=5004 ``` #### ffmpeg ``` ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=30 -vcodec libvpx -cpu-used 5 -deadline 1 -g 10 -error-resilient 1 -auto-alt-ref 1 -f rtp 'rtp://127.0.0.1:5004?pkt_size=1200' ``` If you wish to send audio replace all occurrences of `vp8` with Opus in `main.go` then run ``` ffmpeg -f lavfi -i 'sine=frequency=1000' -c:a libopus -b:a 48000 -sample_fmt s16p -ssrc 1 -payload_type 111 -f rtp -max_delay 0 -application lowdelay 'rtp://127.0.0.1:5004?pkt_size=1200' ``` If you wish to send H264 instead of VP8 replace all occurrences of `vp8` with H264 in `main.go` then run ``` ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=30 -pix_fmt yuv420p -c:v libx264 -g 10 -preset ultrafast -tune zerolatency -f rtp 'rtp://127.0.0.1:5004?pkt_size=1200' ``` ### Input rtp-to-webrtc's SessionDescription into your browser Copy the text that `rtp-to-webrtc` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle, enjoy your video! A video should start playing in your browser above the input boxes. Congrats, you have used Pion WebRTC! Now start building something cool ## Dealing with broken/lossy inputs Pion WebRTC also provides a [SampleBuilder](https://pkg.go.dev/github.com/pion/webrtc/v3@v3.0.4/pkg/media/samplebuilder). This consumes RTP packets and returns samples. It can be used to re-order and delay for lossy streams. You can see its usage in this example in [daf27b](https://github.com/pion/webrtc/commit/daf27bd0598233b57428b7809587ec3c09510413). Currently it isn't working with H264, but is useful for VP8 and Opus. See [#1652](https://github.com/pion/webrtc/issues/1652) for the status of fixing for H264. webrtc-3.1.56/examples/rtp-to-webrtc/main.go000066400000000000000000000060201437620512100206750ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "errors" "fmt" "io" "net" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func main() { peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, }) if err != nil { panic(err) } // Open a UDP Listener for RTP Packets on port 5004 listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5004}) if err != nil { panic(err) } defer func() { if err = listener.Close(); err != nil { panic(err) } }() // Create a video track videoTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion") if err != nil { panic(err) } rtpSender, err := peerConnection.AddTrack(videoTrack) if err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) if connectionState == webrtc.ICEConnectionStateFailed { if closeErr := peerConnection.Close(); closeErr != nil { panic(closeErr) } } }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Read RTP packets forever and send them to the WebRTC Client inboundRTPPacket := make([]byte, 1600) // UDP MTU for { n, _, err := listener.ReadFrom(inboundRTPPacket) if err != nil { panic(fmt.Sprintf("error during read: %s", err)) } if _, err = videoTrack.Write(inboundRTPPacket[:n]); err != nil { if errors.Is(err, io.ErrClosedPipe) { // The peerConnection has been closed. return } panic(err) } } } webrtc-3.1.56/examples/save-to-disk-av1/000077500000000000000000000000001437620512100177065ustar00rootroot00000000000000webrtc-3.1.56/examples/save-to-disk-av1/README.md000066400000000000000000000032471437620512100211730ustar00rootroot00000000000000# save-to-disk-av1 save-to-disk-av1 is a simple application that shows how to save a video to disk using AV1. If you wish to save VP8 and Opus instead of AV1 see [save-to-disk](https://github.com/pion/webrtc/tree/master/examples/save-to-disk) If you wish to save VP8/Opus inside the same file see [save-to-webm](https://github.com/pion/example-webrtc-applications/tree/master/save-to-webm) ## Instructions ### Download save-to-disk-av1 ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/save-to-disk-av1 ``` ### Open save-to-disk-av1 example page [jsfiddle.net](https://jsfiddle.net/xjcve6d3/) you should see your Webcam, two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`. ### Run save-to-disk-av1, with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually. We will use this value in the next step. #### Linux/macOS Run `echo $BROWSER_SDP | save-to-disk-av1` #### Windows 1. Paste the SessionDescription into a file. 1. Run `save-to-disk-av1 < my_file` ### Input save-to-disk-av1's SessionDescription into your browser Copy the text that `save-to-disk-av1` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle, wait, close jsfiddle, enjoy your video! In the folder you ran `save-to-disk-av1` you should now have a file `output.ivf` play with your video player of choice! > Note: In order to correctly create the files, the remote client (JSFiddle) should be closed. The Go example will automatically close itself. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/save-to-disk-av1/main.go000066400000000000000000000113631437620512100211650ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "fmt" "os" "strings" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/ivfwriter" ) func saveToDisk(i media.Writer, track *webrtc.TrackRemote) { defer func() { if err := i.Close(); err != nil { panic(err) } }() for { rtpPacket, _, err := track.ReadRTP() if err != nil { panic(err) } if err := i.WriteRTP(rtpPacket); err != nil { panic(err) } } } func main() { // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Create a MediaEngine object to configure the supported codec m := &webrtc.MediaEngine{} // Setup the codecs you want to use. // We'll use a VP8 and Opus but you can also define your own if err := m.RegisterCodec(webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeAV1, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, PayloadType: 96, }, webrtc.RTPCodecTypeVideo); err != nil { panic(err) } // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry // for each PeerConnection. i := &interceptor.Registry{} // Use the default set of Interceptors if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { panic(err) } // Create the API object with the MediaEngine api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)) // Prepare the configuration config := webrtc.Configuration{} // Create a new RTCPeerConnection peerConnection, err := api.NewPeerConnection(config) if err != nil { panic(err) } // Allow us to receive 1 video track if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil { panic(err) } ivfFile, err := ivfwriter.New("output.ivf", ivfwriter.WithCodec(webrtc.MimeTypeAV1)) if err != nil { panic(err) } // Set a handler for when a new remote track starts, this handler saves buffers to disk as // an ivf file, since we could have multiple video tracks we provide a counter. // In your application this is where you would handle/process video peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval go func() { ticker := time.NewTicker(time.Second * 3) for range ticker.C { errSend := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}) if errSend != nil { fmt.Println(errSend) } } }() if strings.EqualFold(track.Codec().MimeType, webrtc.MimeTypeAV1) { fmt.Println("Got AV1 track, saving to disk as output.ivf") saveToDisk(ivfFile, track) } }) // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) if connectionState == webrtc.ICEConnectionStateConnected { fmt.Println("Ctrl+C the remote client to stop the demo") } else if connectionState == webrtc.ICEConnectionStateFailed { if closeErr := ivfFile.Close(); closeErr != nil { panic(closeErr) } fmt.Println("Done writing media files") // Gracefully shutdown the peer connection if closeErr := peerConnection.Close(); closeErr != nil { panic(closeErr) } os.Exit(0) } }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(offer) if err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/save-to-disk/000077500000000000000000000000001437620512100172215ustar00rootroot00000000000000webrtc-3.1.56/examples/save-to-disk/README.md000066400000000000000000000030421437620512100204770ustar00rootroot00000000000000# save-to-disk save-to-disk is a simple application that shows how to record your webcam/microphone using Pion WebRTC and save VP8/Opus to disk. If you wish to save VP8/Opus inside the same file see [save-to-webm](https://github.com/pion/example-webrtc-applications/tree/master/save-to-webm) ## Instructions ### Download save-to-disk ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/save-to-disk ``` ### Open save-to-disk example page [jsfiddle.net](https://jsfiddle.net/s179hacu/) you should see your Webcam, two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`. ### Run save-to-disk, with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually. We will use this value in the next step. #### Linux/macOS Run `echo $BROWSER_SDP | save-to-disk` #### Windows 1. Paste the SessionDescription into a file. 1. Run `save-to-disk < my_file` ### Input save-to-disk's SessionDescription into your browser Copy the text that `save-to-disk` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle, wait, close jsfiddle, enjoy your video! In the folder you ran `save-to-disk` you should now have a file `output-1.ivf` play with your video player of choice! > Note: In order to correctly create the files, the remote client (JSFiddle) should be closed. The Go example will automatically close itself. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/save-to-disk/jsfiddle/000077500000000000000000000000001437620512100210055ustar00rootroot00000000000000webrtc-3.1.56/examples/save-to-disk/jsfiddle/demo.css000066400000000000000000000000641437620512100224430ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/save-to-disk/jsfiddle/demo.details000066400000000000000000000002101437620512100232710ustar00rootroot00000000000000--- name: save-to-disk description: Example of using Pion WebRTC to save video to disk in an IVF container authors: - Sean DuBois webrtc-3.1.56/examples/save-to-disk/jsfiddle/demo.html000066400000000000000000000007721437620512100226250ustar00rootroot00000000000000Browser base64 Session Description



    Golang base64 Session Description



    Video

    Logs
    webrtc-3.1.56/examples/save-to-disk/jsfiddle/demo.js000066400000000000000000000025631437620512100222750ustar00rootroot00000000000000/* eslint-env browser */ const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }) const log = msg => { document.getElementById('logs').innerHTML += msg + '
    ' } navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { document.getElementById('video1').srcObject = stream stream.getTracks().forEach(track => pc.addTrack(track, stream)) pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log) }).catch(log) pc.oniceconnectionstatechange = e => log(pc.iceConnectionState) pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySDP = () => { const browserSDP = document.getElementById('localSessionDescription') browserSDP.focus() browserSDP.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' log('Copying SDP was ' + msg) } catch (err) { log('Unable to copy SDP ' + err) } } webrtc-3.1.56/examples/save-to-disk/main.go000066400000000000000000000131651437620512100205020ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "fmt" "os" "strings" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/ivfwriter" "github.com/pion/webrtc/v3/pkg/media/oggwriter" ) func saveToDisk(i media.Writer, track *webrtc.TrackRemote) { defer func() { if err := i.Close(); err != nil { panic(err) } }() for { rtpPacket, _, err := track.ReadRTP() if err != nil { panic(err) } if err := i.WriteRTP(rtpPacket); err != nil { panic(err) } } } func main() { // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Create a MediaEngine object to configure the supported codec m := &webrtc.MediaEngine{} // Setup the codecs you want to use. // We'll use a VP8 and Opus but you can also define your own if err := m.RegisterCodec(webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, PayloadType: 96, }, webrtc.RTPCodecTypeVideo); err != nil { panic(err) } if err := m.RegisterCodec(webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, PayloadType: 111, }, webrtc.RTPCodecTypeAudio); err != nil { panic(err) } // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry // for each PeerConnection. i := &interceptor.Registry{} // Use the default set of Interceptors if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { panic(err) } // Create the API object with the MediaEngine api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)) // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := api.NewPeerConnection(config) if err != nil { panic(err) } // Allow us to receive 1 audio track, and 1 video track if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil { panic(err) } else if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil { panic(err) } oggFile, err := oggwriter.New("output.ogg", 48000, 2) if err != nil { panic(err) } ivfFile, err := ivfwriter.New("output.ivf") if err != nil { panic(err) } // Set a handler for when a new remote track starts, this handler saves buffers to disk as // an ivf file, since we could have multiple video tracks we provide a counter. // In your application this is where you would handle/process video peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { // Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval go func() { ticker := time.NewTicker(time.Second * 3) for range ticker.C { errSend := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}) if errSend != nil { fmt.Println(errSend) } } }() codec := track.Codec() if strings.EqualFold(codec.MimeType, webrtc.MimeTypeOpus) { fmt.Println("Got Opus track, saving to disk as output.opus (48 kHz, 2 channels)") saveToDisk(oggFile, track) } else if strings.EqualFold(codec.MimeType, webrtc.MimeTypeVP8) { fmt.Println("Got VP8 track, saving to disk as output.ivf") saveToDisk(ivfFile, track) } }) // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) if connectionState == webrtc.ICEConnectionStateConnected { fmt.Println("Ctrl+C the remote client to stop the demo") } else if connectionState == webrtc.ICEConnectionStateFailed { if closeErr := oggFile.Close(); closeErr != nil { panic(closeErr) } if closeErr := ivfFile.Close(); closeErr != nil { panic(closeErr) } fmt.Println("Done writing media files") // Gracefully shutdown the peer connection if closeErr := peerConnection.Close(); closeErr != nil { panic(closeErr) } os.Exit(0) } }) // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(offer) if err != nil { panic(err) } // Create answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/simulcast/000077500000000000000000000000001437620512100167175ustar00rootroot00000000000000webrtc-3.1.56/examples/simulcast/README.md000066400000000000000000000026161437620512100202030ustar00rootroot00000000000000# simulcast demonstrates of how to handle incoming track with multiple simulcast rtp streams and show all them back. The browser will not send higher quality streams unless it has the available bandwidth. You can look at the bandwidth estimation in `chrome://webrtc-internals`. It is under `VideoBwe` when `Read Stats From: Legacy non-Standard` is selected. ## Instructions ### Download simulcast ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/simulcast ``` ### Open simulcast example page [jsfiddle.net](https://jsfiddle.net/tz4d5bhj/) you should see two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`. ### Run simulcast, with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually. We will use this value in the next step. #### Linux/macOS Run `echo $BROWSER_SDP | simulcast` #### Windows 1. Paste the SessionDescription into a file. 1. Run `simulcast < my_file` ### Input simulcast's SessionDescription into your browser Copy the text that `simulcast` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle, enjoy your video! Your browser should send a simulcast track to Pion, and then all 3 incoming streams will be relayed back. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/simulcast/jsfiddle/000077500000000000000000000000001437620512100205035ustar00rootroot00000000000000webrtc-3.1.56/examples/simulcast/jsfiddle/demo.css000066400000000000000000000000641437620512100221410ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/simulcast/jsfiddle/demo.details000066400000000000000000000002551437620512100230000ustar00rootroot00000000000000--- name: simulcast description: Example of how to have Pion handle incoming track with multiple simulcast rtp streams and show all them back. authors: - Simone Gotti webrtc-3.1.56/examples/simulcast/jsfiddle/demo.html000066400000000000000000000011701437620512100223140ustar00rootroot00000000000000 Browser base64 Session Description



    Golang base64 Session Description



    Browser stream
    Video from server
    webrtc-3.1.56/examples/simulcast/jsfiddle/demo.js000066400000000000000000000047371437620512100220000ustar00rootroot00000000000000/* eslint-env browser */ // Create peer conn const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }) pc.oniceconnectionstatechange = (e) => { console.log('connection state change', pc.iceConnectionState) } pc.onicecandidate = (event) => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa( JSON.stringify(pc.localDescription) ) } } pc.onnegotiationneeded = (e) => pc .createOffer() .then((d) => pc.setLocalDescription(d)) .catch(console.error) pc.ontrack = (event) => { console.log('Got track event', event) const video = document.createElement('video') video.srcObject = event.streams[0] video.autoplay = true video.width = '500' const label = document.createElement('div') label.textContent = event.streams[0].id document.getElementById('serverVideos').appendChild(label) document.getElementById('serverVideos').appendChild(video) } navigator.mediaDevices .getUserMedia({ video: { width: { ideal: 4096 }, height: { ideal: 2160 }, frameRate: { ideal: 60, min: 10 } }, audio: false }) .then((stream) => { document.getElementById('browserVideo').srcObject = stream pc.addTransceiver(stream.getVideoTracks()[0], { direction: 'sendonly', streams: [stream], sendEncodings: [ // for firefox order matters... first high resolution, then scaled resolutions... { rid: 'f' }, { rid: 'h', scaleResolutionDownBy: 2.0 }, { rid: 'q', scaleResolutionDownBy: 4.0 } ] }) pc.addTransceiver('video') pc.addTransceiver('video') pc.addTransceiver('video') }) window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { console.log('answer', JSON.parse(atob(sd))) pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySDP = () => { const browserSDP = document.getElementById('localSessionDescription') browserSDP.focus() browserSDP.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' console.log('Copying SDP was ' + msg) } catch (err) { console.log('Unable to copy SDP ' + err) } } webrtc-3.1.56/examples/simulcast/main.go000066400000000000000000000133121437620512100201720ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "errors" "fmt" "io" "os" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func main() { // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Enable Extension Headers needed for Simulcast m := &webrtc.MediaEngine{} if err := m.RegisterDefaultCodecs(); err != nil { panic(err) } for _, extension := range []string{ "urn:ietf:params:rtp-hdrext:sdes:mid", "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", } { if err := m.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeVideo); err != nil { panic(err) } } // Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline. // This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection` // this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry // for each PeerConnection. i := &interceptor.Registry{} // Use the default set of Interceptors if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { panic(err) } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)).NewPeerConnection(config) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() outputTracks := map[string]*webrtc.TrackLocalStaticRTP{} // Create Track that we send video back to browser on outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video_q", "pion_q") if err != nil { panic(err) } outputTracks["q"] = outputTrack outputTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video_h", "pion_h") if err != nil { panic(err) } outputTracks["h"] = outputTrack outputTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video_f", "pion_f") if err != nil { panic(err) } outputTracks["f"] = outputTrack // Add this newly created track to the PeerConnection if _, err = peerConnection.AddTrack(outputTracks["q"]); err != nil { panic(err) } if _, err = peerConnection.AddTrack(outputTracks["h"]); err != nil { panic(err) } if _, err = peerConnection.AddTrack(outputTracks["f"]); err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. processRTCP := func(rtpSender *webrtc.RTPSender) { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } } for _, rtpSender := range peerConnection.GetSenders() { go processRTCP(rtpSender) } // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } // Set a handler for when a new remote track starts peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { fmt.Println("Track has started") // Start reading from all the streams and sending them to the related output track rid := track.RID() go func() { ticker := time.NewTicker(3 * time.Second) for range ticker.C { fmt.Printf("Sending pli for stream with rid: %q, ssrc: %d\n", track.RID(), track.SSRC()) if writeErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}); writeErr != nil { fmt.Println(writeErr) } } }() for { // Read RTP packets being sent to Pion packet, _, readErr := track.ReadRTP() if readErr != nil { panic(readErr) } if writeErr := outputTracks[rid].WriteRTP(packet); writeErr != nil && !errors.Is(writeErr, io.ErrClosedPipe) { panic(writeErr) } } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Create an answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete // Output the answer in base64 so we can paste it in browser fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Block forever select {} } webrtc-3.1.56/examples/swap-tracks/000077500000000000000000000000001437620512100171525ustar00rootroot00000000000000webrtc-3.1.56/examples/swap-tracks/README.md000066400000000000000000000022411437620512100204300ustar00rootroot00000000000000# swap-tracks swap-tracks demonstrates how to swap multiple incoming tracks on a single outgoing track. ## Instructions ### Download swap-tracks ``` export GO111MODULE=on go get github.com/pion/webrtc/v3/examples/swap-tracks ``` ### Open swap-tracks example page [jsfiddle.net](https://jsfiddle.net/1rx5on86/) you should see two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`. ### Run swap-tracks, with your browsers SessionDescription as stdin In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually. We will use this value in the next step. #### Linux/macOS Run `echo $BROWSER_SDP | swap-tracks` #### Windows 1. Paste the SessionDescription into a file. 1. Run `swap-tracks < my_file` ### Input swap-tracks's SessionDescription into your browser Copy the text that `swap-tracks` just emitted and copy into second text area ### Hit 'Start Session' in jsfiddle, enjoy your video! Your browser should send streams to Pion, and then a stream will be relayed back, changing every 5 seconds. Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/swap-tracks/jsfiddle/000077500000000000000000000000001437620512100207365ustar00rootroot00000000000000webrtc-3.1.56/examples/swap-tracks/jsfiddle/demo.css000066400000000000000000000000641437620512100223740ustar00rootroot00000000000000textarea { width: 500px; min-height: 75px; }webrtc-3.1.56/examples/swap-tracks/jsfiddle/demo.details000066400000000000000000000002121437620512100232240ustar00rootroot00000000000000--- name: swap-tracks description: Example of how to have Pion swap incoming tracks on a single outgoing track authors: - Chad Retz webrtc-3.1.56/examples/swap-tracks/jsfiddle/demo.html000066400000000000000000000015151437620512100225520ustar00rootroot00000000000000Browser base64 Session Description



    Golang base64 Session Description



    Browser stream 1
    Browser stream 2
    Browser stream 3
    Video from server

    webrtc-3.1.56/examples/swap-tracks/jsfiddle/demo.js000066400000000000000000000047541437620512100222320ustar00rootroot00000000000000/* eslint-env browser */ // Create peer conn const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }) pc.oniceconnectionstatechange = e => { console.debug('connection state change', pc.iceConnectionState) } pc.onicecandidate = event => { if (event.candidate === null) { document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription)) } } pc.onnegotiationneeded = e => pc.createOffer().then(d => pc.setLocalDescription(d)).catch(console.error) pc.ontrack = event => { console.log('Got track event', event) document.getElementById('serverVideo').srcObject = new MediaStream([event.track]) } const canvases = [ document.getElementById('canvasOne'), document.getElementById('canvasTwo'), document.getElementById('canvasThree') ] // Firefox requires getContext to be invoked on an HTML Canvas Element // prior to captureStream const canvasContexts = canvases.map(c => c.getContext('2d')) // Capture canvas streams and add to peer conn const streams = canvases.map(c => c.captureStream()) streams.forEach(stream => stream.getVideoTracks().forEach(track => pc.addTrack(track, stream))) // Start circles requestAnimationFrame(() => drawCircle(canvasContexts[0], '#006699', 0)) requestAnimationFrame(() => drawCircle(canvasContexts[1], '#cf635f', 0)) requestAnimationFrame(() => drawCircle(canvasContexts[2], '#46c240', 0)) function drawCircle (ctx, color, angle) { // Background ctx.clearRect(0, 0, 200, 200) ctx.fillStyle = '#eeeeee' ctx.fillRect(0, 0, 200, 200) // Draw and fill in circle ctx.beginPath() const radius = 25 + 50 * Math.abs(Math.cos(angle)) ctx.arc(100, 100, radius, 0, Math.PI * 2, false) ctx.closePath() ctx.fillStyle = color ctx.fill() // Call again requestAnimationFrame(() => drawCircle(ctx, color, angle + (Math.PI / 64))) } window.startSession = () => { const sd = document.getElementById('remoteSessionDescription').value if (sd === '') { return alert('Session Description must not be empty') } try { pc.setRemoteDescription(JSON.parse(atob(sd))) } catch (e) { alert(e) } } window.copySDP = () => { const browserSDP = document.getElementById('localSessionDescription') browserSDP.focus() browserSDP.select() try { const successful = document.execCommand('copy') const msg = successful ? 'successful' : 'unsuccessful' console.log('Copying SDP was ' + msg) } catch (err) { console.log('Unable to copy SDP ' + err) } } webrtc-3.1.56/examples/swap-tracks/main.go000066400000000000000000000131071437620512100204270ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "context" "errors" "fmt" "io" "time" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/examples/internal/signal" ) func main() { // nolint:gocognit // Everything below is the Pion WebRTC API! Thanks for using it ❤️. // Prepare the configuration config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, } // Create a new RTCPeerConnection peerConnection, err := webrtc.NewPeerConnection(config) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() // Create Track that we send video back to browser on outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion") if err != nil { panic(err) } // Add this newly created track to the PeerConnection rtpSender, err := peerConnection.AddTrack(outputTrack) if err != nil { panic(err) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() // Wait for the offer to be pasted offer := webrtc.SessionDescription{} signal.Decode(signal.MustReadStdin(), &offer) // Set the remote SessionDescription err = peerConnection.SetRemoteDescription(offer) if err != nil { panic(err) } // Which track is currently being handled currTrack := 0 // The total number of tracks trackCount := 0 // The channel of packets with a bit of buffer packets := make(chan *rtp.Packet, 60) // Set a handler for when a new remote track starts peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { fmt.Printf("Track has started, of type %d: %s \n", track.PayloadType(), track.Codec().MimeType) trackNum := trackCount trackCount++ // The last timestamp so that we can change the packet to only be the delta var lastTimestamp uint32 // Whether this track is the one currently sending to the channel (on change // of this we send a PLI to have the entire picture updated) var isCurrTrack bool for { // Read RTP packets being sent to Pion rtp, _, readErr := track.ReadRTP() if readErr != nil { panic(readErr) } // Change the timestamp to only be the delta oldTimestamp := rtp.Timestamp if lastTimestamp == 0 { rtp.Timestamp = 0 } else { rtp.Timestamp -= lastTimestamp } lastTimestamp = oldTimestamp // Check if this is the current track if currTrack == trackNum { // If just switched to this track, send PLI to get picture refresh if !isCurrTrack { isCurrTrack = true if writeErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(track.SSRC())}}); writeErr != nil { fmt.Println(writeErr) } } packets <- rtp } else { isCurrTrack = false } } }) ctx, done := context.WithCancel(context.Background()) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. done() } }) // Create an answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { panic(err) } // Create channel that is blocked until ICE Gathering is complete gatherComplete := webrtc.GatheringCompletePromise(peerConnection) // Sets the LocalDescription, and starts our UDP listeners err = peerConnection.SetLocalDescription(answer) if err != nil { panic(err) } // Block until ICE Gathering is complete, disabling trickle ICE // we do this because we only can exchange one signaling message // in a production application you should exchange ICE Candidates via OnICECandidate <-gatherComplete fmt.Println(signal.Encode(*peerConnection.LocalDescription())) // Asynchronously take all packets in the channel and write them out to our // track go func() { var currTimestamp uint32 for i := uint16(0); ; i++ { packet := <-packets // Timestamp on the packet is really a diff, so add it to current currTimestamp += packet.Timestamp packet.Timestamp = currTimestamp // Keep an increasing sequence number packet.SequenceNumber = i // Write out the packet, ignoring closed pipe if nobody is listening if err := outputTrack.WriteRTP(packet); err != nil { if errors.Is(err, io.ErrClosedPipe) { // The peerConnection has been closed. return } panic(err) } } }() // Wait for connection, then rotate the track every 5s fmt.Printf("Waiting for connection\n") for { select { case <-ctx.Done(): return default: } // We haven't gotten any tracks yet if trackCount == 0 { continue } fmt.Printf("Waiting 5 seconds then changing...\n") time.Sleep(5 * time.Second) if currTrack == trackCount-1 { currTrack = 0 } else { currTrack++ } fmt.Printf("Switched to track #%v\n", currTrack+1) } } webrtc-3.1.56/examples/trickle-ice/000077500000000000000000000000001437620512100171065ustar00rootroot00000000000000webrtc-3.1.56/examples/trickle-ice/README.md000066400000000000000000000017461437620512100203750ustar00rootroot00000000000000# trickle-ice trickle-ice demonstrates Pion WebRTC's Trickle ICE APIs. ICE is the subsystem WebRTC uses to establish connectivity. Trickle ICE is the process of sharing addresses as soon as they are gathered. This parallelizes establishing a connection with a remote peer and starting sessions with TURN servers. Using Trickle ICE can dramatically reduce the amount of time it takes to establish a WebRTC connection. Trickle ICE isn't mandatory to use, but highly recommended. ## Instructions ### Download trickle-ice This example requires you to clone the repo since it is serving static HTML. ``` mkdir -p $GOPATH/src/github.com/pion cd $GOPATH/src/github.com/pion git clone https://github.com/pion/webrtc.git cd webrtc/examples/trickle-ice ``` ### Run trickle-ice Execute `go run *.go` ### Open the Web UI Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. ## Note Congrats, you have used Pion WebRTC! Now start building something cool webrtc-3.1.56/examples/trickle-ice/index.html000066400000000000000000000030511437620512100211020ustar00rootroot00000000000000 trickle-ice

    ICE Connection States


    Inbound DataChannel Messages

    webrtc-3.1.56/examples/trickle-ice/main.go000066400000000000000000000056451437620512100203730ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "net/http" "time" "github.com/pion/webrtc/v3" "golang.org/x/net/websocket" ) // websocketServer is called for every new inbound WebSocket func websocketServer(ws *websocket.Conn) { // nolint:gocognit // Create a new RTCPeerConnection peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{}) if err != nil { panic(err) } // When Pion gathers a new ICE Candidate send it to the client. This is how // ice trickle is implemented. Everytime we have a new candidate available we send // it as soon as it is ready. We don't wait to emit a Offer/Answer until they are // all available peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) { if c == nil { return } outbound, marshalErr := json.Marshal(c.ToJSON()) if marshalErr != nil { panic(marshalErr) } if _, err = ws.Write(outbound); err != nil { panic(err) } }) // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String()) }) // Send the current time via a DataChannel to the remote peer every 3 seconds peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { d.OnOpen(func() { for range time.Tick(time.Second * 3) { if err = d.SendText(time.Now().String()); err != nil { panic(err) } } }) }) buf := make([]byte, 1500) for { // Read each inbound WebSocket Message n, err := ws.Read(buf) if err != nil { panic(err) } // Unmarshal each inbound WebSocket message var ( candidate webrtc.ICECandidateInit offer webrtc.SessionDescription ) switch { // Attempt to unmarshal as a SessionDescription. If the SDP field is empty // assume it is not one. case json.Unmarshal(buf[:n], &offer) == nil && offer.SDP != "": if err = peerConnection.SetRemoteDescription(offer); err != nil { panic(err) } answer, answerErr := peerConnection.CreateAnswer(nil) if answerErr != nil { panic(answerErr) } if err = peerConnection.SetLocalDescription(answer); err != nil { panic(err) } outbound, marshalErr := json.Marshal(answer) if marshalErr != nil { panic(marshalErr) } if _, err = ws.Write(outbound); err != nil { panic(err) } // Attempt to unmarshal as a ICECandidateInit. If the candidate field is empty // assume it is not one. case json.Unmarshal(buf[:n], &candidate) == nil && candidate.Candidate != "": if err = peerConnection.AddICECandidate(candidate); err != nil { panic(err) } default: panic("Unknown message") } } } func main() { http.Handle("/", http.FileServer(http.Dir("."))) http.Handle("/websocket", websocket.Handler(websocketServer)) fmt.Println("Open http://localhost:8080 to access this demo") panic(http.ListenAndServe(":8080", nil)) } webrtc-3.1.56/examples/vnet/000077500000000000000000000000001437620512100156675ustar00rootroot00000000000000webrtc-3.1.56/examples/vnet/README.md000066400000000000000000000014471437620512100171540ustar00rootroot00000000000000# vnet vnet is the virtual network layer for Pion. This allows developers to simulate issues that cause issues with production WebRTC deployments. See the full documentation for vnet [here](https://github.com/pion/transport/tree/master/vnet#vnet) ## What can vnet do * Simulate different network topologies. Assert when a STUN/TURN server is actually needed. * Simulate packet loss, jitter, re-ordering. See how your application performs under adverse conditions. * Measure the total bandwidth used. Determine the total cost of running your application. * More! We would love to continue extending this to support everyones needs. ## Instructions Each directory contains a single `main.go` that aims to demonstrate a single feature of vnet. They can all be run directly, and require no additional setup. webrtc-3.1.56/examples/vnet/show-network-usage/000077500000000000000000000000001437620512100214405ustar00rootroot00000000000000webrtc-3.1.56/examples/vnet/show-network-usage/main.go000066400000000000000000000154241437620512100227210ustar00rootroot00000000000000//go:build !js // +build !js package main import ( "fmt" "log" "net" "os" "sync/atomic" "time" "github.com/pion/logging" "github.com/pion/transport/v2/vnet" "github.com/pion/webrtc/v3" ) /* VNet Configuration + - - - - - - - - - - - - - - - - - - - - - - - + VNet | +-------------------------------------------+ | | wan:vnet.Router | | +---------+----------------------+----------+ | | | | +---------+----------+ +---------+----------+ | | offerVNet:vnet.Net | |answerVNet:vnet.Net | | +---------+----------+ +---------+----------+ | | | + - - - - - + - - - - - - - - - - -+- - - - - - + | | +---------+----------+ +---------+----------+ |offerPeerConnection | |answerPeerConnection| +--------------------+ +--------------------+ */ func main() { var inboundBytes int32 // for offerPeerConnection var outboundBytes int32 // for offerPeerConnection // Create a root router wan, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "1.2.3.0/24", LoggerFactory: logging.NewDefaultLoggerFactory(), }) panicIfError(err) // Add a filter that monitors the traffic on the router wan.AddChunkFilter(func(c vnet.Chunk) bool { netType := c.SourceAddr().Network() if netType == "udp" { dstAddr := c.DestinationAddr().String() host, _, err2 := net.SplitHostPort(dstAddr) panicIfError(err2) if host == "1.2.3.4" { // c.UserData() returns a []byte of UDP payload atomic.AddInt32(&inboundBytes, int32(len(c.UserData()))) } srcAddr := c.SourceAddr().String() host, _, err2 = net.SplitHostPort(srcAddr) panicIfError(err2) if host == "1.2.3.4" { // c.UserData() returns a []byte of UDP payload atomic.AddInt32(&outboundBytes, int32(len(c.UserData()))) } } return true }) // Log throughput every 3 seconds go func() { duration := 2 * time.Second for { time.Sleep(duration) inBytes := atomic.SwapInt32(&inboundBytes, 0) // read & reset outBytes := atomic.SwapInt32(&outboundBytes, 0) // read & reset inboundThroughput := float64(inBytes) / duration.Seconds() outboundThroughput := float64(outBytes) / duration.Seconds() log.Printf("inbound throughput : %.01f [Byte/s]\n", inboundThroughput) log.Printf("outbound throughput: %.01f [Byte/s]\n", outboundThroughput) } }() // Create a network interface for offerer offerVNet, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"1.2.3.4"}, }) panicIfError(err) // Add the network interface to the router panicIfError(wan.AddNet(offerVNet)) offerSettingEngine := webrtc.SettingEngine{} offerSettingEngine.SetVNet(offerVNet) offerAPI := webrtc.NewAPI(webrtc.WithSettingEngine(offerSettingEngine)) // Create a network interface for answerer answerVNet, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"1.2.3.5"}, }) panicIfError(err) // Add the network interface to the router panicIfError(wan.AddNet(answerVNet)) answerSettingEngine := webrtc.SettingEngine{} answerSettingEngine.SetVNet(answerVNet) answerAPI := webrtc.NewAPI(webrtc.WithSettingEngine(answerSettingEngine)) // Start the virtual network by calling Start() on the root router panicIfError(wan.Start()) offerPeerConnection, err := offerAPI.NewPeerConnection(webrtc.Configuration{}) panicIfError(err) defer func() { if cErr := offerPeerConnection.Close(); cErr != nil { fmt.Printf("cannot close offerPeerConnection: %v\n", cErr) } }() answerPeerConnection, err := answerAPI.NewPeerConnection(webrtc.Configuration{}) panicIfError(err) defer func() { if cErr := answerPeerConnection.Close(); cErr != nil { fmt.Printf("cannot close answerPeerConnection: %v\n", cErr) } }() // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected offerPeerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s (offerer)\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected answerPeerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s (answerer)\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate // send it to the other peer answerPeerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { if i != nil { panicIfError(offerPeerConnection.AddICECandidate(i.ToJSON())) } }) // Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate // send it to the other peer offerPeerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { if i != nil { panicIfError(answerPeerConnection.AddICECandidate(i.ToJSON())) } }) offerDataChannel, err := offerPeerConnection.CreateDataChannel("label", nil) panicIfError(err) msgSendLoop := func(dc *webrtc.DataChannel, interval time.Duration) { for { time.Sleep(interval) panicIfError(dc.SendText("My DataChannel Message")) } } offerDataChannel.OnOpen(func() { // Send test from offerer every 100 msec msgSendLoop(offerDataChannel, 100*time.Millisecond) }) answerPeerConnection.OnDataChannel(func(answerDataChannel *webrtc.DataChannel) { answerDataChannel.OnOpen(func() { // Send test from answerer every 200 msec msgSendLoop(answerDataChannel, 200*time.Millisecond) }) }) offer, err := offerPeerConnection.CreateOffer(nil) panicIfError(err) panicIfError(offerPeerConnection.SetLocalDescription(offer)) panicIfError(answerPeerConnection.SetRemoteDescription(offer)) answer, err := answerPeerConnection.CreateAnswer(nil) panicIfError(err) panicIfError(answerPeerConnection.SetLocalDescription(answer)) panicIfError(offerPeerConnection.SetRemoteDescription(answer)) // Block forever select {} } func panicIfError(err error) { if err != nil { panic(err) } } webrtc-3.1.56/gathering_complete_promise.go000066400000000000000000000021011437620512100210140ustar00rootroot00000000000000package webrtc import ( "context" ) // GatheringCompletePromise is a Pion specific helper function that returns a channel that is closed when gathering is complete. // This function may be helpful in cases where you are unable to trickle your ICE Candidates. // // It is better to not use this function, and instead trickle candidates. If you use this function you will see longer connection startup times. // When the call is connected you will see no impact however. func GatheringCompletePromise(pc *PeerConnection) (gatherComplete <-chan struct{}) { gatheringComplete, done := context.WithCancel(context.Background()) // It's possible to miss the GatherComplete event since setGatherCompleteHandler is an atomic operation and the // promise might have been created after the gathering is finished. Therefore, we need to check if the ICE gathering // state has changed to complete so that we don't block the caller forever. pc.setGatherCompleteHandler(func() { done() }) if pc.ICEGatheringState() == ICEGatheringStateComplete { done() } return gatheringComplete.Done() } webrtc-3.1.56/gathering_complete_promise_example_test.go000066400000000000000000000032771437620512100236050ustar00rootroot00000000000000package webrtc import ( "fmt" "strings" ) // ExampleGatheringCompletePromise demonstrates how to implement // non-trickle ICE in Pion, an older form of ICE that does not require an // asynchronous side channel between peers: negotiation is just a single // offer-answer exchange. It works by explicitly waiting for all local // ICE candidates to have been gathered before sending an offer to the peer. func ExampleGatheringCompletePromise() { // create a peer connection pc, err := NewPeerConnection(Configuration{}) if err != nil { panic(err) } defer func() { closeErr := pc.Close() if closeErr != nil { panic(closeErr) } }() // add at least one transceiver to the peer connection, or nothing // interesting will happen. This could use pc.AddTrack instead. _, err = pc.AddTransceiverFromKind(RTPCodecTypeVideo) if err != nil { panic(err) } // create a first offer that does not contain any local candidates offer, err := pc.CreateOffer(nil) if err != nil { panic(err) } // gatherComplete is a channel that will be closed when // the gathering of local candidates is complete. gatherComplete := GatheringCompletePromise(pc) // apply the offer err = pc.SetLocalDescription(offer) if err != nil { panic(err) } // wait for gathering of local candidates to complete <-gatherComplete // compute the local offer again offer2 := pc.LocalDescription() // this second offer contains all candidates, and may be sent to // the peer with no need for further communication. In this // example, we simply check that it contains at least one // candidate. hasCandidate := strings.Contains(offer2.SDP, "\na=candidate:") if hasCandidate { fmt.Println("Ok!") } // Output: Ok! } webrtc-3.1.56/go.mod000066400000000000000000000012521437620512100142030ustar00rootroot00000000000000module github.com/pion/webrtc/v3 go 1.13 require ( github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.17.0 // indirect github.com/pion/datachannel v1.5.5 github.com/pion/dtls/v2 v2.2.6 github.com/pion/ice/v2 v2.3.1 github.com/pion/interceptor v0.1.12 github.com/pion/logging v0.2.2 github.com/pion/randutil v0.1.0 github.com/pion/rtcp v1.2.10 github.com/pion/rtp v1.7.13 github.com/pion/sctp v1.8.6 github.com/pion/sdp/v3 v3.0.6 github.com/pion/srtp/v2 v2.0.12 github.com/pion/transport/v2 v2.0.2 github.com/sclevine/agouti v3.0.0+incompatible github.com/stretchr/testify v1.8.1 golang.org/x/crypto v0.6.0 // indirect golang.org/x/net v0.7.0 ) webrtc-3.1.56/go.sum000066400000000000000000000402721437620512100142350ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4= github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY= github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc= github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8= github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8= github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U= github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI= github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY= github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y= github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk= github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw= github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg= github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0= github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI= github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs= github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54= github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sclevine/agouti v3.0.0+incompatible h1:8IBJS6PWz3uTlMP3YBIR5f+KAldcGuOeFkFbUWfBgK4= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= webrtc-3.1.56/ice_go.go000066400000000000000000000005331437620512100146520ustar00rootroot00000000000000//go:build !js // +build !js package webrtc // NewICETransport creates a new NewICETransport. // This constructor is part of the ORTC API. It is not // meant to be used together with the basic WebRTC API. func (api *API) NewICETransport(gatherer *ICEGatherer) *ICETransport { return NewICETransport(gatherer, api.settingEngine.LoggerFactory) } webrtc-3.1.56/icecandidate.go000066400000000000000000000107061437620512100160250ustar00rootroot00000000000000package webrtc import ( "fmt" "github.com/pion/ice/v2" ) // ICECandidate represents a ice candidate type ICECandidate struct { statsID string Foundation string `json:"foundation"` Priority uint32 `json:"priority"` Address string `json:"address"` Protocol ICEProtocol `json:"protocol"` Port uint16 `json:"port"` Typ ICECandidateType `json:"type"` Component uint16 `json:"component"` RelatedAddress string `json:"relatedAddress"` RelatedPort uint16 `json:"relatedPort"` TCPType string `json:"tcpType"` } // Conversion for package ice func newICECandidatesFromICE(iceCandidates []ice.Candidate) ([]ICECandidate, error) { candidates := []ICECandidate{} for _, i := range iceCandidates { c, err := newICECandidateFromICE(i) if err != nil { return nil, err } candidates = append(candidates, c) } return candidates, nil } func newICECandidateFromICE(i ice.Candidate) (ICECandidate, error) { typ, err := convertTypeFromICE(i.Type()) if err != nil { return ICECandidate{}, err } protocol, err := NewICEProtocol(i.NetworkType().NetworkShort()) if err != nil { return ICECandidate{}, err } c := ICECandidate{ statsID: i.ID(), Foundation: i.Foundation(), Priority: i.Priority(), Address: i.Address(), Protocol: protocol, Port: uint16(i.Port()), Component: i.Component(), Typ: typ, TCPType: i.TCPType().String(), } if i.RelatedAddress() != nil { c.RelatedAddress = i.RelatedAddress().Address c.RelatedPort = uint16(i.RelatedAddress().Port) } return c, nil } func (c ICECandidate) toICE() (ice.Candidate, error) { candidateID := c.statsID switch c.Typ { case ICECandidateTypeHost: config := ice.CandidateHostConfig{ CandidateID: candidateID, Network: c.Protocol.String(), Address: c.Address, Port: int(c.Port), Component: c.Component, TCPType: ice.NewTCPType(c.TCPType), Foundation: c.Foundation, Priority: c.Priority, } return ice.NewCandidateHost(&config) case ICECandidateTypeSrflx: config := ice.CandidateServerReflexiveConfig{ CandidateID: candidateID, Network: c.Protocol.String(), Address: c.Address, Port: int(c.Port), Component: c.Component, Foundation: c.Foundation, Priority: c.Priority, RelAddr: c.RelatedAddress, RelPort: int(c.RelatedPort), } return ice.NewCandidateServerReflexive(&config) case ICECandidateTypePrflx: config := ice.CandidatePeerReflexiveConfig{ CandidateID: candidateID, Network: c.Protocol.String(), Address: c.Address, Port: int(c.Port), Component: c.Component, Foundation: c.Foundation, Priority: c.Priority, RelAddr: c.RelatedAddress, RelPort: int(c.RelatedPort), } return ice.NewCandidatePeerReflexive(&config) case ICECandidateTypeRelay: config := ice.CandidateRelayConfig{ CandidateID: candidateID, Network: c.Protocol.String(), Address: c.Address, Port: int(c.Port), Component: c.Component, Foundation: c.Foundation, Priority: c.Priority, RelAddr: c.RelatedAddress, RelPort: int(c.RelatedPort), } return ice.NewCandidateRelay(&config) default: return nil, fmt.Errorf("%w: %s", errICECandidateTypeUnknown, c.Typ) } } func convertTypeFromICE(t ice.CandidateType) (ICECandidateType, error) { switch t { case ice.CandidateTypeHost: return ICECandidateTypeHost, nil case ice.CandidateTypeServerReflexive: return ICECandidateTypeSrflx, nil case ice.CandidateTypePeerReflexive: return ICECandidateTypePrflx, nil case ice.CandidateTypeRelay: return ICECandidateTypeRelay, nil default: return ICECandidateType(t), fmt.Errorf("%w: %s", errICECandidateTypeUnknown, t) } } func (c ICECandidate) String() string { ic, err := c.toICE() if err != nil { return fmt.Sprintf("%#v failed to convert to ICE: %s", c, err) } return ic.String() } // ToJSON returns an ICECandidateInit // as indicated by the spec https://w3c.github.io/webrtc-pc/#dom-rtcicecandidate-tojson func (c ICECandidate) ToJSON() ICECandidateInit { zeroVal := uint16(0) emptyStr := "" candidateStr := "" candidate, err := c.toICE() if err == nil { candidateStr = candidate.Marshal() } return ICECandidateInit{ Candidate: fmt.Sprintf("candidate:%s", candidateStr), SDPMid: &emptyStr, SDPMLineIndex: &zeroVal, } } webrtc-3.1.56/icecandidate_test.go000066400000000000000000000111661437620512100170650ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/pion/ice/v2" "github.com/stretchr/testify/assert" ) func TestICECandidate_Convert(t *testing.T) { testCases := []struct { native ICECandidate expectedType ice.CandidateType expectedNetwork string expectedAddress string expectedPort int expectedComponent uint16 expectedRelatedAddress *ice.CandidateRelatedAddress }{ { ICECandidate{ Foundation: "foundation", Priority: 128, Address: "1.0.0.1", Protocol: ICEProtocolUDP, Port: 1234, Typ: ICECandidateTypeHost, Component: 1, }, ice.CandidateTypeHost, "udp", "1.0.0.1", 1234, 1, nil, }, { ICECandidate{ Foundation: "foundation", Priority: 128, Address: "::1", Protocol: ICEProtocolUDP, Port: 1234, Typ: ICECandidateTypeSrflx, Component: 1, RelatedAddress: "1.0.0.1", RelatedPort: 4321, }, ice.CandidateTypeServerReflexive, "udp", "::1", 1234, 1, &ice.CandidateRelatedAddress{ Address: "1.0.0.1", Port: 4321, }, }, { ICECandidate{ Foundation: "foundation", Priority: 128, Address: "::1", Protocol: ICEProtocolUDP, Port: 1234, Typ: ICECandidateTypePrflx, Component: 1, RelatedAddress: "1.0.0.1", RelatedPort: 4321, }, ice.CandidateTypePeerReflexive, "udp", "::1", 1234, 1, &ice.CandidateRelatedAddress{ Address: "1.0.0.1", Port: 4321, }, }, } for i, testCase := range testCases { var expectedICE ice.Candidate var err error switch testCase.expectedType { // nolint:exhaustive case ice.CandidateTypeHost: config := ice.CandidateHostConfig{ Network: testCase.expectedNetwork, Address: testCase.expectedAddress, Port: testCase.expectedPort, Component: testCase.expectedComponent, Foundation: "foundation", Priority: 128, } expectedICE, err = ice.NewCandidateHost(&config) case ice.CandidateTypeServerReflexive: config := ice.CandidateServerReflexiveConfig{ Network: testCase.expectedNetwork, Address: testCase.expectedAddress, Port: testCase.expectedPort, Component: testCase.expectedComponent, Foundation: "foundation", Priority: 128, RelAddr: testCase.expectedRelatedAddress.Address, RelPort: testCase.expectedRelatedAddress.Port, } expectedICE, err = ice.NewCandidateServerReflexive(&config) case ice.CandidateTypePeerReflexive: config := ice.CandidatePeerReflexiveConfig{ Network: testCase.expectedNetwork, Address: testCase.expectedAddress, Port: testCase.expectedPort, Component: testCase.expectedComponent, Foundation: "foundation", Priority: 128, RelAddr: testCase.expectedRelatedAddress.Address, RelPort: testCase.expectedRelatedAddress.Port, } expectedICE, err = ice.NewCandidatePeerReflexive(&config) } assert.NoError(t, err) // first copy the candidate ID so it matches the new one testCase.native.statsID = expectedICE.ID() actualICE, err := testCase.native.toICE() assert.NoError(t, err) assert.Equal(t, expectedICE, actualICE, "testCase: %d ice not equal %v", i, actualICE) } } func TestConvertTypeFromICE(t *testing.T) { t.Run("host", func(t *testing.T) { ct, err := convertTypeFromICE(ice.CandidateTypeHost) if err != nil { t.Fatal("failed coverting ice.CandidateTypeHost") } if ct != ICECandidateTypeHost { t.Fatal("should be converted to ICECandidateTypeHost") } }) t.Run("srflx", func(t *testing.T) { ct, err := convertTypeFromICE(ice.CandidateTypeServerReflexive) if err != nil { t.Fatal("failed coverting ice.CandidateTypeServerReflexive") } if ct != ICECandidateTypeSrflx { t.Fatal("should be converted to ICECandidateTypeSrflx") } }) t.Run("prflx", func(t *testing.T) { ct, err := convertTypeFromICE(ice.CandidateTypePeerReflexive) if err != nil { t.Fatal("failed coverting ice.CandidateTypePeerReflexive") } if ct != ICECandidateTypePrflx { t.Fatal("should be converted to ICECandidateTypePrflx") } }) } func TestICECandidate_ToJSON(t *testing.T) { candidate := ICECandidate{ Foundation: "foundation", Priority: 128, Address: "1.0.0.1", Protocol: ICEProtocolUDP, Port: 1234, Typ: ICECandidateTypeHost, Component: 1, } candidateInit := candidate.ToJSON() assert.Equal(t, uint16(0), *candidateInit.SDPMLineIndex) assert.Equal(t, "candidate:foundation 1 udp 128 1.0.0.1 1234 typ host", candidateInit.Candidate) } webrtc-3.1.56/icecandidateinit.go000066400000000000000000000004451437620512100167100ustar00rootroot00000000000000package webrtc // ICECandidateInit is used to serialize ice candidates type ICECandidateInit struct { Candidate string `json:"candidate"` SDPMid *string `json:"sdpMid"` SDPMLineIndex *uint16 `json:"sdpMLineIndex"` UsernameFragment *string `json:"usernameFragment"` } webrtc-3.1.56/icecandidateinit_test.go000066400000000000000000000023241437620512100177450ustar00rootroot00000000000000package webrtc import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" ) func TestICECandidateInit_Serialization(t *testing.T) { tt := []struct { candidate ICECandidateInit serialized string }{ {ICECandidateInit{ Candidate: "candidate:abc123", SDPMid: refString("0"), SDPMLineIndex: refUint16(0), UsernameFragment: refString("def"), }, `{"candidate":"candidate:abc123","sdpMid":"0","sdpMLineIndex":0,"usernameFragment":"def"}`}, {ICECandidateInit{ Candidate: "candidate:abc123", }, `{"candidate":"candidate:abc123","sdpMid":null,"sdpMLineIndex":null,"usernameFragment":null}`}, } for i, tc := range tt { b, err := json.Marshal(tc.candidate) if err != nil { t.Errorf("Failed to marshal %d: %v", i, err) } actualSerialized := string(b) if actualSerialized != tc.serialized { t.Errorf("%d expected %s got %s", i, tc.serialized, actualSerialized) } var actual ICECandidateInit err = json.Unmarshal(b, &actual) if err != nil { t.Errorf("Failed to unmarshal %d: %v", i, err) } assert.Equal(t, tc.candidate, actual, "should match") } } func refString(s string) *string { return &s } func refUint16(i uint16) *uint16 { return &i } webrtc-3.1.56/icecandidatepair.go000066400000000000000000000013671437620512100167040ustar00rootroot00000000000000package webrtc import "fmt" // ICECandidatePair represents an ICE Candidate pair type ICECandidatePair struct { statsID string Local *ICECandidate Remote *ICECandidate } func newICECandidatePairStatsID(localID, remoteID string) string { return fmt.Sprintf("%s-%s", localID, remoteID) } func (p *ICECandidatePair) String() string { return fmt.Sprintf("(local) %s <-> (remote) %s", p.Local, p.Remote) } // NewICECandidatePair returns an initialized *ICECandidatePair // for the given pair of ICECandidate instances func NewICECandidatePair(local, remote *ICECandidate) *ICECandidatePair { statsID := newICECandidatePairStatsID(local.statsID, remote.statsID) return &ICECandidatePair{ statsID: statsID, Local: local, Remote: remote, } } webrtc-3.1.56/icecandidatetype.go000066400000000000000000000060121437620512100167220ustar00rootroot00000000000000package webrtc import ( "fmt" "github.com/pion/ice/v2" ) // ICECandidateType represents the type of the ICE candidate used. type ICECandidateType int const ( // ICECandidateTypeHost indicates that the candidate is of Host type as // described in https://tools.ietf.org/html/rfc8445#section-5.1.1.1. A // candidate obtained by binding to a specific port from an IP address on // the host. This includes IP addresses on physical interfaces and logical // ones, such as ones obtained through VPNs. ICECandidateTypeHost ICECandidateType = iota + 1 // ICECandidateTypeSrflx indicates the the candidate is of Server // Reflexive type as described // https://tools.ietf.org/html/rfc8445#section-5.1.1.2. A candidate type // whose IP address and port are a binding allocated by a NAT for an ICE // agent after it sends a packet through the NAT to a server, such as a // STUN server. ICECandidateTypeSrflx // ICECandidateTypePrflx indicates that the candidate is of Peer // Reflexive type. A candidate type whose IP address and port are a binding // allocated by a NAT for an ICE agent after it sends a packet through the // NAT to its peer. ICECandidateTypePrflx // ICECandidateTypeRelay indicates the the candidate is of Relay type as // described in https://tools.ietf.org/html/rfc8445#section-5.1.1.2. A // candidate type obtained from a relay server, such as a TURN server. ICECandidateTypeRelay ) // This is done this way because of a linter. const ( iceCandidateTypeHostStr = "host" iceCandidateTypeSrflxStr = "srflx" iceCandidateTypePrflxStr = "prflx" iceCandidateTypeRelayStr = "relay" ) // NewICECandidateType takes a string and converts it into ICECandidateType func NewICECandidateType(raw string) (ICECandidateType, error) { switch raw { case iceCandidateTypeHostStr: return ICECandidateTypeHost, nil case iceCandidateTypeSrflxStr: return ICECandidateTypeSrflx, nil case iceCandidateTypePrflxStr: return ICECandidateTypePrflx, nil case iceCandidateTypeRelayStr: return ICECandidateTypeRelay, nil default: return ICECandidateType(Unknown), fmt.Errorf("%w: %s", errICECandidateTypeUnknown, raw) } } func (t ICECandidateType) String() string { switch t { case ICECandidateTypeHost: return iceCandidateTypeHostStr case ICECandidateTypeSrflx: return iceCandidateTypeSrflxStr case ICECandidateTypePrflx: return iceCandidateTypePrflxStr case ICECandidateTypeRelay: return iceCandidateTypeRelayStr default: return ErrUnknownType.Error() } } func getCandidateType(candidateType ice.CandidateType) (ICECandidateType, error) { switch candidateType { case ice.CandidateTypeHost: return ICECandidateTypeHost, nil case ice.CandidateTypeServerReflexive: return ICECandidateTypeSrflx, nil case ice.CandidateTypePeerReflexive: return ICECandidateTypePrflx, nil case ice.CandidateTypeRelay: return ICECandidateTypeRelay, nil default: // NOTE: this should never happen[tm] err := fmt.Errorf("%w: %s", errICEInvalidConvertCandidateType, candidateType.String()) return ICECandidateType(Unknown), err } } webrtc-3.1.56/icecandidatetype_test.go000066400000000000000000000022421437620512100177620ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestICECandidateType(t *testing.T) { testCases := []struct { typeString string shouldFail bool expectedType ICECandidateType }{ {unknownStr, true, ICECandidateType(Unknown)}, {"host", false, ICECandidateTypeHost}, {"srflx", false, ICECandidateTypeSrflx}, {"prflx", false, ICECandidateTypePrflx}, {"relay", false, ICECandidateTypeRelay}, } for i, testCase := range testCases { actual, err := NewICECandidateType(testCase.typeString) if (err != nil) != testCase.shouldFail { t.Error(err) } assert.Equal(t, testCase.expectedType, actual, "testCase: %d %v", i, testCase, ) } } func TestICECandidateType_String(t *testing.T) { testCases := []struct { cType ICECandidateType expectedString string }{ {ICECandidateType(Unknown), unknownStr}, {ICECandidateTypeHost, "host"}, {ICECandidateTypeSrflx, "srflx"}, {ICECandidateTypePrflx, "prflx"}, {ICECandidateTypeRelay, "relay"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.cType.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/icecomponent.go000066400000000000000000000024051437620512100161100ustar00rootroot00000000000000package webrtc // ICEComponent describes if the ice transport is used for RTP // (or RTCP multiplexing). type ICEComponent int const ( // ICEComponentRTP indicates that the ICE Transport is used for RTP (or // RTCP multiplexing), as defined in // https://tools.ietf.org/html/rfc5245#section-4.1.1.1. Protocols // multiplexed with RTP (e.g. data channel) share its component ID. This // represents the component-id value 1 when encoded in candidate-attribute. ICEComponentRTP ICEComponent = iota + 1 // ICEComponentRTCP indicates that the ICE Transport is used for RTCP as // defined by https://tools.ietf.org/html/rfc5245#section-4.1.1.1. This // represents the component-id value 2 when encoded in candidate-attribute. ICEComponentRTCP ) // This is done this way because of a linter. const ( iceComponentRTPStr = "rtp" iceComponentRTCPStr = "rtcp" ) func newICEComponent(raw string) ICEComponent { switch raw { case iceComponentRTPStr: return ICEComponentRTP case iceComponentRTCPStr: return ICEComponentRTCP default: return ICEComponent(Unknown) } } func (t ICEComponent) String() string { switch t { case ICEComponentRTP: return iceComponentRTPStr case ICEComponentRTCP: return iceComponentRTCPStr default: return ErrUnknownType.Error() } } webrtc-3.1.56/icecomponent_test.go000066400000000000000000000015451437620512100171530ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestICEComponent(t *testing.T) { testCases := []struct { componentString string expectedComponent ICEComponent }{ {unknownStr, ICEComponent(Unknown)}, {"rtp", ICEComponentRTP}, {"rtcp", ICEComponentRTCP}, } for i, testCase := range testCases { assert.Equal(t, newICEComponent(testCase.componentString), testCase.expectedComponent, "testCase: %d %v", i, testCase, ) } } func TestICEComponent_String(t *testing.T) { testCases := []struct { state ICEComponent expectedString string }{ {ICEComponent(Unknown), unknownStr}, {ICEComponentRTP, "rtp"}, {ICEComponentRTCP, "rtcp"}, } for i, testCase := range testCases { assert.Equal(t, testCase.state.String(), testCase.expectedString, "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/iceconnectionstate.go000066400000000000000000000061551437620512100173140ustar00rootroot00000000000000package webrtc // ICEConnectionState indicates signaling state of the ICE Connection. type ICEConnectionState int const ( // ICEConnectionStateNew indicates that any of the ICETransports are // in the "new" state and none of them are in the "checking", "disconnected" // or "failed" state, or all ICETransports are in the "closed" state, or // there are no transports. ICEConnectionStateNew ICEConnectionState = iota + 1 // ICEConnectionStateChecking indicates that any of the ICETransports // are in the "checking" state and none of them are in the "disconnected" // or "failed" state. ICEConnectionStateChecking // ICEConnectionStateConnected indicates that all ICETransports are // in the "connected", "completed" or "closed" state and at least one of // them is in the "connected" state. ICEConnectionStateConnected // ICEConnectionStateCompleted indicates that all ICETransports are // in the "completed" or "closed" state and at least one of them is in the // "completed" state. ICEConnectionStateCompleted // ICEConnectionStateDisconnected indicates that any of the // ICETransports are in the "disconnected" state and none of them are // in the "failed" state. ICEConnectionStateDisconnected // ICEConnectionStateFailed indicates that any of the ICETransports // are in the "failed" state. ICEConnectionStateFailed // ICEConnectionStateClosed indicates that the PeerConnection's // isClosed is true. ICEConnectionStateClosed ) // This is done this way because of a linter. const ( iceConnectionStateNewStr = "new" iceConnectionStateCheckingStr = "checking" iceConnectionStateConnectedStr = "connected" iceConnectionStateCompletedStr = "completed" iceConnectionStateDisconnectedStr = "disconnected" iceConnectionStateFailedStr = "failed" iceConnectionStateClosedStr = "closed" ) // NewICEConnectionState takes a string and converts it to ICEConnectionState func NewICEConnectionState(raw string) ICEConnectionState { switch raw { case iceConnectionStateNewStr: return ICEConnectionStateNew case iceConnectionStateCheckingStr: return ICEConnectionStateChecking case iceConnectionStateConnectedStr: return ICEConnectionStateConnected case iceConnectionStateCompletedStr: return ICEConnectionStateCompleted case iceConnectionStateDisconnectedStr: return ICEConnectionStateDisconnected case iceConnectionStateFailedStr: return ICEConnectionStateFailed case iceConnectionStateClosedStr: return ICEConnectionStateClosed default: return ICEConnectionState(Unknown) } } func (c ICEConnectionState) String() string { switch c { case ICEConnectionStateNew: return iceConnectionStateNewStr case ICEConnectionStateChecking: return iceConnectionStateCheckingStr case ICEConnectionStateConnected: return iceConnectionStateConnectedStr case ICEConnectionStateCompleted: return iceConnectionStateCompletedStr case ICEConnectionStateDisconnected: return iceConnectionStateDisconnectedStr case ICEConnectionStateFailed: return iceConnectionStateFailedStr case ICEConnectionStateClosed: return iceConnectionStateClosedStr default: return ErrUnknownType.Error() } } webrtc-3.1.56/iceconnectionstate_test.go000066400000000000000000000025521437620512100203500ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewICEConnectionState(t *testing.T) { testCases := []struct { stateString string expectedState ICEConnectionState }{ {unknownStr, ICEConnectionState(Unknown)}, {"new", ICEConnectionStateNew}, {"checking", ICEConnectionStateChecking}, {"connected", ICEConnectionStateConnected}, {"completed", ICEConnectionStateCompleted}, {"disconnected", ICEConnectionStateDisconnected}, {"failed", ICEConnectionStateFailed}, {"closed", ICEConnectionStateClosed}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedState, NewICEConnectionState(testCase.stateString), "testCase: %d %v", i, testCase, ) } } func TestICEConnectionState_String(t *testing.T) { testCases := []struct { state ICEConnectionState expectedString string }{ {ICEConnectionState(Unknown), unknownStr}, {ICEConnectionStateNew, "new"}, {ICEConnectionStateChecking, "checking"}, {ICEConnectionStateConnected, "connected"}, {ICEConnectionStateCompleted, "completed"}, {ICEConnectionStateDisconnected, "disconnected"}, {ICEConnectionStateFailed, "failed"}, {ICEConnectionStateClosed, "closed"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.state.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/icecredentialtype.go000066400000000000000000000032201437620512100171160ustar00rootroot00000000000000package webrtc import ( "encoding/json" "fmt" ) // ICECredentialType indicates the type of credentials used to connect to // an ICE server. type ICECredentialType int const ( // ICECredentialTypePassword describes username and password based // credentials as described in https://tools.ietf.org/html/rfc5389. ICECredentialTypePassword ICECredentialType = iota // ICECredentialTypeOauth describes token based credential as described // in https://tools.ietf.org/html/rfc7635. ICECredentialTypeOauth ) // This is done this way because of a linter. const ( iceCredentialTypePasswordStr = "password" iceCredentialTypeOauthStr = "oauth" ) func newICECredentialType(raw string) (ICECredentialType, error) { switch raw { case iceCredentialTypePasswordStr: return ICECredentialTypePassword, nil case iceCredentialTypeOauthStr: return ICECredentialTypeOauth, nil default: return ICECredentialTypePassword, errInvalidICECredentialTypeString } } func (t ICECredentialType) String() string { switch t { case ICECredentialTypePassword: return iceCredentialTypePasswordStr case ICECredentialTypeOauth: return iceCredentialTypeOauthStr default: return ErrUnknownType.Error() } } // UnmarshalJSON parses the JSON-encoded data and stores the result func (t *ICECredentialType) UnmarshalJSON(b []byte) error { var val string if err := json.Unmarshal(b, &val); err != nil { return err } tmp, err := newICECredentialType(val) if err != nil { return fmt.Errorf("%w: (%s)", err, val) } *t = tmp return nil } // MarshalJSON returns the JSON encoding func (t ICECredentialType) MarshalJSON() ([]byte, error) { return json.Marshal(t.String()) } webrtc-3.1.56/icecredentialtype_test.go000066400000000000000000000044601437620512100201640ustar00rootroot00000000000000package webrtc import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" ) func TestNewICECredentialType(t *testing.T) { testCases := []struct { credentialTypeString string expectedCredentialType ICECredentialType }{ {"password", ICECredentialTypePassword}, {"oauth", ICECredentialTypeOauth}, } for i, testCase := range testCases { tpe, err := newICECredentialType(testCase.credentialTypeString) assert.NoError(t, err) assert.Equal(t, testCase.expectedCredentialType, tpe, "testCase: %d %v", i, testCase, ) } } func TestICECredentialType_String(t *testing.T) { testCases := []struct { credentialType ICECredentialType expectedString string }{ {ICECredentialTypePassword, "password"}, {ICECredentialTypeOauth, "oauth"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.credentialType.String(), "testCase: %d %v", i, testCase, ) } } func TestICECredentialType_new(t *testing.T) { testCases := []struct { credentialType ICECredentialType expectedString string }{ {ICECredentialTypePassword, "password"}, {ICECredentialTypeOauth, "oauth"}, } for i, testCase := range testCases { tpe, err := newICECredentialType(testCase.expectedString) assert.NoError(t, err) assert.Equal(t, tpe, testCase.credentialType, "testCase: %d %v", i, testCase, ) } } func TestICECredentialType_Json(t *testing.T) { testCases := []struct { credentialType ICECredentialType jsonRepresentation []byte }{ {ICECredentialTypePassword, []byte("\"password\"")}, {ICECredentialTypeOauth, []byte("\"oauth\"")}, } for i, testCase := range testCases { m, err := json.Marshal(testCase.credentialType) assert.NoError(t, err) assert.Equal(t, testCase.jsonRepresentation, m, "Marshal testCase: %d %v", i, testCase, ) var ct ICECredentialType err = json.Unmarshal(testCase.jsonRepresentation, &ct) assert.NoError(t, err) assert.Equal(t, testCase.credentialType, ct, "Unmarshal testCase: %d %v", i, testCase, ) } { ct := ICECredentialType(1000) err := json.Unmarshal([]byte("\"invalid\""), &ct) assert.Error(t, err) assert.Equal(t, ct, ICECredentialType(1000)) err = json.Unmarshal([]byte("\"invalid"), &ct) assert.Error(t, err) assert.Equal(t, ct, ICECredentialType(1000)) } } webrtc-3.1.56/icegatherer.go000066400000000000000000000311131437620512100157050ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "fmt" "sync" "sync/atomic" "github.com/pion/ice/v2" "github.com/pion/logging" ) // ICEGatherer gathers local host, server reflexive and relay // candidates, as well as enabling the retrieval of local Interactive // Connectivity Establishment (ICE) parameters which can be // exchanged in signaling. type ICEGatherer struct { lock sync.RWMutex log logging.LeveledLogger state ICEGathererState validatedServers []*ice.URL gatherPolicy ICETransportPolicy agent *ice.Agent onLocalCandidateHandler atomic.Value // func(candidate *ICECandidate) onStateChangeHandler atomic.Value // func(state ICEGathererState) // Used for GatheringCompletePromise onGatheringCompleteHandler atomic.Value // func() api *API } // NewICEGatherer creates a new NewICEGatherer. // This constructor is part of the ORTC API. It is not // meant to be used together with the basic WebRTC API. func (api *API) NewICEGatherer(opts ICEGatherOptions) (*ICEGatherer, error) { var validatedServers []*ice.URL if len(opts.ICEServers) > 0 { for _, server := range opts.ICEServers { url, err := server.urls() if err != nil { return nil, err } validatedServers = append(validatedServers, url...) } } return &ICEGatherer{ state: ICEGathererStateNew, gatherPolicy: opts.ICEGatherPolicy, validatedServers: validatedServers, api: api, log: api.settingEngine.LoggerFactory.NewLogger("ice"), }, nil } func (g *ICEGatherer) createAgent() error { g.lock.Lock() defer g.lock.Unlock() if g.agent != nil || g.State() != ICEGathererStateNew { return nil } candidateTypes := []ice.CandidateType{} if g.api.settingEngine.candidates.ICELite { candidateTypes = append(candidateTypes, ice.CandidateTypeHost) } else if g.gatherPolicy == ICETransportPolicyRelay { candidateTypes = append(candidateTypes, ice.CandidateTypeRelay) } var nat1To1CandiTyp ice.CandidateType switch g.api.settingEngine.candidates.NAT1To1IPCandidateType { case ICECandidateTypeHost: nat1To1CandiTyp = ice.CandidateTypeHost case ICECandidateTypeSrflx: nat1To1CandiTyp = ice.CandidateTypeServerReflexive default: nat1To1CandiTyp = ice.CandidateTypeUnspecified } mDNSMode := g.api.settingEngine.candidates.MulticastDNSMode if mDNSMode != ice.MulticastDNSModeDisabled && mDNSMode != ice.MulticastDNSModeQueryAndGather { // If enum is in state we don't recognized default to MulticastDNSModeQueryOnly mDNSMode = ice.MulticastDNSModeQueryOnly } config := &ice.AgentConfig{ Lite: g.api.settingEngine.candidates.ICELite, Urls: g.validatedServers, PortMin: g.api.settingEngine.ephemeralUDP.PortMin, PortMax: g.api.settingEngine.ephemeralUDP.PortMax, DisconnectedTimeout: g.api.settingEngine.timeout.ICEDisconnectedTimeout, FailedTimeout: g.api.settingEngine.timeout.ICEFailedTimeout, KeepaliveInterval: g.api.settingEngine.timeout.ICEKeepaliveInterval, LoggerFactory: g.api.settingEngine.LoggerFactory, CandidateTypes: candidateTypes, HostAcceptanceMinWait: g.api.settingEngine.timeout.ICEHostAcceptanceMinWait, SrflxAcceptanceMinWait: g.api.settingEngine.timeout.ICESrflxAcceptanceMinWait, PrflxAcceptanceMinWait: g.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait, RelayAcceptanceMinWait: g.api.settingEngine.timeout.ICERelayAcceptanceMinWait, InterfaceFilter: g.api.settingEngine.candidates.InterfaceFilter, IPFilter: g.api.settingEngine.candidates.IPFilter, NAT1To1IPs: g.api.settingEngine.candidates.NAT1To1IPs, NAT1To1IPCandidateType: nat1To1CandiTyp, IncludeLoopback: g.api.settingEngine.candidates.IncludeLoopbackCandidate, Net: g.api.settingEngine.net, MulticastDNSMode: mDNSMode, MulticastDNSHostName: g.api.settingEngine.candidates.MulticastDNSHostName, LocalUfrag: g.api.settingEngine.candidates.UsernameFragment, LocalPwd: g.api.settingEngine.candidates.Password, TCPMux: g.api.settingEngine.iceTCPMux, UDPMux: g.api.settingEngine.iceUDPMux, ProxyDialer: g.api.settingEngine.iceProxyDialer, } requestedNetworkTypes := g.api.settingEngine.candidates.ICENetworkTypes if len(requestedNetworkTypes) == 0 { requestedNetworkTypes = supportedNetworkTypes() } for _, typ := range requestedNetworkTypes { config.NetworkTypes = append(config.NetworkTypes, ice.NetworkType(typ)) } agent, err := ice.NewAgent(config) if err != nil { return err } g.agent = agent return nil } // Gather ICE candidates. func (g *ICEGatherer) Gather() error { if err := g.createAgent(); err != nil { return err } agent := g.getAgent() // it is possible agent had just been closed if agent == nil { return fmt.Errorf("%w: unable to gather", errICEAgentNotExist) } g.setState(ICEGathererStateGathering) if err := agent.OnCandidate(func(candidate ice.Candidate) { onLocalCandidateHandler := func(*ICECandidate) {} if handler, ok := g.onLocalCandidateHandler.Load().(func(candidate *ICECandidate)); ok && handler != nil { onLocalCandidateHandler = handler } onGatheringCompleteHandler := func() {} if handler, ok := g.onGatheringCompleteHandler.Load().(func()); ok && handler != nil { onGatheringCompleteHandler = handler } if candidate != nil { c, err := newICECandidateFromICE(candidate) if err != nil { g.log.Warnf("Failed to convert ice.Candidate: %s", err) return } onLocalCandidateHandler(&c) } else { g.setState(ICEGathererStateComplete) onGatheringCompleteHandler() onLocalCandidateHandler(nil) } }); err != nil { return err } return agent.GatherCandidates() } // Close prunes all local candidates, and closes the ports. func (g *ICEGatherer) Close() error { g.lock.Lock() defer g.lock.Unlock() if g.agent == nil { return nil } else if err := g.agent.Close(); err != nil { return err } g.agent = nil g.setState(ICEGathererStateClosed) return nil } // GetLocalParameters returns the ICE parameters of the ICEGatherer. func (g *ICEGatherer) GetLocalParameters() (ICEParameters, error) { if err := g.createAgent(); err != nil { return ICEParameters{}, err } agent := g.getAgent() // it is possible agent had just been closed if agent == nil { return ICEParameters{}, fmt.Errorf("%w: unable to get local parameters", errICEAgentNotExist) } frag, pwd, err := agent.GetLocalUserCredentials() if err != nil { return ICEParameters{}, err } return ICEParameters{ UsernameFragment: frag, Password: pwd, ICELite: false, }, nil } // GetLocalCandidates returns the sequence of valid local candidates associated with the ICEGatherer. func (g *ICEGatherer) GetLocalCandidates() ([]ICECandidate, error) { if err := g.createAgent(); err != nil { return nil, err } agent := g.getAgent() // it is possible agent had just been closed if agent == nil { return nil, fmt.Errorf("%w: unable to get local candidates", errICEAgentNotExist) } iceCandidates, err := agent.GetLocalCandidates() if err != nil { return nil, err } return newICECandidatesFromICE(iceCandidates) } // OnLocalCandidate sets an event handler which fires when a new local ICE candidate is available // Take note that the handler will be called with a nil pointer when gathering is finished. func (g *ICEGatherer) OnLocalCandidate(f func(*ICECandidate)) { g.onLocalCandidateHandler.Store(f) } // OnStateChange fires any time the ICEGatherer changes func (g *ICEGatherer) OnStateChange(f func(ICEGathererState)) { g.onStateChangeHandler.Store(f) } // State indicates the current state of the ICE gatherer. func (g *ICEGatherer) State() ICEGathererState { return atomicLoadICEGathererState(&g.state) } func (g *ICEGatherer) setState(s ICEGathererState) { atomicStoreICEGathererState(&g.state, s) if handler, ok := g.onStateChangeHandler.Load().(func(state ICEGathererState)); ok && handler != nil { handler(s) } } func (g *ICEGatherer) getAgent() *ice.Agent { g.lock.RLock() defer g.lock.RUnlock() return g.agent } func (g *ICEGatherer) collectStats(collector *statsReportCollector) { agent := g.getAgent() if agent == nil { return } collector.Collecting() go func(collector *statsReportCollector, agent *ice.Agent) { for _, candidatePairStats := range agent.GetCandidatePairsStats() { collector.Collecting() state, err := toStatsICECandidatePairState(candidatePairStats.State) if err != nil { g.log.Error(err.Error()) } pairID := newICECandidatePairStatsID(candidatePairStats.LocalCandidateID, candidatePairStats.RemoteCandidateID) stats := ICECandidatePairStats{ Timestamp: statsTimestampFrom(candidatePairStats.Timestamp), Type: StatsTypeCandidatePair, ID: pairID, // TransportID: LocalCandidateID: candidatePairStats.LocalCandidateID, RemoteCandidateID: candidatePairStats.RemoteCandidateID, State: state, Nominated: candidatePairStats.Nominated, PacketsSent: candidatePairStats.PacketsSent, PacketsReceived: candidatePairStats.PacketsReceived, BytesSent: candidatePairStats.BytesSent, BytesReceived: candidatePairStats.BytesReceived, LastPacketSentTimestamp: statsTimestampFrom(candidatePairStats.LastPacketSentTimestamp), LastPacketReceivedTimestamp: statsTimestampFrom(candidatePairStats.LastPacketReceivedTimestamp), FirstRequestTimestamp: statsTimestampFrom(candidatePairStats.FirstRequestTimestamp), LastRequestTimestamp: statsTimestampFrom(candidatePairStats.LastRequestTimestamp), LastResponseTimestamp: statsTimestampFrom(candidatePairStats.LastResponseTimestamp), TotalRoundTripTime: candidatePairStats.TotalRoundTripTime, CurrentRoundTripTime: candidatePairStats.CurrentRoundTripTime, AvailableOutgoingBitrate: candidatePairStats.AvailableOutgoingBitrate, AvailableIncomingBitrate: candidatePairStats.AvailableIncomingBitrate, CircuitBreakerTriggerCount: candidatePairStats.CircuitBreakerTriggerCount, RequestsReceived: candidatePairStats.RequestsReceived, RequestsSent: candidatePairStats.RequestsSent, ResponsesReceived: candidatePairStats.ResponsesReceived, ResponsesSent: candidatePairStats.ResponsesSent, RetransmissionsReceived: candidatePairStats.RetransmissionsReceived, RetransmissionsSent: candidatePairStats.RetransmissionsSent, ConsentRequestsSent: candidatePairStats.ConsentRequestsSent, ConsentExpiredTimestamp: statsTimestampFrom(candidatePairStats.ConsentExpiredTimestamp), } collector.Collect(stats.ID, stats) } for _, candidateStats := range agent.GetLocalCandidatesStats() { collector.Collecting() networkType, err := getNetworkType(candidateStats.NetworkType) if err != nil { g.log.Error(err.Error()) } candidateType, err := getCandidateType(candidateStats.CandidateType) if err != nil { g.log.Error(err.Error()) } stats := ICECandidateStats{ Timestamp: statsTimestampFrom(candidateStats.Timestamp), ID: candidateStats.ID, Type: StatsTypeLocalCandidate, NetworkType: networkType, IP: candidateStats.IP, Port: int32(candidateStats.Port), Protocol: networkType.Protocol(), CandidateType: candidateType, Priority: int32(candidateStats.Priority), URL: candidateStats.URL, RelayProtocol: candidateStats.RelayProtocol, Deleted: candidateStats.Deleted, } collector.Collect(stats.ID, stats) } for _, candidateStats := range agent.GetRemoteCandidatesStats() { collector.Collecting() networkType, err := getNetworkType(candidateStats.NetworkType) if err != nil { g.log.Error(err.Error()) } candidateType, err := getCandidateType(candidateStats.CandidateType) if err != nil { g.log.Error(err.Error()) } stats := ICECandidateStats{ Timestamp: statsTimestampFrom(candidateStats.Timestamp), ID: candidateStats.ID, Type: StatsTypeRemoteCandidate, NetworkType: networkType, IP: candidateStats.IP, Port: int32(candidateStats.Port), Protocol: networkType.Protocol(), CandidateType: candidateType, Priority: int32(candidateStats.Priority), URL: candidateStats.URL, RelayProtocol: candidateStats.RelayProtocol, } collector.Collect(stats.ID, stats) } collector.Done() }(collector, agent) } webrtc-3.1.56/icegatherer_test.go000066400000000000000000000063331437620512100167520ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "context" "strings" "testing" "time" "github.com/pion/ice/v2" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) func TestNewICEGatherer_Success(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 20) defer lim.Stop() report := test.CheckRoutines(t) defer report() opts := ICEGatherOptions{ ICEServers: []ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}, } gatherer, err := NewAPI().NewICEGatherer(opts) if err != nil { t.Error(err) } if gatherer.State() != ICEGathererStateNew { t.Fatalf("Expected gathering state new") } gatherFinished := make(chan struct{}) gatherer.OnLocalCandidate(func(i *ICECandidate) { if i == nil { close(gatherFinished) } }) if err = gatherer.Gather(); err != nil { t.Error(err) } <-gatherFinished params, err := gatherer.GetLocalParameters() if err != nil { t.Error(err) } if params.UsernameFragment == "" || params.Password == "" { t.Fatalf("Empty local username or password frag") } candidates, err := gatherer.GetLocalCandidates() if err != nil { t.Error(err) } if len(candidates) == 0 { t.Fatalf("No candidates gathered") } assert.NoError(t, gatherer.Close()) } func TestICEGather_mDNSCandidateGathering(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 20) defer lim.Stop() report := test.CheckRoutines(t) defer report() s := SettingEngine{} s.SetICEMulticastDNSMode(ice.MulticastDNSModeQueryAndGather) gatherer, err := NewAPI(WithSettingEngine(s)).NewICEGatherer(ICEGatherOptions{}) if err != nil { t.Error(err) } gotMulticastDNSCandidate, resolveFunc := context.WithCancel(context.Background()) gatherer.OnLocalCandidate(func(c *ICECandidate) { if c != nil && strings.HasSuffix(c.Address, ".local") { resolveFunc() } }) assert.NoError(t, gatherer.Gather()) <-gotMulticastDNSCandidate.Done() assert.NoError(t, gatherer.Close()) } func TestICEGatherer_AlreadyClosed(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 20) defer lim.Stop() report := test.CheckRoutines(t) defer report() opts := ICEGatherOptions{ ICEServers: []ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}, } t.Run("Gather", func(t *testing.T) { gatherer, err := NewAPI().NewICEGatherer(opts) assert.NoError(t, err) err = gatherer.createAgent() assert.NoError(t, err) err = gatherer.Close() assert.NoError(t, err) err = gatherer.Gather() assert.ErrorIs(t, err, errICEAgentNotExist) }) t.Run("GetLocalParameters", func(t *testing.T) { gatherer, err := NewAPI().NewICEGatherer(opts) assert.NoError(t, err) err = gatherer.createAgent() assert.NoError(t, err) err = gatherer.Close() assert.NoError(t, err) _, err = gatherer.GetLocalParameters() assert.ErrorIs(t, err, errICEAgentNotExist) }) t.Run("GetLocalCandidates", func(t *testing.T) { gatherer, err := NewAPI().NewICEGatherer(opts) assert.NoError(t, err) err = gatherer.createAgent() assert.NoError(t, err) err = gatherer.Close() assert.NoError(t, err) _, err = gatherer.GetLocalCandidates() assert.ErrorIs(t, err, errICEAgentNotExist) }) } webrtc-3.1.56/icegathererstate.go000066400000000000000000000024431437620512100167520ustar00rootroot00000000000000package webrtc import ( "sync/atomic" ) // ICEGathererState represents the current state of the ICE gatherer. type ICEGathererState uint32 const ( // ICEGathererStateNew indicates object has been created but // gather() has not been called. ICEGathererStateNew ICEGathererState = iota + 1 // ICEGathererStateGathering indicates gather() has been called, // and the ICEGatherer is in the process of gathering candidates. ICEGathererStateGathering // ICEGathererStateComplete indicates the ICEGatherer has completed gathering. ICEGathererStateComplete // ICEGathererStateClosed indicates the closed state can only be entered // when the ICEGatherer has been closed intentionally by calling close(). ICEGathererStateClosed ) func (s ICEGathererState) String() string { switch s { case ICEGathererStateNew: return "new" case ICEGathererStateGathering: return "gathering" case ICEGathererStateComplete: return "complete" case ICEGathererStateClosed: return "closed" default: return unknownStr } } func atomicStoreICEGathererState(state *ICEGathererState, newState ICEGathererState) { atomic.StoreUint32((*uint32)(state), uint32(newState)) } func atomicLoadICEGathererState(state *ICEGathererState) ICEGathererState { return ICEGathererState(atomic.LoadUint32((*uint32)(state))) } webrtc-3.1.56/icegathererstate_test.go000066400000000000000000000010751437620512100200110ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestICEGathererState_String(t *testing.T) { testCases := []struct { state ICEGathererState expectedString string }{ {ICEGathererState(Unknown), unknownStr}, {ICEGathererStateNew, "new"}, {ICEGathererStateGathering, "gathering"}, {ICEGathererStateComplete, "complete"}, {ICEGathererStateClosed, "closed"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.state.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/icegatheringstate.go000066400000000000000000000030541437620512100171200ustar00rootroot00000000000000package webrtc // ICEGatheringState describes the state of the candidate gathering process. type ICEGatheringState int const ( // ICEGatheringStateNew indicates that any of the ICETransports are // in the "new" gathering state and none of the transports are in the // "gathering" state, or there are no transports. ICEGatheringStateNew ICEGatheringState = iota + 1 // ICEGatheringStateGathering indicates that any of the ICETransports // are in the "gathering" state. ICEGatheringStateGathering // ICEGatheringStateComplete indicates that at least one ICETransport // exists, and all ICETransports are in the "completed" gathering state. ICEGatheringStateComplete ) // This is done this way because of a linter. const ( iceGatheringStateNewStr = "new" iceGatheringStateGatheringStr = "gathering" iceGatheringStateCompleteStr = "complete" ) // NewICEGatheringState takes a string and converts it to ICEGatheringState func NewICEGatheringState(raw string) ICEGatheringState { switch raw { case iceGatheringStateNewStr: return ICEGatheringStateNew case iceGatheringStateGatheringStr: return ICEGatheringStateGathering case iceGatheringStateCompleteStr: return ICEGatheringStateComplete default: return ICEGatheringState(Unknown) } } func (t ICEGatheringState) String() string { switch t { case ICEGatheringStateNew: return iceGatheringStateNewStr case ICEGatheringStateGathering: return iceGatheringStateGatheringStr case ICEGatheringStateComplete: return iceGatheringStateCompleteStr default: return ErrUnknownType.Error() } } webrtc-3.1.56/icegatheringstate_test.go000066400000000000000000000017711437620512100201630ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewICEGatheringState(t *testing.T) { testCases := []struct { stateString string expectedState ICEGatheringState }{ {unknownStr, ICEGatheringState(Unknown)}, {"new", ICEGatheringStateNew}, {"gathering", ICEGatheringStateGathering}, {"complete", ICEGatheringStateComplete}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedState, NewICEGatheringState(testCase.stateString), "testCase: %d %v", i, testCase, ) } } func TestICEGatheringState_String(t *testing.T) { testCases := []struct { state ICEGatheringState expectedString string }{ {ICEGatheringState(Unknown), unknownStr}, {ICEGatheringStateNew, "new"}, {ICEGatheringStateGathering, "gathering"}, {ICEGatheringStateComplete, "complete"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.state.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/icegatheroptions.go000066400000000000000000000003041437620512100167700ustar00rootroot00000000000000package webrtc // ICEGatherOptions provides options relating to the gathering of ICE candidates. type ICEGatherOptions struct { ICEServers []ICEServer ICEGatherPolicy ICETransportPolicy } webrtc-3.1.56/icemux.go000066400000000000000000000013601437620512100147160ustar00rootroot00000000000000package webrtc import ( "net" "github.com/pion/ice/v2" "github.com/pion/logging" ) // NewICETCPMux creates a new instance of ice.TCPMuxDefault. It enables use of // passive ICE TCP candidates. func NewICETCPMux(logger logging.LeveledLogger, listener net.Listener, readBufferSize int) ice.TCPMux { return ice.NewTCPMuxDefault(ice.TCPMuxParams{ Listener: listener, Logger: logger, ReadBufferSize: readBufferSize, }) } // NewICEUDPMux creates a new instance of ice.UDPMuxDefault. It allows many PeerConnections to be served // by a single UDP Port. func NewICEUDPMux(logger logging.LeveledLogger, udpConn net.PacketConn) ice.UDPMux { return ice.NewUDPMuxDefault(ice.UDPMuxParams{ UDPConn: udpConn, Logger: logger, }) } webrtc-3.1.56/iceparameters.go000066400000000000000000000004341437620512100162510ustar00rootroot00000000000000package webrtc // ICEParameters includes the ICE username fragment // and password and other ICE-related parameters. type ICEParameters struct { UsernameFragment string `json:"usernameFragment"` Password string `json:"password"` ICELite bool `json:"iceLite"` } webrtc-3.1.56/iceprotocol.go000066400000000000000000000020141437620512100157430ustar00rootroot00000000000000package webrtc import ( "fmt" "strings" ) // ICEProtocol indicates the transport protocol type that is used in the // ice.URL structure. type ICEProtocol int const ( // ICEProtocolUDP indicates the URL uses a UDP transport. ICEProtocolUDP ICEProtocol = iota + 1 // ICEProtocolTCP indicates the URL uses a TCP transport. ICEProtocolTCP ) // This is done this way because of a linter. const ( iceProtocolUDPStr = "udp" iceProtocolTCPStr = "tcp" ) // NewICEProtocol takes a string and converts it to ICEProtocol func NewICEProtocol(raw string) (ICEProtocol, error) { switch { case strings.EqualFold(iceProtocolUDPStr, raw): return ICEProtocolUDP, nil case strings.EqualFold(iceProtocolTCPStr, raw): return ICEProtocolTCP, nil default: return ICEProtocol(Unknown), fmt.Errorf("%w: %s", errICEProtocolUnknown, raw) } } func (t ICEProtocol) String() string { switch t { case ICEProtocolUDP: return iceProtocolUDPStr case ICEProtocolTCP: return iceProtocolTCPStr default: return ErrUnknownType.Error() } } webrtc-3.1.56/iceprotocol_test.go000066400000000000000000000020151437620512100170030ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewICEProtocol(t *testing.T) { testCases := []struct { protoString string shouldFail bool expectedProto ICEProtocol }{ {unknownStr, true, ICEProtocol(Unknown)}, {"udp", false, ICEProtocolUDP}, {"tcp", false, ICEProtocolTCP}, {"UDP", false, ICEProtocolUDP}, {"TCP", false, ICEProtocolTCP}, } for i, testCase := range testCases { actual, err := NewICEProtocol(testCase.protoString) if (err != nil) != testCase.shouldFail { t.Error(err) } assert.Equal(t, testCase.expectedProto, actual, "testCase: %d %v", i, testCase, ) } } func TestICEProtocol_String(t *testing.T) { testCases := []struct { proto ICEProtocol expectedString string }{ {ICEProtocol(Unknown), unknownStr}, {ICEProtocolUDP, "udp"}, {ICEProtocolTCP, "tcp"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.proto.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/icerole.go000066400000000000000000000022241437620512100150460ustar00rootroot00000000000000package webrtc // ICERole describes the role ice.Agent is playing in selecting the // preferred the candidate pair. type ICERole int const ( // ICERoleControlling indicates that the ICE agent that is responsible // for selecting the final choice of candidate pairs and signaling them // through STUN and an updated offer, if needed. In any session, one agent // is always controlling. The other is the controlled agent. ICERoleControlling ICERole = iota + 1 // ICERoleControlled indicates that an ICE agent that waits for the // controlling agent to select the final choice of candidate pairs. ICERoleControlled ) // This is done this way because of a linter. const ( iceRoleControllingStr = "controlling" iceRoleControlledStr = "controlled" ) func newICERole(raw string) ICERole { switch raw { case iceRoleControllingStr: return ICERoleControlling case iceRoleControlledStr: return ICERoleControlled default: return ICERole(Unknown) } } func (t ICERole) String() string { switch t { case ICERoleControlling: return iceRoleControllingStr case ICERoleControlled: return iceRoleControlledStr default: return ErrUnknownType.Error() } } webrtc-3.1.56/icerole_test.go000066400000000000000000000015251437620512100161100ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewICERole(t *testing.T) { testCases := []struct { roleString string expectedRole ICERole }{ {unknownStr, ICERole(Unknown)}, {"controlling", ICERoleControlling}, {"controlled", ICERoleControlled}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedRole, newICERole(testCase.roleString), "testCase: %d %v", i, testCase, ) } } func TestICERole_String(t *testing.T) { testCases := []struct { proto ICERole expectedString string }{ {ICERole(Unknown), unknownStr}, {ICERoleControlling, "controlling"}, {ICERoleControlled, "controlled"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.proto.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/iceserver.go000066400000000000000000000101721437620512100154140ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "encoding/json" "github.com/pion/ice/v2" "github.com/pion/webrtc/v3/pkg/rtcerr" ) // ICEServer describes a single STUN and TURN server that can be used by // the ICEAgent to establish a connection with a peer. type ICEServer struct { URLs []string `json:"urls"` Username string `json:"username,omitempty"` Credential interface{} `json:"credential,omitempty"` CredentialType ICECredentialType `json:"credentialType,omitempty"` } func (s ICEServer) parseURL(i int) (*ice.URL, error) { return ice.ParseURL(s.URLs[i]) } func (s ICEServer) validate() error { _, err := s.urls() return err } func (s ICEServer) urls() ([]*ice.URL, error) { urls := []*ice.URL{} for i := range s.URLs { url, err := s.parseURL(i) if err != nil { return nil, &rtcerr.InvalidAccessError{Err: err} } if url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS { // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.2) if s.Username == "" || s.Credential == nil { return nil, &rtcerr.InvalidAccessError{Err: ErrNoTurnCredentials} } url.Username = s.Username switch s.CredentialType { case ICECredentialTypePassword: // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.3) password, ok := s.Credential.(string) if !ok { return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials} } url.Password = password case ICECredentialTypeOauth: // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.4) if _, ok := s.Credential.(OAuthCredential); !ok { return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials} } default: return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials} } } urls = append(urls, url) } return urls, nil } func iceserverUnmarshalUrls(val interface{}) (*[]string, error) { s, ok := val.([]interface{}) if !ok { return nil, errInvalidICEServer } out := make([]string, len(s)) for idx, url := range s { out[idx], ok = url.(string) if !ok { return nil, errInvalidICEServer } } return &out, nil } func iceserverUnmarshalOauth(val interface{}) (*OAuthCredential, error) { c, ok := val.(map[string]interface{}) if !ok { return nil, errInvalidICEServer } MACKey, ok := c["MACKey"].(string) if !ok { return nil, errInvalidICEServer } AccessToken, ok := c["AccessToken"].(string) if !ok { return nil, errInvalidICEServer } return &OAuthCredential{ MACKey: MACKey, AccessToken: AccessToken, }, nil } func (s *ICEServer) iceserverUnmarshalFields(m map[string]interface{}) error { if val, ok := m["urls"]; ok { u, err := iceserverUnmarshalUrls(val) if err != nil { return err } s.URLs = *u } else { s.URLs = []string{} } if val, ok := m["username"]; ok { s.Username, ok = val.(string) if !ok { return errInvalidICEServer } } if val, ok := m["credentialType"]; ok { ct, ok := val.(string) if !ok { return errInvalidICEServer } tpe, err := newICECredentialType(ct) if err != nil { return err } s.CredentialType = tpe } else { s.CredentialType = ICECredentialTypePassword } if val, ok := m["credential"]; ok { switch s.CredentialType { case ICECredentialTypePassword: s.Credential = val case ICECredentialTypeOauth: c, err := iceserverUnmarshalOauth(val) if err != nil { return err } s.Credential = *c default: return errInvalidICECredentialTypeString } } return nil } // UnmarshalJSON parses the JSON-encoded data and stores the result func (s *ICEServer) UnmarshalJSON(b []byte) error { var tmp interface{} err := json.Unmarshal(b, &tmp) if err != nil { return err } if m, ok := tmp.(map[string]interface{}); ok { return s.iceserverUnmarshalFields(m) } return errInvalidICEServer } // MarshalJSON returns the JSON encoding func (s ICEServer) MarshalJSON() ([]byte, error) { m := make(map[string]interface{}) m["urls"] = s.URLs if s.Username != "" { m["username"] = s.Username } if s.Credential != nil { m["credential"] = s.Credential } m["credentialType"] = s.CredentialType return json.Marshal(m) } webrtc-3.1.56/iceserver_js.go000066400000000000000000000016061437620512100161120ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import ( "errors" "github.com/pion/ice/v2" ) // ICEServer describes a single STUN and TURN server that can be used by // the ICEAgent to establish a connection with a peer. type ICEServer struct { URLs []string Username string // Note: TURN is not supported in the WASM bindings yet Credential interface{} CredentialType ICECredentialType } func (s ICEServer) parseURL(i int) (*ice.URL, error) { return ice.ParseURL(s.URLs[i]) } func (s ICEServer) validate() ([]*ice.URL, error) { urls := []*ice.URL{} for i := range s.URLs { url, err := s.parseURL(i) if err != nil { return nil, err } if url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS { return nil, errors.New("TURN is not currently supported in the JavaScript/Wasm bindings") } urls = append(urls, url) } return urls, nil } webrtc-3.1.56/iceserver_test.go000066400000000000000000000106731437620512100164610ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "encoding/json" "testing" "github.com/pion/ice/v2" "github.com/pion/webrtc/v3/pkg/rtcerr" "github.com/stretchr/testify/assert" ) func TestICEServer_validate(t *testing.T) { t.Run("Success", func(t *testing.T) { testCases := []struct { iceServer ICEServer expectedValidate bool }{ {ICEServer{ URLs: []string{"turn:192.158.29.39?transport=udp"}, Username: "unittest", Credential: "placeholder", CredentialType: ICECredentialTypePassword, }, true}, {ICEServer{ URLs: []string{"turn:[2001:db8:1234:5678::1]?transport=udp"}, Username: "unittest", Credential: "placeholder", CredentialType: ICECredentialTypePassword, }, true}, {ICEServer{ URLs: []string{"turn:192.158.29.39?transport=udp"}, Username: "unittest", Credential: OAuthCredential{ MACKey: "WmtzanB3ZW9peFhtdm42NzUzNG0=", AccessToken: "AAwg3kPHWPfvk9bDFL936wYvkoctMADzQ5VhNDgeMR3+ZlZ35byg972fW8QjpEl7bx91YLBPFsIhsxloWcXPhA==", }, CredentialType: ICECredentialTypeOauth, }, true}, } for i, testCase := range testCases { var iceServer ICEServer jsonobj, err := json.Marshal(testCase.iceServer) assert.NoError(t, err) err = json.Unmarshal(jsonobj, &iceServer) assert.NoError(t, err) assert.Equal(t, iceServer, testCase.iceServer) _, err = testCase.iceServer.urls() assert.Nil(t, err, "testCase: %d %v", i, testCase) } }) t.Run("Failure", func(t *testing.T) { testCases := []struct { iceServer ICEServer expectedErr error }{ {ICEServer{ URLs: []string{"turn:192.158.29.39?transport=udp"}, }, &rtcerr.InvalidAccessError{Err: ErrNoTurnCredentials}}, {ICEServer{ URLs: []string{"turn:192.158.29.39?transport=udp"}, Username: "unittest", Credential: false, CredentialType: ICECredentialTypePassword, }, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials}}, {ICEServer{ URLs: []string{"turn:192.158.29.39?transport=udp"}, Username: "unittest", Credential: false, CredentialType: ICECredentialTypeOauth, }, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials}}, {ICEServer{ URLs: []string{"turn:192.158.29.39?transport=udp"}, Username: "unittest", Credential: false, CredentialType: Unknown, }, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials}}, {ICEServer{ URLs: []string{"stun:google.de?transport=udp"}, Username: "unittest", Credential: false, CredentialType: ICECredentialTypeOauth, }, &rtcerr.InvalidAccessError{Err: ice.ErrSTUNQuery}}, } for i, testCase := range testCases { _, err := testCase.iceServer.urls() assert.EqualError(t, err, testCase.expectedErr.Error(), "testCase: %d %v", i, testCase, ) } }) t.Run("JsonFailure", func(t *testing.T) { testCases := [][]byte{ []byte(`{"urls":"NOTAURL","username":"unittest","credential":"placeholder","credentialType":"password"}`), []byte(`{"urls":["turn:[2001:db8:1234:5678::1]?transport=udp"],"username":"unittest","credential":"placeholder","credentialType":"invalid"}`), []byte(`{"urls":["turn:[2001:db8:1234:5678::1]?transport=udp"],"username":6,"credential":"placeholder","credentialType":"password"}`), []byte(`{"urls":["turn:192.158.29.39?transport=udp"],"username":"unittest","credential":{"Bad Object": true},"credentialType":"oauth"}`), []byte(`{"urls":["turn:192.158.29.39?transport=udp"],"username":"unittest","credential":{"MACKey":"WmtzanB3ZW9peFhtdm42NzUzNG0=","AccessToken":null,"credentialType":"oauth"}`), []byte(`{"urls":["turn:192.158.29.39?transport=udp"],"username":"unittest","credential":{"MACKey":"WmtzanB3ZW9peFhtdm42NzUzNG0=","AccessToken":null,"credentialType":"password"}`), []byte(`{"urls":["turn:192.158.29.39?transport=udp"],"username":"unittest","credential":{"MACKey":1337,"AccessToken":"AAwg3kPHWPfvk9bDFL936wYvkoctMADzQ5VhNDgeMR3+ZlZ35byg972fW8QjpEl7bx91YLBPFsIhsxloWcXPhA=="},"credentialType":"oauth"}`), } for i, testCase := range testCases { var tc ICEServer err := json.Unmarshal(testCase, &tc) assert.Error(t, err, "testCase: %d %v", i, string(testCase)) } }) } func TestICEServerZeroValue(t *testing.T) { server := ICEServer{ URLs: []string{"turn:galene.org:1195"}, Username: "galene", Credential: "secret", } assert.Equal(t, server.CredentialType, ICECredentialTypePassword) } webrtc-3.1.56/icetransport.go000066400000000000000000000212151437620512100161420ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "context" "fmt" "sync" "sync/atomic" "time" "github.com/pion/ice/v2" "github.com/pion/logging" "github.com/pion/webrtc/v3/internal/mux" ) // ICETransport allows an application access to information about the ICE // transport over which packets are sent and received. type ICETransport struct { lock sync.RWMutex role ICERole onConnectionStateChangeHandler atomic.Value // func(ICETransportState) internalOnConnectionStateChangeHandler atomic.Value // func(ICETransportState) onSelectedCandidatePairChangeHandler atomic.Value // func(*ICECandidatePair) state atomic.Value // ICETransportState gatherer *ICEGatherer conn *ice.Conn mux *mux.Mux ctx context.Context ctxCancel func() loggerFactory logging.LoggerFactory log logging.LeveledLogger } // GetSelectedCandidatePair returns the selected candidate pair on which packets are sent // if there is no selected pair nil is returned func (t *ICETransport) GetSelectedCandidatePair() (*ICECandidatePair, error) { agent := t.gatherer.getAgent() if agent == nil { return nil, nil //nolint:nilnil } icePair, err := agent.GetSelectedCandidatePair() if icePair == nil || err != nil { return nil, err } local, err := newICECandidateFromICE(icePair.Local) if err != nil { return nil, err } remote, err := newICECandidateFromICE(icePair.Remote) if err != nil { return nil, err } return &ICECandidatePair{Local: &local, Remote: &remote}, nil } // NewICETransport creates a new NewICETransport. func NewICETransport(gatherer *ICEGatherer, loggerFactory logging.LoggerFactory) *ICETransport { iceTransport := &ICETransport{ gatherer: gatherer, loggerFactory: loggerFactory, log: loggerFactory.NewLogger("ortc"), } iceTransport.setState(ICETransportStateNew) return iceTransport } // Start incoming connectivity checks based on its configured role. func (t *ICETransport) Start(gatherer *ICEGatherer, params ICEParameters, role *ICERole) error { t.lock.Lock() defer t.lock.Unlock() if t.State() != ICETransportStateNew { return errICETransportNotInNew } if gatherer != nil { t.gatherer = gatherer } if err := t.ensureGatherer(); err != nil { return err } agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to start ICETransport", errICEAgentNotExist) } if err := agent.OnConnectionStateChange(func(iceState ice.ConnectionState) { state := newICETransportStateFromICE(iceState) t.setState(state) t.onConnectionStateChange(state) }); err != nil { return err } if err := agent.OnSelectedCandidatePairChange(func(local, remote ice.Candidate) { candidates, err := newICECandidatesFromICE([]ice.Candidate{local, remote}) if err != nil { t.log.Warnf("%w: %s", errICECandiatesCoversionFailed, err) return } t.onSelectedCandidatePairChange(NewICECandidatePair(&candidates[0], &candidates[1])) }); err != nil { return err } if role == nil { controlled := ICERoleControlled role = &controlled } t.role = *role t.ctx, t.ctxCancel = context.WithCancel(context.Background()) // Drop the lock here to allow ICE candidates to be // added so that the agent can complete a connection t.lock.Unlock() var iceConn *ice.Conn var err error switch *role { case ICERoleControlling: iceConn, err = agent.Dial(t.ctx, params.UsernameFragment, params.Password) case ICERoleControlled: iceConn, err = agent.Accept(t.ctx, params.UsernameFragment, params.Password) default: err = errICERoleUnknown } // Reacquire the lock to set the connection/mux t.lock.Lock() if err != nil { return err } t.conn = iceConn config := mux.Config{ Conn: t.conn, BufferSize: int(t.gatherer.api.settingEngine.getReceiveMTU()), LoggerFactory: t.loggerFactory, } t.mux = mux.NewMux(config) return nil } // restart is not exposed currently because ORTC has users create a whole new ICETransport // so for now lets keep it private so we don't cause ORTC users to depend on non-standard APIs func (t *ICETransport) restart() error { t.lock.Lock() defer t.lock.Unlock() agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to restart ICETransport", errICEAgentNotExist) } if err := agent.Restart(t.gatherer.api.settingEngine.candidates.UsernameFragment, t.gatherer.api.settingEngine.candidates.Password); err != nil { return err } return t.gatherer.Gather() } // Stop irreversibly stops the ICETransport. func (t *ICETransport) Stop() error { t.lock.Lock() defer t.lock.Unlock() t.setState(ICETransportStateClosed) if t.ctxCancel != nil { t.ctxCancel() } if t.mux != nil { return t.mux.Close() } else if t.gatherer != nil { return t.gatherer.Close() } return nil } // OnSelectedCandidatePairChange sets a handler that is invoked when a new // ICE candidate pair is selected func (t *ICETransport) OnSelectedCandidatePairChange(f func(*ICECandidatePair)) { t.onSelectedCandidatePairChangeHandler.Store(f) } func (t *ICETransport) onSelectedCandidatePairChange(pair *ICECandidatePair) { if handler, ok := t.onSelectedCandidatePairChangeHandler.Load().(func(*ICECandidatePair)); ok { handler(pair) } } // OnConnectionStateChange sets a handler that is fired when the ICE // connection state changes. func (t *ICETransport) OnConnectionStateChange(f func(ICETransportState)) { t.onConnectionStateChangeHandler.Store(f) } func (t *ICETransport) onConnectionStateChange(state ICETransportState) { if handler, ok := t.onConnectionStateChangeHandler.Load().(func(ICETransportState)); ok { handler(state) } if handler, ok := t.internalOnConnectionStateChangeHandler.Load().(func(ICETransportState)); ok { handler(state) } } // Role indicates the current role of the ICE transport. func (t *ICETransport) Role() ICERole { t.lock.RLock() defer t.lock.RUnlock() return t.role } // SetRemoteCandidates sets the sequence of candidates associated with the remote ICETransport. func (t *ICETransport) SetRemoteCandidates(remoteCandidates []ICECandidate) error { t.lock.RLock() defer t.lock.RUnlock() if err := t.ensureGatherer(); err != nil { return err } agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to set remote candidates", errICEAgentNotExist) } for _, c := range remoteCandidates { i, err := c.toICE() if err != nil { return err } if err = agent.AddRemoteCandidate(i); err != nil { return err } } return nil } // AddRemoteCandidate adds a candidate associated with the remote ICETransport. func (t *ICETransport) AddRemoteCandidate(remoteCandidate *ICECandidate) error { t.lock.RLock() defer t.lock.RUnlock() var ( c ice.Candidate err error ) if err = t.ensureGatherer(); err != nil { return err } if remoteCandidate != nil { if c, err = remoteCandidate.toICE(); err != nil { return err } } agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to add remote candidates", errICEAgentNotExist) } return agent.AddRemoteCandidate(c) } // State returns the current ice transport state. func (t *ICETransport) State() ICETransportState { if v, ok := t.state.Load().(ICETransportState); ok { return v } return ICETransportState(0) } func (t *ICETransport) setState(i ICETransportState) { t.state.Store(i) } func (t *ICETransport) newEndpoint(f mux.MatchFunc) *mux.Endpoint { t.lock.Lock() defer t.lock.Unlock() return t.mux.NewEndpoint(f) } func (t *ICETransport) ensureGatherer() error { if t.gatherer == nil { return errICEGathererNotStarted } else if t.gatherer.getAgent() == nil { if err := t.gatherer.createAgent(); err != nil { return err } } return nil } func (t *ICETransport) collectStats(collector *statsReportCollector) { t.lock.Lock() conn := t.conn t.lock.Unlock() collector.Collecting() stats := TransportStats{ Timestamp: statsTimestampFrom(time.Now()), Type: StatsTypeTransport, ID: "iceTransport", } if conn != nil { stats.BytesSent = conn.BytesSent() stats.BytesReceived = conn.BytesReceived() } collector.Collect(stats.ID, stats) } func (t *ICETransport) haveRemoteCredentialsChange(newUfrag, newPwd string) bool { t.lock.Lock() defer t.lock.Unlock() agent := t.gatherer.getAgent() if agent == nil { return false } uFrag, uPwd, err := agent.GetRemoteUserCredentials() if err != nil { return false } return uFrag != newUfrag || uPwd != newPwd } func (t *ICETransport) setRemoteCredentials(newUfrag, newPwd string) error { t.lock.Lock() defer t.lock.Unlock() agent := t.gatherer.getAgent() if agent == nil { return fmt.Errorf("%w: unable to SetRemoteCredentials", errICEAgentNotExist) } return agent.SetRemoteCredentials(newUfrag, newPwd) } webrtc-3.1.56/icetransport_js.go000066400000000000000000000014071437620512100166370ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import "syscall/js" // ICETransport allows an application access to information about the ICE // transport over which packets are sent and received. type ICETransport struct { // Pointer to the underlying JavaScript ICETransport object. underlying js.Value } // GetSelectedCandidatePair returns the selected candidate pair on which packets are sent // if there is no selected pair nil is returned func (t *ICETransport) GetSelectedCandidatePair() (*ICECandidatePair, error) { val := t.underlying.Call("getSelectedCandidatePair") if val.IsNull() || val.IsUndefined() { return nil, nil } return NewICECandidatePair( valueToICECandidate(val.Get("local")), valueToICECandidate(val.Get("remote")), ), nil } webrtc-3.1.56/icetransport_test.go000066400000000000000000000060171437620512100172040ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "sync" "sync/atomic" "testing" "time" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) func TestICETransport_OnConnectionStateChange(t *testing.T) { report := test.CheckRoutines(t) defer report() lim := test.TimeOut(time.Second * 30) defer lim.Stop() pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) var ( iceComplete sync.WaitGroup peerConnectionConnected sync.WaitGroup ) iceComplete.Add(2) peerConnectionConnected.Add(2) onIceComplete := func(s ICETransportState) { if s == ICETransportStateConnected { iceComplete.Done() } } pcOffer.SCTP().Transport().ICETransport().OnConnectionStateChange(onIceComplete) pcAnswer.SCTP().Transport().ICETransport().OnConnectionStateChange(onIceComplete) onConnected := func(s PeerConnectionState) { if s == PeerConnectionStateConnected { peerConnectionConnected.Done() } } pcOffer.OnConnectionStateChange(onConnected) pcAnswer.OnConnectionStateChange(onConnected) assert.NoError(t, signalPair(pcOffer, pcAnswer)) iceComplete.Wait() peerConnectionConnected.Wait() closePairNow(t, pcOffer, pcAnswer) } func TestICETransport_OnSelectedCandidatePairChange(t *testing.T) { report := test.CheckRoutines(t) defer report() lim := test.TimeOut(time.Second * 30) defer lim.Stop() pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) iceComplete := make(chan bool) pcAnswer.OnICEConnectionStateChange(func(iceState ICEConnectionState) { if iceState == ICEConnectionStateConnected { time.Sleep(3 * time.Second) close(iceComplete) } }) senderCalledCandidateChange := int32(0) pcOffer.SCTP().Transport().ICETransport().OnSelectedCandidatePairChange(func(pair *ICECandidatePair) { atomic.StoreInt32(&senderCalledCandidateChange, 1) }) assert.NoError(t, signalPair(pcOffer, pcAnswer)) <-iceComplete if atomic.LoadInt32(&senderCalledCandidateChange) == 0 { t.Fatalf("Sender ICETransport OnSelectedCandidateChange was never called") } closePairNow(t, pcOffer, pcAnswer) } func TestICETransport_GetSelectedCandidatePair(t *testing.T) { offerer, answerer, err := newPair() assert.NoError(t, err) peerConnectionConnected := untilConnectionState(PeerConnectionStateConnected, offerer, answerer) offererSelectedPair, err := offerer.SCTP().Transport().ICETransport().GetSelectedCandidatePair() assert.NoError(t, err) assert.Nil(t, offererSelectedPair) answererSelectedPair, err := answerer.SCTP().Transport().ICETransport().GetSelectedCandidatePair() assert.NoError(t, err) assert.Nil(t, answererSelectedPair) assert.NoError(t, signalPair(offerer, answerer)) peerConnectionConnected.Wait() offererSelectedPair, err = offerer.SCTP().Transport().ICETransport().GetSelectedCandidatePair() assert.NoError(t, err) assert.NotNil(t, offererSelectedPair) answererSelectedPair, err = answerer.SCTP().Transport().ICETransport().GetSelectedCandidatePair() assert.NoError(t, err) assert.NotNil(t, answererSelectedPair) closePairNow(t, offerer, answerer) } webrtc-3.1.56/icetransportpolicy.go000066400000000000000000000032531437620512100173640ustar00rootroot00000000000000package webrtc import ( "encoding/json" ) // ICETransportPolicy defines the ICE candidate policy surface the // permitted candidates. Only these candidates are used for connectivity checks. type ICETransportPolicy int // ICEGatherPolicy is the ORTC equivalent of ICETransportPolicy type ICEGatherPolicy = ICETransportPolicy const ( // ICETransportPolicyAll indicates any type of candidate is used. ICETransportPolicyAll ICETransportPolicy = iota // ICETransportPolicyRelay indicates only media relay candidates such // as candidates passing through a TURN server are used. ICETransportPolicyRelay ) // This is done this way because of a linter. const ( iceTransportPolicyRelayStr = "relay" iceTransportPolicyAllStr = "all" ) // NewICETransportPolicy takes a string and converts it to ICETransportPolicy func NewICETransportPolicy(raw string) ICETransportPolicy { switch raw { case iceTransportPolicyRelayStr: return ICETransportPolicyRelay case iceTransportPolicyAllStr: return ICETransportPolicyAll default: return ICETransportPolicy(Unknown) } } func (t ICETransportPolicy) String() string { switch t { case ICETransportPolicyRelay: return iceTransportPolicyRelayStr case ICETransportPolicyAll: return iceTransportPolicyAllStr default: return ErrUnknownType.Error() } } // UnmarshalJSON parses the JSON-encoded data and stores the result func (t *ICETransportPolicy) UnmarshalJSON(b []byte) error { var val string if err := json.Unmarshal(b, &val); err != nil { return err } *t = NewICETransportPolicy(val) return nil } // MarshalJSON returns the JSON encoding func (t ICETransportPolicy) MarshalJSON() ([]byte, error) { return json.Marshal(t.String()) } webrtc-3.1.56/icetransportpolicy_test.go000066400000000000000000000015111437620512100204160ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewICETransportPolicy(t *testing.T) { testCases := []struct { policyString string expectedPolicy ICETransportPolicy }{ {"relay", ICETransportPolicyRelay}, {"all", ICETransportPolicyAll}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedPolicy, NewICETransportPolicy(testCase.policyString), "testCase: %d %v", i, testCase, ) } } func TestICETransportPolicy_String(t *testing.T) { testCases := []struct { policy ICETransportPolicy expectedString string }{ {ICETransportPolicyRelay, "relay"}, {ICETransportPolicyAll, "all"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.policy.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/icetransportstate.go000066400000000000000000000064741437620512100172150ustar00rootroot00000000000000package webrtc import "github.com/pion/ice/v2" // ICETransportState represents the current state of the ICE transport. type ICETransportState int const ( // ICETransportStateNew indicates the ICETransport is waiting // for remote candidates to be supplied. ICETransportStateNew = iota + 1 // ICETransportStateChecking indicates the ICETransport has // received at least one remote candidate, and a local and remote // ICECandidateComplete dictionary was not added as the last candidate. ICETransportStateChecking // ICETransportStateConnected indicates the ICETransport has // received a response to an outgoing connectivity check, or has // received incoming DTLS/media after a successful response to an // incoming connectivity check, but is still checking other candidate // pairs to see if there is a better connection. ICETransportStateConnected // ICETransportStateCompleted indicates the ICETransport tested // all appropriate candidate pairs and at least one functioning // candidate pair has been found. ICETransportStateCompleted // ICETransportStateFailed indicates the ICETransport the last // candidate was added and all appropriate candidate pairs have either // failed connectivity checks or have lost consent. ICETransportStateFailed // ICETransportStateDisconnected indicates the ICETransport has received // at least one local and remote candidate, but the final candidate was // received yet and all appropriate candidate pairs thus far have been // tested and failed. ICETransportStateDisconnected // ICETransportStateClosed indicates the ICETransport has shut down // and is no longer responding to STUN requests. ICETransportStateClosed ) func (c ICETransportState) String() string { switch c { case ICETransportStateNew: return "new" case ICETransportStateChecking: return "checking" case ICETransportStateConnected: return "connected" case ICETransportStateCompleted: return "completed" case ICETransportStateFailed: return "failed" case ICETransportStateDisconnected: return "disconnected" case ICETransportStateClosed: return "closed" default: return unknownStr } } func newICETransportStateFromICE(i ice.ConnectionState) ICETransportState { switch i { case ice.ConnectionStateNew: return ICETransportStateNew case ice.ConnectionStateChecking: return ICETransportStateChecking case ice.ConnectionStateConnected: return ICETransportStateConnected case ice.ConnectionStateCompleted: return ICETransportStateCompleted case ice.ConnectionStateFailed: return ICETransportStateFailed case ice.ConnectionStateDisconnected: return ICETransportStateDisconnected case ice.ConnectionStateClosed: return ICETransportStateClosed default: return ICETransportState(Unknown) } } func (c ICETransportState) toICE() ice.ConnectionState { switch c { case ICETransportStateNew: return ice.ConnectionStateNew case ICETransportStateChecking: return ice.ConnectionStateChecking case ICETransportStateConnected: return ice.ConnectionStateConnected case ICETransportStateCompleted: return ice.ConnectionStateCompleted case ICETransportStateFailed: return ice.ConnectionStateFailed case ICETransportStateDisconnected: return ice.ConnectionStateDisconnected case ICETransportStateClosed: return ice.ConnectionStateClosed default: return ice.ConnectionState(Unknown) } } webrtc-3.1.56/icetransportstate_test.go000066400000000000000000000031321437620512100202400ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/pion/ice/v2" "github.com/stretchr/testify/assert" ) func TestICETransportState_String(t *testing.T) { testCases := []struct { state ICETransportState expectedString string }{ {ICETransportState(Unknown), unknownStr}, {ICETransportStateNew, "new"}, {ICETransportStateChecking, "checking"}, {ICETransportStateConnected, "connected"}, {ICETransportStateCompleted, "completed"}, {ICETransportStateFailed, "failed"}, {ICETransportStateDisconnected, "disconnected"}, {ICETransportStateClosed, "closed"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.state.String(), "testCase: %d %v", i, testCase, ) } } func TestICETransportState_Convert(t *testing.T) { testCases := []struct { native ICETransportState ice ice.ConnectionState }{ {ICETransportState(Unknown), ice.ConnectionState(Unknown)}, {ICETransportStateNew, ice.ConnectionStateNew}, {ICETransportStateChecking, ice.ConnectionStateChecking}, {ICETransportStateConnected, ice.ConnectionStateConnected}, {ICETransportStateCompleted, ice.ConnectionStateCompleted}, {ICETransportStateFailed, ice.ConnectionStateFailed}, {ICETransportStateDisconnected, ice.ConnectionStateDisconnected}, {ICETransportStateClosed, ice.ConnectionStateClosed}, } for i, testCase := range testCases { assert.Equal(t, testCase.native.toICE(), testCase.ice, "testCase: %d %v", i, testCase, ) assert.Equal(t, testCase.native, newICETransportStateFromICE(testCase.ice), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/interceptor.go000066400000000000000000000117721437620512100157720ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "sync/atomic" "github.com/pion/interceptor" "github.com/pion/interceptor/pkg/nack" "github.com/pion/interceptor/pkg/report" "github.com/pion/interceptor/pkg/twcc" "github.com/pion/rtp" "github.com/pion/sdp/v3" ) // RegisterDefaultInterceptors will register some useful interceptors. // If you want to customize which interceptors are loaded, you should copy the // code from this method and remove unwanted interceptors. func RegisterDefaultInterceptors(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Registry) error { if err := ConfigureNack(mediaEngine, interceptorRegistry); err != nil { return err } if err := ConfigureRTCPReports(interceptorRegistry); err != nil { return err } if err := ConfigureTWCCSender(mediaEngine, interceptorRegistry); err != nil { return err } return nil } // ConfigureRTCPReports will setup everything necessary for generating Sender and Receiver Reports func ConfigureRTCPReports(interceptorRegistry *interceptor.Registry) error { reciver, err := report.NewReceiverInterceptor() if err != nil { return err } sender, err := report.NewSenderInterceptor() if err != nil { return err } interceptorRegistry.Add(reciver) interceptorRegistry.Add(sender) return nil } // ConfigureNack will setup everything necessary for handling generating/responding to nack messages. func ConfigureNack(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Registry) error { generator, err := nack.NewGeneratorInterceptor() if err != nil { return err } responder, err := nack.NewResponderInterceptor() if err != nil { return err } mediaEngine.RegisterFeedback(RTCPFeedback{Type: "nack"}, RTPCodecTypeVideo) mediaEngine.RegisterFeedback(RTCPFeedback{Type: "nack", Parameter: "pli"}, RTPCodecTypeVideo) interceptorRegistry.Add(responder) interceptorRegistry.Add(generator) return nil } // ConfigureTWCCHeaderExtensionSender will setup everything necessary for adding // a TWCC header extension to outgoing RTP packets. This will allow the remote peer to generate TWCC reports. func ConfigureTWCCHeaderExtensionSender(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Registry) error { if err := mediaEngine.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, RTPCodecTypeVideo); err != nil { return err } if err := mediaEngine.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, RTPCodecTypeAudio); err != nil { return err } i, err := twcc.NewHeaderExtensionInterceptor() if err != nil { return err } interceptorRegistry.Add(i) return nil } // ConfigureTWCCSender will setup everything necessary for generating TWCC reports. func ConfigureTWCCSender(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Registry) error { mediaEngine.RegisterFeedback(RTCPFeedback{Type: TypeRTCPFBTransportCC}, RTPCodecTypeVideo) if err := mediaEngine.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, RTPCodecTypeVideo); err != nil { return err } mediaEngine.RegisterFeedback(RTCPFeedback{Type: TypeRTCPFBTransportCC}, RTPCodecTypeAudio) if err := mediaEngine.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, RTPCodecTypeAudio); err != nil { return err } generator, err := twcc.NewSenderInterceptor() if err != nil { return err } interceptorRegistry.Add(generator) return nil } type interceptorToTrackLocalWriter struct{ interceptor atomic.Value } // interceptor.RTPWriter } func (i *interceptorToTrackLocalWriter) WriteRTP(header *rtp.Header, payload []byte) (int, error) { if writer, ok := i.interceptor.Load().(interceptor.RTPWriter); ok && writer != nil { return writer.Write(header, payload, interceptor.Attributes{}) } return 0, nil } func (i *interceptorToTrackLocalWriter) Write(b []byte) (int, error) { packet := &rtp.Packet{} if err := packet.Unmarshal(b); err != nil { return 0, err } return i.WriteRTP(&packet.Header, packet.Payload) } func createStreamInfo(id string, ssrc SSRC, payloadType PayloadType, codec RTPCodecCapability, webrtcHeaderExtensions []RTPHeaderExtensionParameter) *interceptor.StreamInfo { headerExtensions := make([]interceptor.RTPHeaderExtension, 0, len(webrtcHeaderExtensions)) for _, h := range webrtcHeaderExtensions { headerExtensions = append(headerExtensions, interceptor.RTPHeaderExtension{ID: h.ID, URI: h.URI}) } feedbacks := make([]interceptor.RTCPFeedback, 0, len(codec.RTCPFeedback)) for _, f := range codec.RTCPFeedback { feedbacks = append(feedbacks, interceptor.RTCPFeedback{Type: f.Type, Parameter: f.Parameter}) } return &interceptor.StreamInfo{ ID: id, Attributes: interceptor.Attributes{}, SSRC: uint32(ssrc), PayloadType: uint8(payloadType), RTPHeaderExtensions: headerExtensions, MimeType: codec.MimeType, ClockRate: codec.ClockRate, Channels: codec.Channels, SDPFmtpLine: codec.SDPFmtpLine, RTCPFeedback: feedbacks, } } webrtc-3.1.56/interceptor_test.go000066400000000000000000000163741437620512100170340ustar00rootroot00000000000000//go:build !js // +build !js package webrtc // import ( "context" "sync/atomic" "testing" "time" "github.com/pion/interceptor" mock_interceptor "github.com/pion/interceptor/pkg/mock" "github.com/pion/rtp" "github.com/pion/transport/v2/test" "github.com/pion/webrtc/v3/pkg/media" "github.com/stretchr/testify/assert" ) // E2E test of the features of Interceptors // * Assert an extension can be set on an outbound packet // * Assert an extension can be read on an outbound packet // * Assert that attributes set by an interceptor are returned to the Reader func TestPeerConnection_Interceptor(t *testing.T) { to := test.TimeOut(time.Second * 20) defer to.Stop() report := test.CheckRoutines(t) defer report() createPC := func() *PeerConnection { m := &MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) ir := &interceptor.Registry{} ir.Add(&mock_interceptor.Factory{ NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) { return &mock_interceptor.Interceptor{ BindLocalStreamFn: func(_ *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { // set extension on outgoing packet header.Extension = true header.ExtensionProfile = 0xBEDE assert.NoError(t, header.SetExtension(2, []byte("foo"))) return writer.Write(header, payload, attributes) }) }, BindRemoteStreamFn: func(_ *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { if a == nil { a = interceptor.Attributes{} } a.Set("attribute", "value") return reader.Read(b, a) }) }, }, nil }, }) pc, err := NewAPI(WithMediaEngine(m), WithInterceptorRegistry(ir)).NewPeerConnection(Configuration{}) assert.NoError(t, err) return pc } offerer := createPC() answerer := createPC() track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = offerer.AddTrack(track) assert.NoError(t, err) seenRTP, seenRTPCancel := context.WithCancel(context.Background()) answerer.OnTrack(func(track *TrackRemote, receiver *RTPReceiver) { p, attributes, readErr := track.ReadRTP() assert.NoError(t, readErr) assert.Equal(t, p.Extension, true) assert.Equal(t, "foo", string(p.GetExtension(2))) assert.Equal(t, "value", attributes.Get("attribute")) seenRTPCancel() }) assert.NoError(t, signalPair(offerer, answerer)) func() { ticker := time.NewTicker(time.Millisecond * 20) for { select { case <-seenRTP.Done(): return case <-ticker.C: assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second})) } } }() closePairNow(t, offerer, answerer) } func Test_Interceptor_BindUnbind(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() m := &MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) var ( cntBindRTCPReader uint32 cntBindRTCPWriter uint32 cntBindLocalStream uint32 cntUnbindLocalStream uint32 cntBindRemoteStream uint32 cntUnbindRemoteStream uint32 cntClose uint32 ) mockInterceptor := &mock_interceptor.Interceptor{ BindRTCPReaderFn: func(reader interceptor.RTCPReader) interceptor.RTCPReader { atomic.AddUint32(&cntBindRTCPReader, 1) return reader }, BindRTCPWriterFn: func(writer interceptor.RTCPWriter) interceptor.RTCPWriter { atomic.AddUint32(&cntBindRTCPWriter, 1) return writer }, BindLocalStreamFn: func(i *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter { atomic.AddUint32(&cntBindLocalStream, 1) return writer }, UnbindLocalStreamFn: func(i *interceptor.StreamInfo) { atomic.AddUint32(&cntUnbindLocalStream, 1) }, BindRemoteStreamFn: func(i *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { atomic.AddUint32(&cntBindRemoteStream, 1) return reader }, UnbindRemoteStreamFn: func(i *interceptor.StreamInfo) { atomic.AddUint32(&cntUnbindRemoteStream, 1) }, CloseFn: func() error { atomic.AddUint32(&cntClose, 1) return nil }, } ir := &interceptor.Registry{} ir.Add(&mock_interceptor.Factory{ NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) { return mockInterceptor, nil }, }) sender, receiver, err := NewAPI(WithMediaEngine(m), WithInterceptorRegistry(ir)).newPair(Configuration{}) assert.NoError(t, err) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = sender.AddTrack(track) assert.NoError(t, err) receiverReady, receiverReadyFn := context.WithCancel(context.Background()) receiver.OnTrack(func(track *TrackRemote, _ *RTPReceiver) { _, _, readErr := track.ReadRTP() assert.NoError(t, readErr) receiverReadyFn() }) assert.NoError(t, signalPair(sender, receiver)) ticker := time.NewTicker(time.Millisecond * 20) defer ticker.Stop() func() { for { select { case <-receiverReady.Done(): return case <-ticker.C: // Send packet to make receiver track actual creates RTPReceiver. assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second})) } } }() closePairNow(t, sender, receiver) // Bind/UnbindLocal/RemoteStream should be called from one side. if cnt := atomic.LoadUint32(&cntBindLocalStream); cnt != 1 { t.Errorf("BindLocalStreamFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntUnbindLocalStream); cnt != 1 { t.Errorf("UnbindLocalStreamFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntBindRemoteStream); cnt != 1 { t.Errorf("BindRemoteStreamFn is expected to be called once, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntUnbindRemoteStream); cnt != 1 { t.Errorf("UnbindRemoteStreamFn is expected to be called once, but called %d times", cnt) } // BindRTCPWriter/Reader and Close should be called from both side. if cnt := atomic.LoadUint32(&cntBindRTCPWriter); cnt != 2 { t.Errorf("BindRTCPWriterFn is expected to be called twice, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntBindRTCPReader); cnt != 2 { t.Errorf("BindRTCPReaderFn is expected to be called twice, but called %d times", cnt) } if cnt := atomic.LoadUint32(&cntClose); cnt != 2 { t.Errorf("CloseFn is expected to be called twice, but called %d times", cnt) } } func Test_InterceptorRegistry_Build(t *testing.T) { registryBuildCount := 0 ir := &interceptor.Registry{} ir.Add(&mock_interceptor.Factory{ NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) { registryBuildCount++ return &interceptor.NoOp{}, nil }, }) peerConnectionA, err := NewAPI(WithInterceptorRegistry(ir)).NewPeerConnection(Configuration{}) assert.NoError(t, err) peerConnectionB, err := NewAPI(WithInterceptorRegistry(ir)).NewPeerConnection(Configuration{}) assert.NoError(t, err) assert.Equal(t, 2, registryBuildCount) closePairNow(t, peerConnectionA, peerConnectionB) } webrtc-3.1.56/internal/000077500000000000000000000000001437620512100147115ustar00rootroot00000000000000webrtc-3.1.56/internal/fmtp/000077500000000000000000000000001437620512100156575ustar00rootroot00000000000000webrtc-3.1.56/internal/fmtp/fmtp.go000066400000000000000000000035721437620512100171630ustar00rootroot00000000000000// Package fmtp implements per codec parsing of fmtp lines package fmtp import ( "strings" ) // FMTP interface for implementing custom // FMTP parsers based on MimeType type FMTP interface { // MimeType returns the MimeType associated with // the fmtp MimeType() string // Match compares two fmtp descriptions for // compatibility based on the MimeType Match(f FMTP) bool // Parameter returns a value for the associated key // if contained in the parsed fmtp string Parameter(key string) (string, bool) } // Parse parses an fmtp string based on the MimeType func Parse(mimetype, line string) FMTP { var f FMTP parameters := make(map[string]string) for _, p := range strings.Split(line, ";") { pp := strings.SplitN(strings.TrimSpace(p), "=", 2) key := strings.ToLower(pp[0]) var value string if len(pp) > 1 { value = pp[1] } parameters[key] = value } switch { case strings.EqualFold(mimetype, "video/h264"): f = &h264FMTP{ parameters: parameters, } default: f = &genericFMTP{ mimeType: mimetype, parameters: parameters, } } return f } type genericFMTP struct { mimeType string parameters map[string]string } func (g *genericFMTP) MimeType() string { return g.mimeType } // Match returns true if g and b are compatible fmtp descriptions // The generic implementation is used for MimeTypes that are not defined func (g *genericFMTP) Match(b FMTP) bool { c, ok := b.(*genericFMTP) if !ok { return false } if !strings.EqualFold(g.mimeType, c.MimeType()) { return false } for k, v := range g.parameters { if vb, ok := c.parameters[k]; ok && !strings.EqualFold(vb, v) { return false } } for k, v := range c.parameters { if va, ok := g.parameters[k]; ok && !strings.EqualFold(va, v) { return false } } return true } func (g *genericFMTP) Parameter(key string) (string, bool) { v, ok := g.parameters[key] return v, ok } webrtc-3.1.56/internal/fmtp/fmtp_test.go000066400000000000000000000061561437620512100202230ustar00rootroot00000000000000package fmtp import ( "reflect" "testing" ) func TestGenericParseFmtp(t *testing.T) { testCases := map[string]struct { input string expected FMTP }{ "OneParam": { input: "key-name=value", expected: &genericFMTP{ mimeType: "generic", parameters: map[string]string{ "key-name": "value", }, }, }, "OneParamWithWhiteSpeces": { input: "\tkey-name=value ", expected: &genericFMTP{ mimeType: "generic", parameters: map[string]string{ "key-name": "value", }, }, }, "TwoParams": { input: "key-name=value;key2=value2", expected: &genericFMTP{ mimeType: "generic", parameters: map[string]string{ "key-name": "value", "key2": "value2", }, }, }, "TwoParamsWithWhiteSpeces": { input: "key-name=value; \n\tkey2=value2 ", expected: &genericFMTP{ mimeType: "generic", parameters: map[string]string{ "key-name": "value", "key2": "value2", }, }, }, } for name, testCase := range testCases { testCase := testCase t.Run(name, func(t *testing.T) { f := Parse("generic", testCase.input) if !reflect.DeepEqual(testCase.expected, f) { t.Errorf("Expected Fmtp params: %v, got: %v", testCase.expected, f) } if f.MimeType() != "generic" { t.Errorf("Expected MimeType of generic, got: %s", f.MimeType()) } }) } } func TestGenericFmtpCompare(t *testing.T) { consistString := map[bool]string{true: "consist", false: "inconsist"} testCases := map[string]struct { a, b string consist bool }{ "Equal": { a: "key1=value1;key2=value2;key3=value3", b: "key1=value1;key2=value2;key3=value3", consist: true, }, "EqualWithWhitespaceVariants": { a: "key1=value1;key2=value2;key3=value3", b: " key1=value1; \nkey2=value2;\t\nkey3=value3", consist: true, }, "EqualWithCase": { a: "key1=value1;key2=value2;key3=value3", b: "key1=value1;key2=Value2;Key3=value3", consist: true, }, "OneHasExtraParam": { a: "key1=value1;key2=value2;key3=value3", b: "key1=value1;key2=value2;key3=value3;key4=value4", consist: true, }, "Inconsistent": { a: "key1=value1;key2=value2;key3=value3", b: "key1=value1;key2=different_value;key3=value3", consist: false, }, "Inconsistent_OneHasExtraParam": { a: "key1=value1;key2=value2;key3=value3;key4=value4", b: "key1=value1;key2=different_value;key3=value3", consist: false, }, } for name, testCase := range testCases { testCase := testCase check := func(t *testing.T, a, b string) { aa := Parse("", a) bb := Parse("", b) c := aa.Match(bb) if c != testCase.consist { t.Errorf( "'%s' and '%s' are expected to be %s, but treated as %s", a, b, consistString[testCase.consist], consistString[c], ) } // test reverse case here c = bb.Match(aa) if c != testCase.consist { t.Errorf( "'%s' and '%s' are expected to be %s, but treated as %s", a, b, consistString[testCase.consist], consistString[c], ) } } t.Run(name, func(t *testing.T) { check(t, testCase.a, testCase.b) }) } } webrtc-3.1.56/internal/fmtp/h264.go000066400000000000000000000035031437620512100166720ustar00rootroot00000000000000package fmtp import ( "encoding/hex" ) func profileLevelIDMatches(a, b string) bool { aa, err := hex.DecodeString(a) if err != nil || len(aa) < 2 { return false } bb, err := hex.DecodeString(b) if err != nil || len(bb) < 2 { return false } return aa[0] == bb[0] && aa[1] == bb[1] } type h264FMTP struct { parameters map[string]string } func (h *h264FMTP) MimeType() string { return "video/h264" } // Match returns true if h and b are compatible fmtp descriptions // Based on RFC6184 Section 8.2.2: // The parameters identifying a media format configuration for H.264 // are profile-level-id and packetization-mode. These media format // configuration parameters (except for the level part of profile- // level-id) MUST be used symmetrically; that is, the answerer MUST // either maintain all configuration parameters or remove the media // format (payload type) completely if one or more of the parameter // values are not supported. // Informative note: The requirement for symmetric use does not // apply for the level part of profile-level-id and does not apply // for the other stream properties and capability parameters. func (h *h264FMTP) Match(b FMTP) bool { c, ok := b.(*h264FMTP) if !ok { return false } // test packetization-mode hpmode, hok := h.parameters["packetization-mode"] if !hok { return false } cpmode, cok := c.parameters["packetization-mode"] if !cok { return false } if hpmode != cpmode { return false } // test profile-level-id hplid, hok := h.parameters["profile-level-id"] if !hok { return false } cplid, cok := c.parameters["profile-level-id"] if !cok { return false } if !profileLevelIDMatches(hplid, cplid) { return false } return true } func (h *h264FMTP) Parameter(key string) (string, bool) { v, ok := h.parameters[key] return v, ok } webrtc-3.1.56/internal/fmtp/h264_test.go000066400000000000000000000075061437620512100177400ustar00rootroot00000000000000package fmtp import ( "reflect" "testing" ) func TestH264FMTPParse(t *testing.T) { testCases := map[string]struct { input string expected FMTP }{ "OneParam": { input: "key-name=value", expected: &h264FMTP{ parameters: map[string]string{ "key-name": "value", }, }, }, "OneParamWithWhiteSpeces": { input: "\tkey-name=value ", expected: &h264FMTP{ parameters: map[string]string{ "key-name": "value", }, }, }, "TwoParams": { input: "key-name=value;key2=value2", expected: &h264FMTP{ parameters: map[string]string{ "key-name": "value", "key2": "value2", }, }, }, "TwoParamsWithWhiteSpeces": { input: "key-name=value; \n\tkey2=value2 ", expected: &h264FMTP{ parameters: map[string]string{ "key-name": "value", "key2": "value2", }, }, }, } for name, testCase := range testCases { testCase := testCase t.Run(name, func(t *testing.T) { f := Parse("video/h264", testCase.input) if !reflect.DeepEqual(testCase.expected, f) { t.Errorf("Expected Fmtp params: %v, got: %v", testCase.expected, f) } if f.MimeType() != "video/h264" { t.Errorf("Expected MimeType of video/h264, got: %s", f.MimeType()) } }) } } func TestH264FMTPCompare(t *testing.T) { consistString := map[bool]string{true: "consist", false: "inconsist"} testCases := map[string]struct { a, b string consist bool }{ "Equal": { a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", b: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", consist: true, }, "EqualWithWhitespaceVariants": { a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", b: " level-asymmetry-allowed=1; \npacketization-mode=1;\t\nprofile-level-id=42e01f", consist: true, }, "EqualWithCase": { a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", b: "level-asymmetry-allowed=1;packetization-mode=1;PROFILE-LEVEL-ID=42e01f", consist: true, }, "OneHasExtraParam": { a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", b: "packetization-mode=1;profile-level-id=42e01f", consist: true, }, "DifferentProfileLevelIDVersions": { a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", b: "packetization-mode=1;profile-level-id=42e029", consist: true, }, "Inconsistent": { a: "packetization-mode=1;profile-level-id=42e029", b: "packetization-mode=0;profile-level-id=42e029", consist: false, }, "Inconsistent_MissingPacketizationMode": { a: "packetization-mode=1;profile-level-id=42e029", b: "profile-level-id=42e029", consist: false, }, "Inconsistent_MissingProfileLevelID": { a: "packetization-mode=1;profile-level-id=42e029", b: "packetization-mode=1", consist: false, }, "Inconsistent_InvalidProfileLevelID": { a: "packetization-mode=1;profile-level-id=42e029", b: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=41e029", consist: false, }, } for name, testCase := range testCases { testCase := testCase check := func(t *testing.T, a, b string) { aa := Parse("video/h264", a) bb := Parse("video/h264", b) c := aa.Match(bb) if c != testCase.consist { t.Errorf( "'%s' and '%s' are expected to be %s, but treated as %s", a, b, consistString[testCase.consist], consistString[c], ) } // test reverse case here c = bb.Match(aa) if c != testCase.consist { t.Errorf( "'%s' and '%s' are expected to be %s, but treated as %s", a, b, consistString[testCase.consist], consistString[c], ) } } t.Run(name, func(t *testing.T) { check(t, testCase.a, testCase.b) }) } } webrtc-3.1.56/internal/mux/000077500000000000000000000000001437620512100155225ustar00rootroot00000000000000webrtc-3.1.56/internal/mux/endpoint.go000066400000000000000000000027071437620512100176770ustar00rootroot00000000000000package mux import ( "errors" "io" "net" "time" "github.com/pion/ice/v2" "github.com/pion/transport/v2/packetio" ) // Endpoint implements net.Conn. It is used to read muxed packets. type Endpoint struct { mux *Mux buffer *packetio.Buffer } // Close unregisters the endpoint from the Mux func (e *Endpoint) Close() (err error) { err = e.close() if err != nil { return err } e.mux.RemoveEndpoint(e) return nil } func (e *Endpoint) close() error { return e.buffer.Close() } // Read reads a packet of len(p) bytes from the underlying conn // that are matched by the associated MuxFunc func (e *Endpoint) Read(p []byte) (int, error) { return e.buffer.Read(p) } // Write writes len(p) bytes to the underlying conn func (e *Endpoint) Write(p []byte) (int, error) { n, err := e.mux.nextConn.Write(p) if errors.Is(err, ice.ErrNoCandidatePairs) { return 0, nil } else if errors.Is(err, ice.ErrClosed) { return 0, io.ErrClosedPipe } return n, err } // LocalAddr is a stub func (e *Endpoint) LocalAddr() net.Addr { return e.mux.nextConn.LocalAddr() } // RemoteAddr is a stub func (e *Endpoint) RemoteAddr() net.Addr { return e.mux.nextConn.RemoteAddr() } // SetDeadline is a stub func (e *Endpoint) SetDeadline(t time.Time) error { return nil } // SetReadDeadline is a stub func (e *Endpoint) SetReadDeadline(t time.Time) error { return nil } // SetWriteDeadline is a stub func (e *Endpoint) SetWriteDeadline(t time.Time) error { return nil } webrtc-3.1.56/internal/mux/mux.go000066400000000000000000000061401437620512100166630ustar00rootroot00000000000000// Package mux multiplexes packets on a single socket (RFC7983) package mux import ( "errors" "io" "net" "sync" "github.com/pion/ice/v2" "github.com/pion/logging" "github.com/pion/transport/v2/packetio" ) // The maximum amount of data that can be buffered before returning errors. const maxBufferSize = 1000 * 1000 // 1MB // Config collects the arguments to mux.Mux construction into // a single structure type Config struct { Conn net.Conn BufferSize int LoggerFactory logging.LoggerFactory } // Mux allows multiplexing type Mux struct { lock sync.RWMutex nextConn net.Conn endpoints map[*Endpoint]MatchFunc bufferSize int closedCh chan struct{} log logging.LeveledLogger } // NewMux creates a new Mux func NewMux(config Config) *Mux { m := &Mux{ nextConn: config.Conn, endpoints: make(map[*Endpoint]MatchFunc), bufferSize: config.BufferSize, closedCh: make(chan struct{}), log: config.LoggerFactory.NewLogger("mux"), } go m.readLoop() return m } // NewEndpoint creates a new Endpoint func (m *Mux) NewEndpoint(f MatchFunc) *Endpoint { e := &Endpoint{ mux: m, buffer: packetio.NewBuffer(), } // Set a maximum size of the buffer in bytes. e.buffer.SetLimitSize(maxBufferSize) m.lock.Lock() m.endpoints[e] = f m.lock.Unlock() return e } // RemoveEndpoint removes an endpoint from the Mux func (m *Mux) RemoveEndpoint(e *Endpoint) { m.lock.Lock() defer m.lock.Unlock() delete(m.endpoints, e) } // Close closes the Mux and all associated Endpoints. func (m *Mux) Close() error { m.lock.Lock() for e := range m.endpoints { if err := e.close(); err != nil { m.lock.Unlock() return err } delete(m.endpoints, e) } m.lock.Unlock() err := m.nextConn.Close() if err != nil { return err } // Wait for readLoop to end <-m.closedCh return nil } func (m *Mux) readLoop() { defer func() { close(m.closedCh) }() buf := make([]byte, m.bufferSize) for { n, err := m.nextConn.Read(buf) switch { case errors.Is(err, io.EOF), errors.Is(err, ice.ErrClosed): return case errors.Is(err, io.ErrShortBuffer), errors.Is(err, packetio.ErrTimeout): m.log.Errorf("mux: failed to read from packetio.Buffer %s", err.Error()) continue case err != nil: m.log.Errorf("mux: ending readLoop packetio.Buffer error %s", err.Error()) return } if err = m.dispatch(buf[:n]); err != nil { m.log.Errorf("mux: ending readLoop dispatch error %s", err.Error()) return } } } func (m *Mux) dispatch(buf []byte) error { var endpoint *Endpoint m.lock.Lock() for e, f := range m.endpoints { if f(buf) { endpoint = e break } } m.lock.Unlock() if endpoint == nil { if len(buf) > 0 { m.log.Warnf("Warning: mux: no endpoint for packet starting with %d", buf[0]) } else { m.log.Warnf("Warning: mux: no endpoint for zero length packet") } return nil } _, err := endpoint.buffer.Write(buf) // Expected when bytes are received faster than the endpoint can process them (#2152, #2180) if errors.Is(err, packetio.ErrFull) { m.log.Infof("mux: endpoint buffer is full, dropping packet") return nil } return err } webrtc-3.1.56/internal/mux/mux_test.go000066400000000000000000000062121437620512100177220ustar00rootroot00000000000000package mux import ( "io" "net" "testing" "time" "github.com/pion/logging" "github.com/pion/transport/v2/packetio" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/require" ) const testPipeBufferSize = 8192 func TestNoEndpoints(t *testing.T) { // In memory pipe ca, cb := net.Pipe() require.NoError(t, cb.Close()) m := NewMux(Config{ Conn: ca, BufferSize: testPipeBufferSize, LoggerFactory: logging.NewDefaultLoggerFactory(), }) require.NoError(t, m.dispatch(make([]byte, 1))) require.NoError(t, m.Close()) require.NoError(t, ca.Close()) } type muxErrorConnReadResult struct { err error data []byte } // muxErrorConn type muxErrorConn struct { net.Conn readResults []muxErrorConnReadResult } func (m *muxErrorConn) Read(b []byte) (n int, err error) { err = m.readResults[0].err copy(b, m.readResults[0].data) n = len(m.readResults[0].data) m.readResults = m.readResults[1:] return } /* Don't end the mux readLoop for packetio.ErrTimeout or io.ErrShortBuffer, assert the following - io.ErrShortBuffer and packetio.ErrTimeout don't end the read loop - io.EOF ends the loop pion/webrtc#1720 */ func TestNonFatalRead(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 20) defer lim.Stop() expectedData := []byte("expectedData") // In memory pipe ca, cb := net.Pipe() require.NoError(t, cb.Close()) conn := &muxErrorConn{ca, []muxErrorConnReadResult{ // Non-fatal timeout error {packetio.ErrTimeout, nil}, {nil, expectedData}, {io.ErrShortBuffer, nil}, {nil, expectedData}, {io.EOF, nil}, }} m := NewMux(Config{ Conn: conn, BufferSize: testPipeBufferSize, LoggerFactory: logging.NewDefaultLoggerFactory(), }) e := m.NewEndpoint(MatchAll) buff := make([]byte, testPipeBufferSize) n, err := e.Read(buff) require.NoError(t, err) require.Equal(t, buff[:n], expectedData) n, err = e.Read(buff) require.NoError(t, err) require.Equal(t, buff[:n], expectedData) <-m.closedCh require.NoError(t, m.Close()) require.NoError(t, ca.Close()) } // If a endpoint returns packetio.ErrFull it is a non-fatal error and shouldn't cause // the mux to be destroyed // pion/webrtc#2180 func TestNonFatalDispatch(t *testing.T) { in, out := net.Pipe() m := NewMux(Config{ Conn: out, LoggerFactory: logging.NewDefaultLoggerFactory(), BufferSize: 1500, }) e := m.NewEndpoint(MatchSRTP) e.buffer.SetLimitSize(1) for i := 0; i <= 25; i++ { srtpPacket := []byte{128, 1, 2, 3, 4} _, err := in.Write(srtpPacket) require.NoError(t, err) } require.NoError(t, m.Close()) require.NoError(t, in.Close()) require.NoError(t, out.Close()) } func BenchmarkDispatch(b *testing.B) { m := &Mux{ endpoints: make(map[*Endpoint]MatchFunc), log: logging.NewDefaultLoggerFactory().NewLogger("mux"), } e := m.NewEndpoint(MatchSRTP) m.NewEndpoint(MatchSRTCP) buf := []byte{128, 1, 2, 3, 4} buf2 := make([]byte, 1200) b.StartTimer() for i := 0; i < b.N; i++ { err := m.dispatch(buf) if err != nil { b.Errorf("dispatch: %v", err) } _, err = e.buffer.Read(buf2) if err != nil { b.Errorf("read: %v", err) } } } webrtc-3.1.56/internal/mux/muxfunc.go000066400000000000000000000033671437620512100175470ustar00rootroot00000000000000package mux // MatchFunc allows custom logic for mapping packets to an Endpoint type MatchFunc func([]byte) bool // MatchAll always returns true func MatchAll(b []byte) bool { return true } // MatchRange returns true if the first byte of buf is in [lower..upper] func MatchRange(lower, upper byte, buf []byte) bool { if len(buf) < 1 { return false } b := buf[0] return b >= lower && b <= upper } // MatchFuncs as described in RFC7983 // https://tools.ietf.org/html/rfc7983 // +----------------+ // | [0..3] -+--> forward to STUN // | | // | [16..19] -+--> forward to ZRTP // | | // packet --> | [20..63] -+--> forward to DTLS // | | // | [64..79] -+--> forward to TURN Channel // | | // | [128..191] -+--> forward to RTP/RTCP // +----------------+ // MatchDTLS is a MatchFunc that accepts packets with the first byte in [20..63] // as defied in RFC7983 func MatchDTLS(b []byte) bool { return MatchRange(20, 63, b) } // MatchSRTPOrSRTCP is a MatchFunc that accepts packets with the first byte in [128..191] // as defied in RFC7983 func MatchSRTPOrSRTCP(b []byte) bool { return MatchRange(128, 191, b) } func isRTCP(buf []byte) bool { // Not long enough to determine RTP/RTCP if len(buf) < 4 { return false } return buf[1] >= 192 && buf[1] <= 223 } // MatchSRTP is a MatchFunc that only matches SRTP and not SRTCP func MatchSRTP(buf []byte) bool { return MatchSRTPOrSRTCP(buf) && !isRTCP(buf) } // MatchSRTCP is a MatchFunc that only matches SRTCP and not SRTP func MatchSRTCP(buf []byte) bool { return MatchSRTPOrSRTCP(buf) && isRTCP(buf) } webrtc-3.1.56/internal/util/000077500000000000000000000000001437620512100156665ustar00rootroot00000000000000webrtc-3.1.56/internal/util/util.go000066400000000000000000000030101437620512100171640ustar00rootroot00000000000000// Package util provides auxiliary functions internally used in webrtc package package util import ( "errors" "strings" "github.com/pion/randutil" ) const ( runesAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ) // Use global random generator to properly seed by crypto grade random. var globalMathRandomGenerator = randutil.NewMathRandomGenerator() // nolint:gochecknoglobals // MathRandAlpha generates a mathmatical random alphabet sequence of the requested length. func MathRandAlpha(n int) string { return globalMathRandomGenerator.GenerateString(n, runesAlpha) } // RandUint32 generates a mathmatical random uint32. func RandUint32() uint32 { return globalMathRandomGenerator.Uint32() } // FlattenErrs flattens multiple errors into one func FlattenErrs(errs []error) error { errs2 := []error{} for _, e := range errs { if e != nil { errs2 = append(errs2, e) } } if len(errs2) == 0 { return nil } return multiError(errs2) } type multiError []error //nolint:errname func (me multiError) Error() string { var errstrings []string for _, err := range me { if err != nil { errstrings = append(errstrings, err.Error()) } } if len(errstrings) == 0 { return "multiError must contain multiple error but is empty" } return strings.Join(errstrings, "\n") } func (me multiError) Is(err error) bool { for _, e := range me { if errors.Is(e, err) { return true } if me2, ok := e.(multiError); ok { //nolint:errorlint if me2.Is(err) { return true } } } return false } webrtc-3.1.56/internal/util/util_test.go000066400000000000000000000021351437620512100202320ustar00rootroot00000000000000package util import ( "errors" "regexp" "testing" ) func TestMathRandAlpha(t *testing.T) { if len(MathRandAlpha(10)) != 10 { t.Errorf("MathRandAlpha return invalid length") } isLetter := regexp.MustCompile(`^[a-zA-Z]+$`).MatchString if !isLetter(MathRandAlpha(10)) { t.Errorf("MathRandAlpha should be AlphaNumeric only") } } func TestMultiError(t *testing.T) { rawErrs := []error{ errors.New("err1"), //nolint errors.New("err2"), //nolint errors.New("err3"), //nolint errors.New("err4"), //nolint } errs := FlattenErrs([]error{ rawErrs[0], nil, rawErrs[1], FlattenErrs([]error{ rawErrs[2], }), }) str := "err1\nerr2\nerr3" if errs.Error() != str { t.Errorf("String representation doesn't match, expected: %s, got: %s", errs.Error(), str) } errIs, ok := errs.(multiError) //nolint:errorlint if !ok { t.Fatal("FlattenErrs returns non-multiError") } for i := 0; i < 3; i++ { if !errIs.Is(rawErrs[i]) { t.Errorf("'%+v' should contains '%v'", errs, rawErrs[i]) } } if errIs.Is(rawErrs[3]) { t.Errorf("'%+v' should not contains '%v'", errs, rawErrs[3]) } } webrtc-3.1.56/js_utils.go000066400000000000000000000065461437620512100152730ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import ( "fmt" "syscall/js" ) // awaitPromise accepts a js.Value representing a Promise. If the promise // resolves, it returns (result, nil). If the promise rejects, it returns // (js.Undefined, error). awaitPromise has a synchronous-like API but does not // block the JavaScript event loop. func awaitPromise(promise js.Value) (js.Value, error) { resultsChan := make(chan js.Value) errChan := make(chan js.Error) thenFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { go func() { resultsChan <- args[0] }() return js.Undefined() }) defer thenFunc.Release() catchFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { go func() { errChan <- js.Error{args[0]} }() return js.Undefined() }) defer catchFunc.Release() promise.Call("then", thenFunc).Call("catch", catchFunc) select { case result := <-resultsChan: return result, nil case err := <-errChan: return js.Undefined(), err } } func valueToUint16Pointer(val js.Value) *uint16 { if val.IsNull() || val.IsUndefined() { return nil } convertedVal := uint16(val.Int()) return &convertedVal } func valueToStringPointer(val js.Value) *string { if val.IsNull() || val.IsUndefined() { return nil } stringVal := val.String() return &stringVal } func stringToValueOrUndefined(val string) js.Value { if val == "" { return js.Undefined() } return js.ValueOf(val) } func uint8ToValueOrUndefined(val uint8) js.Value { if val == 0 { return js.Undefined() } return js.ValueOf(val) } func interfaceToValueOrUndefined(val interface{}) js.Value { if val == nil { return js.Undefined() } return js.ValueOf(val) } func valueToStringOrZero(val js.Value) string { if val.IsUndefined() || val.IsNull() { return "" } return val.String() } func valueToUint8OrZero(val js.Value) uint8 { if val.IsUndefined() || val.IsNull() { return 0 } return uint8(val.Int()) } func valueToUint16OrZero(val js.Value) uint16 { if val.IsNull() || val.IsUndefined() { return 0 } return uint16(val.Int()) } func valueToUint32OrZero(val js.Value) uint32 { if val.IsNull() || val.IsUndefined() { return 0 } return uint32(val.Int()) } func valueToStrings(val js.Value) []string { result := make([]string, val.Length()) for i := 0; i < val.Length(); i++ { result[i] = val.Index(i).String() } return result } func stringPointerToValue(val *string) js.Value { if val == nil { return js.Undefined() } return js.ValueOf(*val) } func uint16PointerToValue(val *uint16) js.Value { if val == nil { return js.Undefined() } return js.ValueOf(*val) } func boolPointerToValue(val *bool) js.Value { if val == nil { return js.Undefined() } return js.ValueOf(*val) } func stringsToValue(strings []string) js.Value { val := make([]interface{}, len(strings)) for i, s := range strings { val[i] = s } return js.ValueOf(val) } func stringEnumToValueOrUndefined(s string) js.Value { if s == "unknown" { return js.Undefined() } return js.ValueOf(s) } // Converts the return value of recover() to an error. func recoveryToError(e interface{}) error { switch e := e.(type) { case error: return e default: return fmt.Errorf("recovered with non-error value: (%T) %s", e, e) } } func uint8ArrayValueToBytes(val js.Value) []byte { result := make([]byte, val.Length()) js.CopyBytesToGo(result, val) return result } webrtc-3.1.56/mediaengine.go000066400000000000000000000455671437620512100157120ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "fmt" "strconv" "strings" "sync" "time" "github.com/pion/rtp" "github.com/pion/rtp/codecs" "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3/internal/fmtp" ) const ( // MimeTypeH264 H264 MIME type. // Note: Matching should be case insensitive. MimeTypeH264 = "video/H264" // MimeTypeH265 H265 MIME type // Note: Matching should be case insensitive. MimeTypeH265 = "video/H265" // MimeTypeOpus Opus MIME type // Note: Matching should be case insensitive. MimeTypeOpus = "audio/opus" // MimeTypeVP8 VP8 MIME type // Note: Matching should be case insensitive. MimeTypeVP8 = "video/VP8" // MimeTypeVP9 VP9 MIME type // Note: Matching should be case insensitive. MimeTypeVP9 = "video/VP9" // MimeTypeAV1 AV1 MIME type // Note: Matching should be case insensitive. MimeTypeAV1 = "video/AV1" // MimeTypeG722 G722 MIME type // Note: Matching should be case insensitive. MimeTypeG722 = "audio/G722" // MimeTypePCMU PCMU MIME type // Note: Matching should be case insensitive. MimeTypePCMU = "audio/PCMU" // MimeTypePCMA PCMA MIME type // Note: Matching should be case insensitive. MimeTypePCMA = "audio/PCMA" ) type mediaEngineHeaderExtension struct { uri string isAudio, isVideo bool // If set only Transceivers of this direction are allowed allowedDirections []RTPTransceiverDirection } // A MediaEngine defines the codecs supported by a PeerConnection, and the // configuration of those codecs. A MediaEngine must not be shared between // PeerConnections. type MediaEngine struct { // If we have attempted to negotiate a codec type yet. negotiatedVideo, negotiatedAudio bool videoCodecs, audioCodecs []RTPCodecParameters negotiatedVideoCodecs, negotiatedAudioCodecs []RTPCodecParameters headerExtensions []mediaEngineHeaderExtension negotiatedHeaderExtensions map[int]mediaEngineHeaderExtension mu sync.RWMutex } // RegisterDefaultCodecs registers the default codecs supported by Pion WebRTC. // RegisterDefaultCodecs is not safe for concurrent use. func (m *MediaEngine) RegisterDefaultCodecs() error { // Default Pion Audio Codecs for _, codec := range []RTPCodecParameters{ { RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil}, PayloadType: 111, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeG722, 8000, 0, "", nil}, PayloadType: 9, }, { RTPCodecCapability: RTPCodecCapability{MimeTypePCMU, 8000, 0, "", nil}, PayloadType: 0, }, { RTPCodecCapability: RTPCodecCapability{MimeTypePCMA, 8000, 0, "", nil}, PayloadType: 8, }, } { if err := m.RegisterCodec(codec, RTPCodecTypeAudio); err != nil { return err } } videoRTCPFeedback := []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}} for _, codec := range []RTPCodecParameters{ { RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", videoRTCPFeedback}, PayloadType: 96, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, PayloadType: 97, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", videoRTCPFeedback}, PayloadType: 98, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil}, PayloadType: 99, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=1", videoRTCPFeedback}, PayloadType: 100, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=100", nil}, PayloadType: 101, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", videoRTCPFeedback}, PayloadType: 102, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=102", nil}, PayloadType: 121, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", videoRTCPFeedback}, PayloadType: 127, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=127", nil}, PayloadType: 120, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", videoRTCPFeedback}, PayloadType: 125, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=125", nil}, PayloadType: 107, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", videoRTCPFeedback}, PayloadType: 108, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=108", nil}, PayloadType: 109, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", videoRTCPFeedback}, PayloadType: 127, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=127", nil}, PayloadType: 120, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", videoRTCPFeedback}, PayloadType: 123, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=123", nil}, PayloadType: 118, }, { RTPCodecCapability: RTPCodecCapability{"video/ulpfec", 90000, 0, "", nil}, PayloadType: 116, }, } { if err := m.RegisterCodec(codec, RTPCodecTypeVideo); err != nil { return err } } return nil } // addCodec will append codec if it not exists func (m *MediaEngine) addCodec(codecs []RTPCodecParameters, codec RTPCodecParameters) []RTPCodecParameters { for _, c := range codecs { if c.MimeType == codec.MimeType && c.PayloadType == codec.PayloadType { return codecs } } return append(codecs, codec) } // RegisterCodec adds codec to the MediaEngine // These are the list of codecs supported by this PeerConnection. // RegisterCodec is not safe for concurrent use. func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType) error { m.mu.Lock() defer m.mu.Unlock() codec.statsID = fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano()) switch typ { case RTPCodecTypeAudio: m.audioCodecs = m.addCodec(m.audioCodecs, codec) case RTPCodecTypeVideo: m.videoCodecs = m.addCodec(m.videoCodecs, codec) default: return ErrUnknownType } return nil } // RegisterHeaderExtension adds a header extension to the MediaEngine // To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapability, typ RTPCodecType, allowedDirections ...RTPTransceiverDirection) error { m.mu.Lock() defer m.mu.Unlock() if m.negotiatedHeaderExtensions == nil { m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{} } if len(allowedDirections) == 0 { allowedDirections = []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly} } for _, direction := range allowedDirections { if direction != RTPTransceiverDirectionRecvonly && direction != RTPTransceiverDirectionSendonly { return ErrRegisterHeaderExtensionInvalidDirection } } extensionIndex := -1 for i := range m.headerExtensions { if extension.URI == m.headerExtensions[i].uri { extensionIndex = i } } if extensionIndex == -1 { m.headerExtensions = append(m.headerExtensions, mediaEngineHeaderExtension{}) extensionIndex = len(m.headerExtensions) - 1 } if typ == RTPCodecTypeAudio { m.headerExtensions[extensionIndex].isAudio = true } else if typ == RTPCodecTypeVideo { m.headerExtensions[extensionIndex].isVideo = true } m.headerExtensions[extensionIndex].uri = extension.URI m.headerExtensions[extensionIndex].allowedDirections = allowedDirections return nil } // RegisterFeedback adds feedback mechanism to already registered codecs. func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType) { m.mu.Lock() defer m.mu.Unlock() switch typ { case RTPCodecTypeVideo: for i, v := range m.videoCodecs { v.RTCPFeedback = append(v.RTCPFeedback, feedback) m.videoCodecs[i] = v } case RTPCodecTypeAudio: for i, v := range m.audioCodecs { v.RTCPFeedback = append(v.RTCPFeedback, feedback) m.audioCodecs[i] = v } } } // getHeaderExtensionID returns the negotiated ID for a header extension. // If the Header Extension isn't enabled ok will be false func (m *MediaEngine) getHeaderExtensionID(extension RTPHeaderExtensionCapability) (val int, audioNegotiated, videoNegotiated bool) { m.mu.RLock() defer m.mu.RUnlock() if m.negotiatedHeaderExtensions == nil { return 0, false, false } for id, h := range m.negotiatedHeaderExtensions { if extension.URI == h.uri { return id, h.isAudio, h.isVideo } } return } // copy copies any user modifiable state of the MediaEngine // all internal state is reset func (m *MediaEngine) copy() *MediaEngine { m.mu.Lock() defer m.mu.Unlock() cloned := &MediaEngine{ videoCodecs: append([]RTPCodecParameters{}, m.videoCodecs...), audioCodecs: append([]RTPCodecParameters{}, m.audioCodecs...), headerExtensions: append([]mediaEngineHeaderExtension{}, m.headerExtensions...), } if len(m.headerExtensions) > 0 { cloned.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{} } return cloned } func findCodecByPayload(codecs []RTPCodecParameters, payloadType PayloadType) *RTPCodecParameters { for _, codec := range codecs { if codec.PayloadType == payloadType { return &codec } } return nil } func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParameters, RTPCodecType, error) { m.mu.RLock() defer m.mu.RUnlock() // if we've negotiated audio or video, check the negotiated types before our // built-in payload types, to ensure we pick the codec the other side wants. if m.negotiatedVideo { if codec := findCodecByPayload(m.negotiatedVideoCodecs, payloadType); codec != nil { return *codec, RTPCodecTypeVideo, nil } } if m.negotiatedAudio { if codec := findCodecByPayload(m.negotiatedAudioCodecs, payloadType); codec != nil { return *codec, RTPCodecTypeAudio, nil } } if !m.negotiatedVideo { if codec := findCodecByPayload(m.videoCodecs, payloadType); codec != nil { return *codec, RTPCodecTypeVideo, nil } } if !m.negotiatedAudio { if codec := findCodecByPayload(m.audioCodecs, payloadType); codec != nil { return *codec, RTPCodecTypeAudio, nil } } return RTPCodecParameters{}, 0, ErrCodecNotFound } func (m *MediaEngine) collectStats(collector *statsReportCollector) { m.mu.RLock() defer m.mu.RUnlock() statsLoop := func(codecs []RTPCodecParameters) { for _, codec := range codecs { collector.Collecting() stats := CodecStats{ Timestamp: statsTimestampFrom(time.Now()), Type: StatsTypeCodec, ID: codec.statsID, PayloadType: codec.PayloadType, MimeType: codec.MimeType, ClockRate: codec.ClockRate, Channels: uint8(codec.Channels), SDPFmtpLine: codec.SDPFmtpLine, } collector.Collect(stats.ID, stats) } } statsLoop(m.videoCodecs) statsLoop(m.audioCodecs) } // Look up a codec and enable if it exists func (m *MediaEngine) matchRemoteCodec(remoteCodec RTPCodecParameters, typ RTPCodecType, exactMatches, partialMatches []RTPCodecParameters) (codecMatchType, error) { codecs := m.videoCodecs if typ == RTPCodecTypeAudio { codecs = m.audioCodecs } remoteFmtp := fmtp.Parse(remoteCodec.RTPCodecCapability.MimeType, remoteCodec.RTPCodecCapability.SDPFmtpLine) if apt, hasApt := remoteFmtp.Parameter("apt"); hasApt { payloadType, err := strconv.ParseUint(apt, 10, 8) if err != nil { return codecMatchNone, err } aptMatch := codecMatchNone for _, codec := range exactMatches { if codec.PayloadType == PayloadType(payloadType) { aptMatch = codecMatchExact break } } if aptMatch == codecMatchNone { for _, codec := range partialMatches { if codec.PayloadType == PayloadType(payloadType) { aptMatch = codecMatchPartial break } } } if aptMatch == codecMatchNone { return codecMatchNone, nil // not an error, we just ignore this codec we don't support } // if apt's media codec is partial match, then apt codec must be partial match too _, matchType := codecParametersFuzzySearch(remoteCodec, codecs) if matchType == codecMatchExact && aptMatch == codecMatchPartial { matchType = codecMatchPartial } return matchType, nil } _, matchType := codecParametersFuzzySearch(remoteCodec, codecs) return matchType, nil } // Look up a header extension and enable if it exists func (m *MediaEngine) updateHeaderExtension(id int, extension string, typ RTPCodecType) error { if m.negotiatedHeaderExtensions == nil { return nil } for _, localExtension := range m.headerExtensions { if localExtension.uri == extension { h := mediaEngineHeaderExtension{uri: extension, allowedDirections: localExtension.allowedDirections} if existingValue, ok := m.negotiatedHeaderExtensions[id]; ok { h = existingValue } switch { case localExtension.isAudio && typ == RTPCodecTypeAudio: h.isAudio = true case localExtension.isVideo && typ == RTPCodecTypeVideo: h.isVideo = true } m.negotiatedHeaderExtensions[id] = h } } return nil } func (m *MediaEngine) pushCodecs(codecs []RTPCodecParameters, typ RTPCodecType) { for _, codec := range codecs { if typ == RTPCodecTypeAudio { m.negotiatedAudioCodecs = m.addCodec(m.negotiatedAudioCodecs, codec) } else if typ == RTPCodecTypeVideo { m.negotiatedVideoCodecs = m.addCodec(m.negotiatedVideoCodecs, codec) } } } // Update the MediaEngine from a remote description func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) error { m.mu.Lock() defer m.mu.Unlock() for _, media := range desc.MediaDescriptions { var typ RTPCodecType switch { case !m.negotiatedAudio && strings.EqualFold(media.MediaName.Media, "audio"): m.negotiatedAudio = true typ = RTPCodecTypeAudio case !m.negotiatedVideo && strings.EqualFold(media.MediaName.Media, "video"): m.negotiatedVideo = true typ = RTPCodecTypeVideo default: continue } codecs, err := codecsFromMediaDescription(media) if err != nil { return err } exactMatches := make([]RTPCodecParameters, 0, len(codecs)) partialMatches := make([]RTPCodecParameters, 0, len(codecs)) for _, codec := range codecs { matchType, mErr := m.matchRemoteCodec(codec, typ, exactMatches, partialMatches) if mErr != nil { return mErr } if matchType == codecMatchExact { exactMatches = append(exactMatches, codec) } else if matchType == codecMatchPartial { partialMatches = append(partialMatches, codec) } } // use exact matches when they exist, otherwise fall back to partial switch { case len(exactMatches) > 0: m.pushCodecs(exactMatches, typ) case len(partialMatches) > 0: m.pushCodecs(partialMatches, typ) default: // no match, not negotiated continue } extensions, err := rtpExtensionsFromMediaDescription(media) if err != nil { return err } for extension, id := range extensions { if err = m.updateHeaderExtension(id, extension, typ); err != nil { return err } } } return nil } func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters { m.mu.RLock() defer m.mu.RUnlock() if typ == RTPCodecTypeVideo { if m.negotiatedVideo { return m.negotiatedVideoCodecs } return m.videoCodecs } else if typ == RTPCodecTypeAudio { if m.negotiatedAudio { return m.negotiatedAudioCodecs } return m.audioCodecs } return nil } func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType, directions []RTPTransceiverDirection) RTPParameters { //nolint:gocognit headerExtensions := make([]RTPHeaderExtensionParameter, 0) // perform before locking to prevent recursive RLocks foundCodecs := m.getCodecsByKind(typ) m.mu.RLock() defer m.mu.RUnlock() if m.negotiatedVideo && typ == RTPCodecTypeVideo || m.negotiatedAudio && typ == RTPCodecTypeAudio { for id, e := range m.negotiatedHeaderExtensions { if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) && (e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) { headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri}) } } } else { mediaHeaderExtensions := make(map[int]mediaEngineHeaderExtension) for _, e := range m.headerExtensions { usingNegotiatedID := false for id := range m.negotiatedHeaderExtensions { if m.negotiatedHeaderExtensions[id].uri == e.uri { usingNegotiatedID = true mediaHeaderExtensions[id] = e break } } if !usingNegotiatedID { for id := 1; id < 15; id++ { idAvailable := true if _, ok := mediaHeaderExtensions[id]; ok { idAvailable = false } if _, taken := m.negotiatedHeaderExtensions[id]; idAvailable && !taken { mediaHeaderExtensions[id] = e break } } } } for id, e := range mediaHeaderExtensions { if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) && (e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) { headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri}) } } } return RTPParameters{ HeaderExtensions: headerExtensions, Codecs: foundCodecs, } } func (m *MediaEngine) getRTPParametersByPayloadType(payloadType PayloadType) (RTPParameters, error) { codec, typ, err := m.getCodecByPayload(payloadType) if err != nil { return RTPParameters{}, err } m.mu.RLock() defer m.mu.RUnlock() headerExtensions := make([]RTPHeaderExtensionParameter, 0) for id, e := range m.negotiatedHeaderExtensions { if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo { headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri}) } } return RTPParameters{ HeaderExtensions: headerExtensions, Codecs: []RTPCodecParameters{codec}, }, nil } func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) { switch strings.ToLower(codec.MimeType) { case strings.ToLower(MimeTypeH264): return &codecs.H264Payloader{}, nil case strings.ToLower(MimeTypeOpus): return &codecs.OpusPayloader{}, nil case strings.ToLower(MimeTypeVP8): return &codecs.VP8Payloader{ EnablePictureID: true, }, nil case strings.ToLower(MimeTypeVP9): return &codecs.VP9Payloader{}, nil case strings.ToLower(MimeTypeAV1): return &codecs.AV1Payloader{}, nil case strings.ToLower(MimeTypeG722): return &codecs.G722Payloader{}, nil case strings.ToLower(MimeTypePCMU), strings.ToLower(MimeTypePCMA): return &codecs.G711Payloader{}, nil default: return nil, ErrNoPayloaderForCodec } } webrtc-3.1.56/mediaengine_test.go000066400000000000000000000472661437620512100167470ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "fmt" "regexp" "strings" "testing" "github.com/pion/sdp/v3" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) // pion/webrtc#1078 func TestOpusCase(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = pc.AddTransceiverFromKind(RTPCodecTypeAudio) assert.NoError(t, err) offer, err := pc.CreateOffer(nil) assert.NoError(t, err) assert.True(t, regexp.MustCompile(`(?m)^a=rtpmap:\d+ opus/48000/2`).MatchString(offer.SDP)) assert.NoError(t, pc.Close()) } // pion/example-webrtc-applications#89 func TestVideoCase(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = pc.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) offer, err := pc.CreateOffer(nil) assert.NoError(t, err) assert.True(t, regexp.MustCompile(`(?m)^a=rtpmap:\d+ H264/90000`).MatchString(offer.SDP)) assert.True(t, regexp.MustCompile(`(?m)^a=rtpmap:\d+ VP8/90000`).MatchString(offer.SDP)) assert.True(t, regexp.MustCompile(`(?m)^a=rtpmap:\d+ VP9/90000`).MatchString(offer.SDP)) assert.NoError(t, pc.Close()) } func TestMediaEngineRemoteDescription(t *testing.T) { mustParse := func(raw string) sdp.SessionDescription { s := sdp.SessionDescription{} assert.NoError(t, s.Unmarshal([]byte(raw))) return s } t.Run("No Media", func(t *testing.T) { const noMedia = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 ` m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) assert.NoError(t, m.updateFromRemoteDescription(mustParse(noMedia))) assert.False(t, m.negotiatedVideo) assert.False(t, m.negotiatedAudio) }) t.Run("Enable Opus", func(t *testing.T) { const opusSamePayload = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 9 UDP/TLS/RTP/SAVPF 111 a=rtpmap:111 opus/48000/2 a=fmtp:111 minptime=10; useinbandfec=1 ` m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload))) assert.False(t, m.negotiatedVideo) assert.True(t, m.negotiatedAudio) opusCodec, _, err := m.getCodecByPayload(111) assert.NoError(t, err) assert.Equal(t, opusCodec.MimeType, MimeTypeOpus) }) t.Run("Change Payload Type", func(t *testing.T) { const opusSamePayload = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 9 UDP/TLS/RTP/SAVPF 112 a=rtpmap:112 opus/48000/2 a=fmtp:112 minptime=10; useinbandfec=1 ` m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload))) assert.False(t, m.negotiatedVideo) assert.True(t, m.negotiatedAudio) _, _, err := m.getCodecByPayload(111) assert.Error(t, err) opusCodec, _, err := m.getCodecByPayload(112) assert.NoError(t, err) assert.Equal(t, opusCodec.MimeType, MimeTypeOpus) }) t.Run("Ambiguous Payload Type", func(t *testing.T) { const opusSamePayload = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 9 UDP/TLS/RTP/SAVPF 96 a=rtpmap:96 opus/48000/2 a=fmtp:96 minptime=10; useinbandfec=1 ` m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload))) assert.False(t, m.negotiatedVideo) assert.True(t, m.negotiatedAudio) opusCodec, _, err := m.getCodecByPayload(96) assert.NoError(t, err) assert.Equal(t, opusCodec.MimeType, MimeTypeOpus) }) t.Run("Case Insensitive", func(t *testing.T) { const opusUpcase = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 9 UDP/TLS/RTP/SAVPF 111 a=rtpmap:111 OPUS/48000/2 a=fmtp:111 minptime=10; useinbandfec=1 ` m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusUpcase))) assert.False(t, m.negotiatedVideo) assert.True(t, m.negotiatedAudio) opusCodec, _, err := m.getCodecByPayload(111) assert.NoError(t, err) assert.Equal(t, opusCodec.MimeType, "audio/OPUS") }) t.Run("Handle different fmtp", func(t *testing.T) { const opusNoFmtp = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 9 UDP/TLS/RTP/SAVPF 111 a=rtpmap:111 opus/48000/2 ` m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusNoFmtp))) assert.False(t, m.negotiatedVideo) assert.True(t, m.negotiatedAudio) opusCodec, _, err := m.getCodecByPayload(111) assert.NoError(t, err) assert.Equal(t, opusCodec.MimeType, MimeTypeOpus) }) t.Run("Header Extensions", func(t *testing.T) { const headerExtensions = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 9 UDP/TLS/RTP/SAVPF 111 a=extmap:7 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=rtpmap:111 opus/48000/2 ` m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) registerSimulcastHeaderExtensions(&m, RTPCodecTypeAudio) assert.NoError(t, m.updateFromRemoteDescription(mustParse(headerExtensions))) assert.False(t, m.negotiatedVideo) assert.True(t, m.negotiatedAudio) absID, absAudioEnabled, absVideoEnabled := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.ABSSendTimeURI}) assert.Equal(t, absID, 0) assert.False(t, absAudioEnabled) assert.False(t, absVideoEnabled) midID, midAudioEnabled, midVideoEnabled := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI}) assert.Equal(t, midID, 7) assert.True(t, midAudioEnabled) assert.False(t, midVideoEnabled) }) t.Run("Prefers exact codec matches", func(t *testing.T) { const profileLevels = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=video 60323 UDP/TLS/RTP/SAVPF 96 98 a=rtpmap:96 H264/90000 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f a=rtpmap:98 H264/90000 a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f ` m := MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", nil}, PayloadType: 127, }, RTPCodecTypeVideo)) assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) assert.True(t, m.negotiatedVideo) assert.False(t, m.negotiatedAudio) supportedH264, _, err := m.getCodecByPayload(98) assert.NoError(t, err) assert.Equal(t, supportedH264.MimeType, MimeTypeH264) _, _, err = m.getCodecByPayload(96) assert.Error(t, err) }) t.Run("Does not match when fmtpline is set and does not match", func(t *testing.T) { const profileLevels = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=video 60323 UDP/TLS/RTP/SAVPF 96 98 a=rtpmap:96 H264/90000 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f ` m := MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", nil}, PayloadType: 127, }, RTPCodecTypeVideo)) assert.Error(t, m.updateFromRemoteDescription(mustParse(profileLevels))) _, _, err := m.getCodecByPayload(96) assert.Error(t, err) }) t.Run("Matches when fmtpline is not set in offer, but exists in mediaengine", func(t *testing.T) { const profileLevels = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=video 60323 UDP/TLS/RTP/SAVPF 96 a=rtpmap:96 VP9/90000 ` m := MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", nil}, PayloadType: 98, }, RTPCodecTypeVideo)) assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) assert.True(t, m.negotiatedVideo) _, _, err := m.getCodecByPayload(96) assert.NoError(t, err) }) t.Run("Matches when fmtpline exists in neither", func(t *testing.T) { const profileLevels = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=video 60323 UDP/TLS/RTP/SAVPF 96 a=rtpmap:96 VP8/90000 ` m := MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 96, }, RTPCodecTypeVideo)) assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) assert.True(t, m.negotiatedVideo) _, _, err := m.getCodecByPayload(96) assert.NoError(t, err) }) t.Run("Matches when rtx apt for exact match codec", func(t *testing.T) { const profileLevels = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=video 60323 UDP/TLS/RTP/SAVPF 94 96 97 a=rtpmap:94 VP8/90000 a=rtpmap:96 VP9/90000 a=fmtp:96 profile-id=2 a=rtpmap:97 rtx/90000 a=fmtp:97 apt=96 ` m := MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 94, }, RTPCodecTypeVideo)) assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=2", nil}, PayloadType: 96, }, RTPCodecTypeVideo)) assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, PayloadType: 97, }, RTPCodecTypeVideo)) assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) assert.True(t, m.negotiatedVideo) _, _, err := m.getCodecByPayload(97) assert.NoError(t, err) }) t.Run("Matches when rtx apt for partial match codec", func(t *testing.T) { const profileLevels = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=video 60323 UDP/TLS/RTP/SAVPF 94 96 97 a=rtpmap:94 VP8/90000 a=rtpmap:96 VP9/90000 a=fmtp:96 profile-id=2 a=rtpmap:97 rtx/90000 a=fmtp:97 apt=96 ` m := MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 94, }, RTPCodecTypeVideo)) assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=1", nil}, PayloadType: 96, }, RTPCodecTypeVideo)) assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, PayloadType: 97, }, RTPCodecTypeVideo)) assert.NoError(t, m.updateFromRemoteDescription(mustParse(profileLevels))) assert.True(t, m.negotiatedVideo) _, _, err := m.getCodecByPayload(97) assert.ErrorIs(t, err, ErrCodecNotFound) }) } func TestMediaEngineHeaderExtensionDirection(t *testing.T) { report := test.CheckRoutines(t) defer report() registerCodec := func(m *MediaEngine) { assert.NoError(t, m.RegisterCodec( RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 0, "", nil}, PayloadType: 111, }, RTPCodecTypeAudio)) } t.Run("No Direction", func(t *testing.T) { m := &MediaEngine{} registerCodec(m) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio)) params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) assert.Equal(t, 1, len(params.HeaderExtensions)) }) t.Run("Same Direction", func(t *testing.T) { m := &MediaEngine{} registerCodec(m) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionRecvonly)) params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) assert.Equal(t, 1, len(params.HeaderExtensions)) }) t.Run("Different Direction", func(t *testing.T) { m := &MediaEngine{} registerCodec(m) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionSendonly)) params := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) assert.Equal(t, 0, len(params.HeaderExtensions)) }) t.Run("Invalid Direction", func(t *testing.T) { m := &MediaEngine{} registerCodec(m) assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionSendrecv), ErrRegisterHeaderExtensionInvalidDirection) assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionInactive), ErrRegisterHeaderExtensionInvalidDirection) assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirection(0)), ErrRegisterHeaderExtensionInvalidDirection) }) t.Run("Unique extmapid with different codec", func(t *testing.T) { m := &MediaEngine{} registerCodec(m) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio)) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test2"}, RTPCodecTypeVideo)) audio := m.getRTPParametersByKind(RTPCodecTypeAudio, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) video := m.getRTPParametersByKind(RTPCodecTypeVideo, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) assert.Equal(t, 1, len(audio.HeaderExtensions)) assert.Equal(t, 1, len(video.HeaderExtensions)) assert.NotEqual(t, audio.HeaderExtensions[0].ID, video.HeaderExtensions[0].ID) }) } // If a user attempts to register a codec twice we should just discard duplicate calls func TestMediaEngineDoubleRegister(t *testing.T) { m := MediaEngine{} assert.NoError(t, m.RegisterCodec( RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 0, "", nil}, PayloadType: 111, }, RTPCodecTypeAudio)) assert.NoError(t, m.RegisterCodec( RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 0, "", nil}, PayloadType: 111, }, RTPCodecTypeAudio)) assert.Equal(t, len(m.audioCodecs), 1) } // The cloned MediaEngine instance should be able to update negotiated header extensions. func TestUpdateHeaderExtenstionToClonedMediaEngine(t *testing.T) { src := MediaEngine{} assert.NoError(t, src.RegisterCodec( RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 0, "", nil}, PayloadType: 111, }, RTPCodecTypeAudio)) assert.NoError(t, src.RegisterHeaderExtension(RTPHeaderExtensionCapability{"test-extension"}, RTPCodecTypeAudio)) validate := func(m *MediaEngine) { assert.NoError(t, m.updateHeaderExtension(2, "test-extension", RTPCodecTypeAudio)) id, audioNegotiated, videoNegotiated := m.getHeaderExtensionID(RTPHeaderExtensionCapability{URI: "test-extension"}) assert.Equal(t, 2, id) assert.True(t, audioNegotiated) assert.False(t, videoNegotiated) } validate(&src) validate(src.copy()) } func TestExtensionIdCollision(t *testing.T) { mustParse := func(raw string) sdp.SessionDescription { s := sdp.SessionDescription{} assert.NoError(t, s.Unmarshal([]byte(raw))) return s } sdpSnippet := `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=audio 9 UDP/TLS/RTP/SAVPF 111 a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:mid a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id a=rtpmap:111 opus/48000/2 ` m := MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{sdp.SDESMidURI}, RTPCodecTypeVideo)) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"urn:3gpp:video-orientation"}, RTPCodecTypeVideo)) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{sdp.SDESMidURI}, RTPCodecTypeAudio)) assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{sdp.AudioLevelURI}, RTPCodecTypeAudio)) assert.NoError(t, m.updateFromRemoteDescription(mustParse(sdpSnippet))) assert.True(t, m.negotiatedAudio) assert.False(t, m.negotiatedVideo) id, audioNegotiated, videoNegotiated := m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.ABSSendTimeURI}) assert.Equal(t, id, 0) assert.False(t, audioNegotiated) assert.False(t, videoNegotiated) id, audioNegotiated, videoNegotiated = m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI}) assert.Equal(t, id, 2) assert.True(t, audioNegotiated) assert.False(t, videoNegotiated) id, audioNegotiated, videoNegotiated = m.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.AudioLevelURI}) assert.Equal(t, id, 1) assert.True(t, audioNegotiated) assert.False(t, videoNegotiated) params := m.getRTPParametersByKind(RTPCodecTypeVideo, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}) extensions := params.HeaderExtensions assert.Equal(t, 2, len(extensions)) midIndex := -1 if extensions[0].URI == sdp.SDESMidURI { midIndex = 0 } else if extensions[1].URI == sdp.SDESMidURI { midIndex = 1 } voIndex := -1 if extensions[0].URI == "urn:3gpp:video-orientation" { voIndex = 0 } else if extensions[1].URI == "urn:3gpp:video-orientation" { voIndex = 1 } assert.NotEqual(t, midIndex, -1) assert.NotEqual(t, voIndex, -1) assert.Equal(t, 2, extensions[midIndex].ID) assert.NotEqual(t, 1, extensions[voIndex].ID) assert.NotEqual(t, 2, extensions[voIndex].ID) assert.NotEqual(t, 5, extensions[voIndex].ID) } func TestCaseInsensitiveMimeType(t *testing.T) { const offerSdp = ` v=0 o=- 8448668841136641781 4 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE 0 1 2 a=extmap-allow-mixed a=msid-semantic: WMS 4beea6b0-cf95-449c-a1ec-78e16b247426 m=video 9 UDP/TLS/RTP/SAVPF 96 127 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:1/MvHwjAyVf27aLu a=ice-pwd:3dBU7cFOBl120v33cynDvN1E a=ice-options:google-ice a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 a=setup:actpass a=mid:1 a=sendonly a=rtpmap:96 VP8/90000 a=rtcp-fb:96 goog-remb a=rtcp-fb:96 transport-cc a=rtcp-fb:96 ccm fir a=rtcp-fb:96 nack a=rtcp-fb:96 nack pli a=rtpmap:127 H264/90000 a=rtcp-fb:127 goog-remb a=rtcp-fb:127 transport-cc a=rtcp-fb:127 ccm fir a=rtcp-fb:127 nack a=rtcp-fb:127 nack pli a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f ` for _, mimeTypeVp8 := range []string{ "video/vp8", "video/VP8", } { t.Run(fmt.Sprintf("MimeType: %s", mimeTypeVp8), func(t *testing.T) { me := &MediaEngine{} feedback := []RTCPFeedback{ {Type: TypeRTCPFBTransportCC}, {Type: TypeRTCPFBCCM, Parameter: "fir"}, {Type: TypeRTCPFBNACK}, {Type: TypeRTCPFBNACK, Parameter: "pli"}, } for _, codec := range []RTPCodecParameters{ { RTPCodecCapability: RTPCodecCapability{MimeType: mimeTypeVp8, ClockRate: 90000, RTCPFeedback: feedback}, PayloadType: 96, }, { RTPCodecCapability: RTPCodecCapability{MimeType: "video/h264", ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", RTCPFeedback: feedback}, PayloadType: 127, }, } { assert.NoError(t, me.RegisterCodec(codec, RTPCodecTypeVideo)) } api := NewAPI(WithMediaEngine(me)) pc, err := api.NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsUnifiedPlan, }) assert.NoError(t, err) offer := SessionDescription{ Type: SDPTypeOffer, SDP: offerSdp, } assert.NoError(t, pc.SetRemoteDescription(offer)) answer, err := pc.CreateAnswer(nil) assert.NoError(t, err) assert.NotNil(t, answer) assert.NoError(t, pc.SetLocalDescription(answer)) assert.True(t, strings.Contains(answer.SDP, "VP8") || strings.Contains(answer.SDP, "vp8")) assert.NoError(t, pc.Close()) }) } } webrtc-3.1.56/networktype.go000066400000000000000000000044421437620512100160230ustar00rootroot00000000000000package webrtc import ( "fmt" "github.com/pion/ice/v2" ) func supportedNetworkTypes() []NetworkType { return []NetworkType{ NetworkTypeUDP4, NetworkTypeUDP6, // NetworkTypeTCP4, // Not supported yet // NetworkTypeTCP6, // Not supported yet } } // NetworkType represents the type of network type NetworkType int const ( // NetworkTypeUDP4 indicates UDP over IPv4. NetworkTypeUDP4 NetworkType = iota + 1 // NetworkTypeUDP6 indicates UDP over IPv6. NetworkTypeUDP6 // NetworkTypeTCP4 indicates TCP over IPv4. NetworkTypeTCP4 // NetworkTypeTCP6 indicates TCP over IPv6. NetworkTypeTCP6 ) // This is done this way because of a linter. const ( networkTypeUDP4Str = "udp4" networkTypeUDP6Str = "udp6" networkTypeTCP4Str = "tcp4" networkTypeTCP6Str = "tcp6" ) func (t NetworkType) String() string { switch t { case NetworkTypeUDP4: return networkTypeUDP4Str case NetworkTypeUDP6: return networkTypeUDP6Str case NetworkTypeTCP4: return networkTypeTCP4Str case NetworkTypeTCP6: return networkTypeTCP6Str default: return ErrUnknownType.Error() } } // Protocol returns udp or tcp func (t NetworkType) Protocol() string { switch t { case NetworkTypeUDP4: return "udp" case NetworkTypeUDP6: return "udp" case NetworkTypeTCP4: return "tcp" case NetworkTypeTCP6: return "tcp" default: return ErrUnknownType.Error() } } // NewNetworkType allows create network type from string // It will be useful for getting custom network types from external config. func NewNetworkType(raw string) (NetworkType, error) { switch raw { case networkTypeUDP4Str: return NetworkTypeUDP4, nil case networkTypeUDP6Str: return NetworkTypeUDP6, nil case networkTypeTCP4Str: return NetworkTypeTCP4, nil case networkTypeTCP6Str: return NetworkTypeTCP6, nil default: return NetworkType(Unknown), fmt.Errorf("%w: %s", errNetworkTypeUnknown, raw) } } func getNetworkType(iceNetworkType ice.NetworkType) (NetworkType, error) { switch iceNetworkType { case ice.NetworkTypeUDP4: return NetworkTypeUDP4, nil case ice.NetworkTypeUDP6: return NetworkTypeUDP6, nil case ice.NetworkTypeTCP4: return NetworkTypeTCP4, nil case ice.NetworkTypeTCP6: return NetworkTypeTCP6, nil default: return NetworkType(Unknown), fmt.Errorf("%w: %s", errNetworkTypeUnknown, iceNetworkType.String()) } } webrtc-3.1.56/networktype_test.go000066400000000000000000000021131437620512100170530ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNetworkType_String(t *testing.T) { testCases := []struct { cType NetworkType expectedString string }{ {NetworkType(Unknown), unknownStr}, {NetworkTypeUDP4, "udp4"}, {NetworkTypeUDP6, "udp6"}, {NetworkTypeTCP4, "tcp4"}, {NetworkTypeTCP6, "tcp6"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.cType.String(), "testCase: %d %v", i, testCase, ) } } func TestNetworkType(t *testing.T) { testCases := []struct { typeString string shouldFail bool expectedType NetworkType }{ {unknownStr, true, NetworkType(Unknown)}, {"udp4", false, NetworkTypeUDP4}, {"udp6", false, NetworkTypeUDP6}, {"tcp4", false, NetworkTypeTCP4}, {"tcp6", false, NetworkTypeTCP6}, } for i, testCase := range testCases { actual, err := NewNetworkType(testCase.typeString) if (err != nil) != testCase.shouldFail { t.Error(err) } assert.Equal(t, testCase.expectedType, actual, "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/oauthcredential.go000066400000000000000000000011231437620512100165740ustar00rootroot00000000000000package webrtc // OAuthCredential represents OAuth credential information which is used by // the STUN/TURN client to connect to an ICE server as defined in // https://tools.ietf.org/html/rfc7635. Note that the kid parameter is not // located in OAuthCredential, but in ICEServer's username member. type OAuthCredential struct { // MACKey is a base64-url encoded format. It is used in STUN message // integrity hash calculation. MACKey string // AccessToken is a base64-encoded format. This is an encrypted // self-contained token that is opaque to the application. AccessToken string } webrtc-3.1.56/offeransweroptions.go000066400000000000000000000015671437620512100173720ustar00rootroot00000000000000package webrtc // OfferAnswerOptions is a base structure which describes the options that // can be used to control the offer/answer creation process. type OfferAnswerOptions struct { // VoiceActivityDetection allows the application to provide information // about whether it wishes voice detection feature to be enabled or disabled. VoiceActivityDetection bool } // AnswerOptions structure describes the options used to control the answer // creation process. type AnswerOptions struct { OfferAnswerOptions } // OfferOptions structure describes the options used to control the offer // creation process type OfferOptions struct { OfferAnswerOptions // ICERestart forces the underlying ice gathering process to be restarted. // When this value is true, the generated description will have ICE // credentials that are different from the current credentials ICERestart bool } webrtc-3.1.56/operations.go000066400000000000000000000030221437620512100156040ustar00rootroot00000000000000package webrtc import ( "container/list" "sync" ) // Operation is a function type operation func() // Operations is a task executor. type operations struct { mu sync.Mutex busy bool ops *list.List } func newOperations() *operations { return &operations{ ops: list.New(), } } // Enqueue adds a new action to be executed. If there are no actions scheduled, // the execution will start immediately in a new goroutine. func (o *operations) Enqueue(op operation) { if op == nil { return } o.mu.Lock() running := o.busy o.ops.PushBack(op) o.busy = true o.mu.Unlock() if !running { go o.start() } } // IsEmpty checks if there are tasks in the queue func (o *operations) IsEmpty() bool { o.mu.Lock() defer o.mu.Unlock() return o.ops.Len() == 0 } // Done blocks until all currently enqueued operations are finished executing. // For more complex synchronization, use Enqueue directly. func (o *operations) Done() { var wg sync.WaitGroup wg.Add(1) o.Enqueue(func() { wg.Done() }) wg.Wait() } func (o *operations) pop() func() { o.mu.Lock() defer o.mu.Unlock() if o.ops.Len() == 0 { return nil } e := o.ops.Front() o.ops.Remove(e) if op, ok := e.Value.(operation); ok { return op } return nil } func (o *operations) start() { defer func() { o.mu.Lock() defer o.mu.Unlock() if o.ops.Len() == 0 { o.busy = false return } // either a new operation was enqueued while we // were busy, or an operation panicked go o.start() }() fn := o.pop() for fn != nil { fn() fn = o.pop() } } webrtc-3.1.56/operations_test.go000066400000000000000000000011101437620512100166370ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestOperations_Enqueue(t *testing.T) { ops := newOperations() for i := 0; i < 100; i++ { results := make([]int, 16) for i := range results { func(j int) { ops.Enqueue(func() { results[j] = j * j }) }(i) } ops.Done() expected := []int{0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225} assert.Equal(t, len(expected), len(results)) assert.Equal(t, expected, results) } } func TestOperations_Done(t *testing.T) { ops := newOperations() ops.Done() } webrtc-3.1.56/ortc_datachannel_test.go000066400000000000000000000025351437620512100177610ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "io" "testing" "time" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) func TestDataChannel_ORTCE2E(t *testing.T) { lim := test.TimeOut(time.Second * 20) defer lim.Stop() report := test.CheckRoutines(t) defer report() stackA, stackB, err := newORTCPair() assert.NoError(t, err) awaitSetup := make(chan struct{}) awaitString := make(chan struct{}) awaitBinary := make(chan struct{}) stackB.sctp.OnDataChannel(func(d *DataChannel) { close(awaitSetup) d.OnMessage(func(msg DataChannelMessage) { if msg.IsString { close(awaitString) } else { close(awaitBinary) } }) }) assert.NoError(t, signalORTCPair(stackA, stackB)) var id uint16 = 1 dcParams := &DataChannelParameters{ Label: "Foo", ID: &id, } channelA, err := stackA.api.NewDataChannel(stackA.sctp, dcParams) assert.NoError(t, err) <-awaitSetup assert.NoError(t, channelA.SendText("ABC")) assert.NoError(t, channelA.Send([]byte("ABC"))) <-awaitString <-awaitBinary assert.NoError(t, stackA.close()) assert.NoError(t, stackB.close()) // attempt to send when channel is closed assert.ErrorIs(t, channelA.Send([]byte("ABC")), io.ErrClosedPipe) assert.ErrorIs(t, channelA.SendText("test"), io.ErrClosedPipe) assert.ErrorIs(t, channelA.ensureOpen(), io.ErrClosedPipe) } webrtc-3.1.56/ortc_media_test.go000066400000000000000000000033261437620512100165750ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "context" "testing" "time" "github.com/pion/transport/v2/test" "github.com/pion/webrtc/v3/pkg/media" "github.com/stretchr/testify/assert" ) func Test_ORTC_Media(t *testing.T) { lim := test.TimeOut(time.Second * 20) defer lim.Stop() report := test.CheckRoutines(t) defer report() stackA, stackB, err := newORTCPair() assert.NoError(t, err) assert.NoError(t, stackA.api.mediaEngine.RegisterDefaultCodecs()) assert.NoError(t, stackB.api.mediaEngine.RegisterDefaultCodecs()) assert.NoError(t, signalORTCPair(stackA, stackB)) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) rtpSender, err := stackA.api.NewRTPSender(track, stackA.dtls) assert.NoError(t, err) assert.NoError(t, rtpSender.Send(rtpSender.GetParameters())) rtpReceiver, err := stackB.api.NewRTPReceiver(RTPCodecTypeVideo, stackB.dtls) assert.NoError(t, err) assert.NoError(t, rtpReceiver.Receive(RTPReceiveParameters{Encodings: []RTPDecodingParameters{ {RTPCodingParameters: rtpSender.GetParameters().Encodings[0].RTPCodingParameters}, }})) seenPacket, seenPacketCancel := context.WithCancel(context.Background()) go func() { track := rtpReceiver.Track() _, _, err := track.ReadRTP() assert.NoError(t, err) seenPacketCancel() }() func() { for range time.Tick(time.Millisecond * 20) { select { case <-seenPacket.Done(): return default: assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second})) } } }() assert.NoError(t, rtpSender.Stop()) assert.NoError(t, rtpReceiver.Stop()) assert.NoError(t, stackA.close()) assert.NoError(t, stackB.close()) } webrtc-3.1.56/ortc_test.go000066400000000000000000000064131437620512100154360ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "github.com/pion/webrtc/v3/internal/util" ) type testORTCStack struct { api *API gatherer *ICEGatherer ice *ICETransport dtls *DTLSTransport sctp *SCTPTransport } func (s *testORTCStack) setSignal(sig *testORTCSignal, isOffer bool) error { iceRole := ICERoleControlled if isOffer { iceRole = ICERoleControlling } err := s.ice.SetRemoteCandidates(sig.ICECandidates) if err != nil { return err } // Start the ICE transport err = s.ice.Start(nil, sig.ICEParameters, &iceRole) if err != nil { return err } // Start the DTLS transport err = s.dtls.Start(sig.DTLSParameters) if err != nil { return err } // Start the SCTP transport err = s.sctp.Start(sig.SCTPCapabilities) if err != nil { return err } return nil } func (s *testORTCStack) getSignal() (*testORTCSignal, error) { gatherFinished := make(chan struct{}) s.gatherer.OnLocalCandidate(func(i *ICECandidate) { if i == nil { close(gatherFinished) } }) if err := s.gatherer.Gather(); err != nil { return nil, err } <-gatherFinished iceCandidates, err := s.gatherer.GetLocalCandidates() if err != nil { return nil, err } iceParams, err := s.gatherer.GetLocalParameters() if err != nil { return nil, err } dtlsParams, err := s.dtls.GetLocalParameters() if err != nil { return nil, err } sctpCapabilities := s.sctp.GetCapabilities() return &testORTCSignal{ ICECandidates: iceCandidates, ICEParameters: iceParams, DTLSParameters: dtlsParams, SCTPCapabilities: sctpCapabilities, }, nil } func (s *testORTCStack) close() error { var closeErrs []error if err := s.sctp.Stop(); err != nil { closeErrs = append(closeErrs, err) } if err := s.ice.Stop(); err != nil { closeErrs = append(closeErrs, err) } return util.FlattenErrs(closeErrs) } type testORTCSignal struct { ICECandidates []ICECandidate ICEParameters ICEParameters DTLSParameters DTLSParameters SCTPCapabilities SCTPCapabilities } func newORTCPair() (stackA *testORTCStack, stackB *testORTCStack, err error) { sa, err := newORTCStack() if err != nil { return nil, nil, err } sb, err := newORTCStack() if err != nil { return nil, nil, err } return sa, sb, nil } func newORTCStack() (*testORTCStack, error) { // Create an API object api := NewAPI() // Create the ICE gatherer gatherer, err := api.NewICEGatherer(ICEGatherOptions{}) if err != nil { return nil, err } // Construct the ICE transport ice := api.NewICETransport(gatherer) // Construct the DTLS transport dtls, err := api.NewDTLSTransport(ice, nil) if err != nil { return nil, err } // Construct the SCTP transport sctp := api.NewSCTPTransport(dtls) return &testORTCStack{ api: api, gatherer: gatherer, ice: ice, dtls: dtls, sctp: sctp, }, nil } func signalORTCPair(stackA *testORTCStack, stackB *testORTCStack) error { sigA, err := stackA.getSignal() if err != nil { return err } sigB, err := stackB.getSignal() if err != nil { return err } a := make(chan error) b := make(chan error) go func() { a <- stackB.setSignal(sigA, false) }() go func() { b <- stackA.setSignal(sigB, true) }() errA := <-a errB := <-b closeErrs := []error{errA, errB} return util.FlattenErrs(closeErrs) } webrtc-3.1.56/package.json000066400000000000000000000002751437620512100153670ustar00rootroot00000000000000{ "name": "webrtc", "repository": "git@github.com:pion/webrtc.git", "private": true, "devDependencies": { "wrtc": "0.4.7" }, "dependencies": { "request": "2.88.2" } } webrtc-3.1.56/peerconnection.go000066400000000000000000002305061437620512100164450ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "errors" "fmt" "io" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/pion/ice/v2" "github.com/pion/interceptor" "github.com/pion/logging" "github.com/pion/rtcp" "github.com/pion/sdp/v3" "github.com/pion/srtp/v2" "github.com/pion/webrtc/v3/internal/util" "github.com/pion/webrtc/v3/pkg/rtcerr" ) // PeerConnection represents a WebRTC connection that establishes a // peer-to-peer communications with another PeerConnection instance in a // browser, or to another endpoint implementing the required protocols. type PeerConnection struct { statsID string mu sync.RWMutex sdpOrigin sdp.Origin // ops is an operations queue which will ensure the enqueued actions are // executed in order. It is used for asynchronously, but serially processing // remote and local descriptions ops *operations configuration Configuration currentLocalDescription *SessionDescription pendingLocalDescription *SessionDescription currentRemoteDescription *SessionDescription pendingRemoteDescription *SessionDescription signalingState SignalingState iceConnectionState atomic.Value // ICEConnectionState connectionState atomic.Value // PeerConnectionState idpLoginURL *string isClosed *atomicBool isNegotiationNeeded *atomicBool negotiationNeededState negotiationNeededState lastOffer string lastAnswer string // a value containing the last known greater mid value // we internally generate mids as numbers. Needed since JSEP // requires that when reusing a media section a new unique mid // should be defined (see JSEP 3.4.1). greaterMid int rtpTransceivers []*RTPTransceiver onSignalingStateChangeHandler func(SignalingState) onICEConnectionStateChangeHandler atomic.Value // func(ICEConnectionState) onConnectionStateChangeHandler atomic.Value // func(PeerConnectionState) onTrackHandler func(*TrackRemote, *RTPReceiver) onDataChannelHandler func(*DataChannel) onNegotiationNeededHandler atomic.Value // func() iceGatherer *ICEGatherer iceTransport *ICETransport dtlsTransport *DTLSTransport sctpTransport *SCTPTransport // A reference to the associated API state used by this connection api *API log logging.LeveledLogger interceptorRTCPWriter interceptor.RTCPWriter } // NewPeerConnection creates a PeerConnection with the default codecs and // interceptors. See RegisterDefaultCodecs and RegisterDefaultInterceptors. // // If you wish to customize the set of available codecs or the set of // active interceptors, create a MediaEngine and call api.NewPeerConnection // instead of this function. func NewPeerConnection(configuration Configuration) (*PeerConnection, error) { m := &MediaEngine{} if err := m.RegisterDefaultCodecs(); err != nil { return nil, err } i := &interceptor.Registry{} if err := RegisterDefaultInterceptors(m, i); err != nil { return nil, err } api := NewAPI(WithMediaEngine(m), WithInterceptorRegistry(i)) return api.NewPeerConnection(configuration) } // NewPeerConnection creates a new PeerConnection with the provided configuration against the received API object func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection, error) { // https://w3c.github.io/webrtc-pc/#constructor (Step #2) // Some variables defined explicitly despite their implicit zero values to // allow better readability to understand what is happening. pc := &PeerConnection{ statsID: fmt.Sprintf("PeerConnection-%d", time.Now().UnixNano()), configuration: Configuration{ ICEServers: []ICEServer{}, ICETransportPolicy: ICETransportPolicyAll, BundlePolicy: BundlePolicyBalanced, RTCPMuxPolicy: RTCPMuxPolicyRequire, Certificates: []Certificate{}, ICECandidatePoolSize: 0, }, ops: newOperations(), isClosed: &atomicBool{}, isNegotiationNeeded: &atomicBool{}, negotiationNeededState: negotiationNeededStateEmpty, lastOffer: "", lastAnswer: "", greaterMid: -1, signalingState: SignalingStateStable, api: api, log: api.settingEngine.LoggerFactory.NewLogger("pc"), } pc.iceConnectionState.Store(ICEConnectionStateNew) pc.connectionState.Store(PeerConnectionStateNew) i, err := api.interceptorRegistry.Build("") if err != nil { return nil, err } pc.api = &API{ settingEngine: api.settingEngine, interceptor: i, } if api.settingEngine.disableMediaEngineCopy { pc.api.mediaEngine = api.mediaEngine } else { pc.api.mediaEngine = api.mediaEngine.copy() } if err = pc.initConfiguration(configuration); err != nil { return nil, err } pc.iceGatherer, err = pc.createICEGatherer() if err != nil { return nil, err } // Create the ice transport iceTransport := pc.createICETransport() pc.iceTransport = iceTransport // Create the DTLS transport dtlsTransport, err := pc.api.NewDTLSTransport(pc.iceTransport, pc.configuration.Certificates) if err != nil { return nil, err } pc.dtlsTransport = dtlsTransport // Create the SCTP transport pc.sctpTransport = pc.api.NewSCTPTransport(pc.dtlsTransport) // Wire up the on datachannel handler pc.sctpTransport.OnDataChannel(func(d *DataChannel) { pc.mu.RLock() handler := pc.onDataChannelHandler pc.mu.RUnlock() if handler != nil { handler(d) } }) pc.interceptorRTCPWriter = pc.api.interceptor.BindRTCPWriter(interceptor.RTCPWriterFunc(pc.writeRTCP)) return pc, nil } // initConfiguration defines validation of the specified Configuration and // its assignment to the internal configuration variable. This function differs // from its SetConfiguration counterpart because most of the checks do not // include verification statements related to the existing state. Thus the // function describes only minor verification of some the struct variables. func (pc *PeerConnection) initConfiguration(configuration Configuration) error { if configuration.PeerIdentity != "" { pc.configuration.PeerIdentity = configuration.PeerIdentity } // https://www.w3.org/TR/webrtc/#constructor (step #3) if len(configuration.Certificates) > 0 { now := time.Now() for _, x509Cert := range configuration.Certificates { if !x509Cert.Expires().IsZero() && now.After(x509Cert.Expires()) { return &rtcerr.InvalidAccessError{Err: ErrCertificateExpired} } pc.configuration.Certificates = append(pc.configuration.Certificates, x509Cert) } } else { sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return &rtcerr.UnknownError{Err: err} } certificate, err := GenerateCertificate(sk) if err != nil { return err } pc.configuration.Certificates = []Certificate{*certificate} } if configuration.BundlePolicy != BundlePolicy(Unknown) { pc.configuration.BundlePolicy = configuration.BundlePolicy } if configuration.RTCPMuxPolicy != RTCPMuxPolicy(Unknown) { pc.configuration.RTCPMuxPolicy = configuration.RTCPMuxPolicy } if configuration.ICECandidatePoolSize != 0 { pc.configuration.ICECandidatePoolSize = configuration.ICECandidatePoolSize } if configuration.ICETransportPolicy != ICETransportPolicy(Unknown) { pc.configuration.ICETransportPolicy = configuration.ICETransportPolicy } if configuration.SDPSemantics != SDPSemantics(Unknown) { pc.configuration.SDPSemantics = configuration.SDPSemantics } sanitizedICEServers := configuration.getICEServers() if len(sanitizedICEServers) > 0 { for _, server := range sanitizedICEServers { if err := server.validate(); err != nil { return err } } pc.configuration.ICEServers = sanitizedICEServers } return nil } // OnSignalingStateChange sets an event handler which is invoked when the // peer connection's signaling state changes func (pc *PeerConnection) OnSignalingStateChange(f func(SignalingState)) { pc.mu.Lock() defer pc.mu.Unlock() pc.onSignalingStateChangeHandler = f } func (pc *PeerConnection) onSignalingStateChange(newState SignalingState) { pc.mu.RLock() handler := pc.onSignalingStateChangeHandler pc.mu.RUnlock() pc.log.Infof("signaling state changed to %s", newState) if handler != nil { go handler(newState) } } // OnDataChannel sets an event handler which is invoked when a data // channel message arrives from a remote peer. func (pc *PeerConnection) OnDataChannel(f func(*DataChannel)) { pc.mu.Lock() defer pc.mu.Unlock() pc.onDataChannelHandler = f } // OnNegotiationNeeded sets an event handler which is invoked when // a change has occurred which requires session negotiation func (pc *PeerConnection) OnNegotiationNeeded(f func()) { pc.onNegotiationNeededHandler.Store(f) } // onNegotiationNeeded enqueues negotiationNeededOp if necessary // caller of this method should hold `pc.mu` lock func (pc *PeerConnection) onNegotiationNeeded() { // https://w3c.github.io/webrtc-pc/#updating-the-negotiation-needed-flag // non-canon step 1 if pc.negotiationNeededState == negotiationNeededStateRun { pc.negotiationNeededState = negotiationNeededStateQueue return } else if pc.negotiationNeededState == negotiationNeededStateQueue { return } pc.negotiationNeededState = negotiationNeededStateRun pc.ops.Enqueue(pc.negotiationNeededOp) } func (pc *PeerConnection) negotiationNeededOp() { // Don't run NegotiatedNeeded checks if OnNegotiationNeeded is not set if handler, ok := pc.onNegotiationNeededHandler.Load().(func()); !ok || handler == nil { return } // https://www.w3.org/TR/webrtc/#updating-the-negotiation-needed-flag // Step 2.1 if pc.isClosed.get() { return } // non-canon step 2.2 if !pc.ops.IsEmpty() { pc.ops.Enqueue(pc.negotiationNeededOp) return } // non-canon, run again if there was a request defer func() { pc.mu.Lock() defer pc.mu.Unlock() if pc.negotiationNeededState == negotiationNeededStateQueue { defer pc.onNegotiationNeeded() } pc.negotiationNeededState = negotiationNeededStateEmpty }() // Step 2.3 if pc.SignalingState() != SignalingStateStable { return } // Step 2.4 if !pc.checkNegotiationNeeded() { pc.isNegotiationNeeded.set(false) return } // Step 2.5 if pc.isNegotiationNeeded.get() { return } // Step 2.6 pc.isNegotiationNeeded.set(true) // Step 2.7 if handler, ok := pc.onNegotiationNeededHandler.Load().(func()); ok && handler != nil { handler() } } func (pc *PeerConnection) checkNegotiationNeeded() bool { //nolint:gocognit // To check if negotiation is needed for connection, perform the following checks: // Skip 1, 2 steps // Step 3 pc.mu.Lock() defer pc.mu.Unlock() localDesc := pc.currentLocalDescription remoteDesc := pc.currentRemoteDescription if localDesc == nil { return true } pc.sctpTransport.lock.Lock() lenDataChannel := len(pc.sctpTransport.dataChannels) pc.sctpTransport.lock.Unlock() if lenDataChannel != 0 && haveDataChannel(localDesc) == nil { return true } for _, t := range pc.rtpTransceivers { // https://www.w3.org/TR/webrtc/#dfn-update-the-negotiation-needed-flag // Step 5.1 // if t.stopping && !t.stopped { // return true // } m := getByMid(t.Mid(), localDesc) // Step 5.2 if !t.stopped && m == nil { return true } if !t.stopped && m != nil { // Step 5.3.1 if t.Direction() == RTPTransceiverDirectionSendrecv || t.Direction() == RTPTransceiverDirectionSendonly { descMsid, okMsid := m.Attribute(sdp.AttrKeyMsid) track := t.Sender().Track() if !okMsid || descMsid != track.StreamID()+" "+track.ID() { return true } } switch localDesc.Type { case SDPTypeOffer: // Step 5.3.2 rm := getByMid(t.Mid(), remoteDesc) if rm == nil { return true } if getPeerDirection(m) != t.Direction() && getPeerDirection(rm) != t.Direction().Revers() { return true } case SDPTypeAnswer: // Step 5.3.3 if _, ok := m.Attribute(t.Direction().String()); !ok { return true } default: } } // Step 5.4 if t.stopped && t.Mid() != "" { if getByMid(t.Mid(), localDesc) != nil || getByMid(t.Mid(), remoteDesc) != nil { return true } } } // Step 6 return false } // OnICECandidate sets an event handler which is invoked when a new ICE // candidate is found. // ICE candidate gathering only begins when SetLocalDescription or // SetRemoteDescription is called. // Take note that the handler will be called with a nil pointer when // gathering is finished. func (pc *PeerConnection) OnICECandidate(f func(*ICECandidate)) { pc.iceGatherer.OnLocalCandidate(f) } // OnICEGatheringStateChange sets an event handler which is invoked when the // ICE candidate gathering state has changed. func (pc *PeerConnection) OnICEGatheringStateChange(f func(ICEGathererState)) { pc.iceGatherer.OnStateChange(f) } // OnTrack sets an event handler which is called when remote track // arrives from a remote peer. func (pc *PeerConnection) OnTrack(f func(*TrackRemote, *RTPReceiver)) { pc.mu.Lock() defer pc.mu.Unlock() pc.onTrackHandler = f } func (pc *PeerConnection) onTrack(t *TrackRemote, r *RTPReceiver) { pc.mu.RLock() handler := pc.onTrackHandler pc.mu.RUnlock() pc.log.Debugf("got new track: %+v", t) if t != nil { if handler != nil { go handler(t, r) } else { pc.log.Warnf("OnTrack unset, unable to handle incoming media streams") } } } // OnICEConnectionStateChange sets an event handler which is called // when an ICE connection state is changed. func (pc *PeerConnection) OnICEConnectionStateChange(f func(ICEConnectionState)) { pc.onICEConnectionStateChangeHandler.Store(f) } func (pc *PeerConnection) onICEConnectionStateChange(cs ICEConnectionState) { pc.iceConnectionState.Store(cs) pc.log.Infof("ICE connection state changed: %s", cs) if handler, ok := pc.onICEConnectionStateChangeHandler.Load().(func(ICEConnectionState)); ok && handler != nil { handler(cs) } } // OnConnectionStateChange sets an event handler which is called // when the PeerConnectionState has changed func (pc *PeerConnection) OnConnectionStateChange(f func(PeerConnectionState)) { pc.onConnectionStateChangeHandler.Store(f) } func (pc *PeerConnection) onConnectionStateChange(cs PeerConnectionState) { pc.connectionState.Store(cs) pc.log.Infof("peer connection state changed: %s", cs) if handler, ok := pc.onConnectionStateChangeHandler.Load().(func(PeerConnectionState)); ok && handler != nil { go handler(cs) } } // SetConfiguration updates the configuration of this PeerConnection object. func (pc *PeerConnection) SetConfiguration(configuration Configuration) error { //nolint:gocognit // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2) if pc.isClosed.get() { return &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #3) if configuration.PeerIdentity != "" { if configuration.PeerIdentity != pc.configuration.PeerIdentity { return &rtcerr.InvalidModificationError{Err: ErrModifyingPeerIdentity} } pc.configuration.PeerIdentity = configuration.PeerIdentity } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #4) if len(configuration.Certificates) > 0 { if len(configuration.Certificates) != len(pc.configuration.Certificates) { return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates} } for i, certificate := range configuration.Certificates { if !pc.configuration.Certificates[i].Equals(certificate) { return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates} } } pc.configuration.Certificates = configuration.Certificates } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #5) if configuration.BundlePolicy != BundlePolicy(Unknown) { if configuration.BundlePolicy != pc.configuration.BundlePolicy { return &rtcerr.InvalidModificationError{Err: ErrModifyingBundlePolicy} } pc.configuration.BundlePolicy = configuration.BundlePolicy } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #6) if configuration.RTCPMuxPolicy != RTCPMuxPolicy(Unknown) { if configuration.RTCPMuxPolicy != pc.configuration.RTCPMuxPolicy { return &rtcerr.InvalidModificationError{Err: ErrModifyingRTCPMuxPolicy} } pc.configuration.RTCPMuxPolicy = configuration.RTCPMuxPolicy } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #7) if configuration.ICECandidatePoolSize != 0 { if pc.configuration.ICECandidatePoolSize != configuration.ICECandidatePoolSize && pc.LocalDescription() != nil { return &rtcerr.InvalidModificationError{Err: ErrModifyingICECandidatePoolSize} } pc.configuration.ICECandidatePoolSize = configuration.ICECandidatePoolSize } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #8) if configuration.ICETransportPolicy != ICETransportPolicy(Unknown) { pc.configuration.ICETransportPolicy = configuration.ICETransportPolicy } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11) if len(configuration.ICEServers) > 0 { // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3) for _, server := range configuration.ICEServers { if err := server.validate(); err != nil { return err } } pc.configuration.ICEServers = configuration.ICEServers } return nil } // GetConfiguration returns a Configuration object representing the current // configuration of this PeerConnection object. The returned object is a // copy and direct mutation on it will not take affect until SetConfiguration // has been called with Configuration passed as its only argument. // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-getconfiguration func (pc *PeerConnection) GetConfiguration() Configuration { return pc.configuration } func (pc *PeerConnection) getStatsID() string { pc.mu.RLock() defer pc.mu.RUnlock() return pc.statsID } // hasLocalDescriptionChanged returns whether local media (rtpTransceivers) has changed // caller of this method should hold `pc.mu` lock func (pc *PeerConnection) hasLocalDescriptionChanged(desc *SessionDescription) bool { for _, t := range pc.rtpTransceivers { m := getByMid(t.Mid(), desc) if m == nil { return true } if getPeerDirection(m) != t.Direction() { return true } } return false } // CreateOffer starts the PeerConnection and generates the localDescription // https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-createoffer func (pc *PeerConnection) CreateOffer(options *OfferOptions) (SessionDescription, error) { //nolint:gocognit useIdentity := pc.idpLoginURL != nil switch { case useIdentity: return SessionDescription{}, errIdentityProviderNotImplemented case pc.isClosed.get(): return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } if options != nil && options.ICERestart { if err := pc.iceTransport.restart(); err != nil { return SessionDescription{}, err } } var ( d *sdp.SessionDescription offer SessionDescription err error ) // This may be necessary to recompute if, for example, createOffer was called when only an // audio RTCRtpTransceiver was added to connection, but while performing the in-parallel // steps to create an offer, a video RTCRtpTransceiver was added, requiring additional // inspection of video system resources. count := 0 pc.mu.Lock() defer pc.mu.Unlock() for { // We cache current transceivers to ensure they aren't // mutated during offer generation. We later check if they have // been mutated and recompute the offer if necessary. currentTransceivers := pc.rtpTransceivers // in-parallel steps to create an offer // https://w3c.github.io/webrtc-pc/#dfn-in-parallel-steps-to-create-an-offer isPlanB := pc.configuration.SDPSemantics == SDPSemanticsPlanB if pc.currentRemoteDescription != nil && isPlanB { isPlanB = descriptionPossiblyPlanB(pc.currentRemoteDescription) } // include unmatched local transceivers if !isPlanB { // update the greater mid if the remote description provides a greater one if pc.currentRemoteDescription != nil { var numericMid int for _, media := range pc.currentRemoteDescription.parsed.MediaDescriptions { mid := getMidValue(media) if mid == "" { continue } numericMid, err = strconv.Atoi(mid) if err != nil { continue } if numericMid > pc.greaterMid { pc.greaterMid = numericMid } } } for _, t := range currentTransceivers { if mid := t.Mid(); mid != "" { numericMid, errMid := strconv.Atoi(mid) if errMid == nil { if numericMid > pc.greaterMid { pc.greaterMid = numericMid } } continue } pc.greaterMid++ err = t.SetMid(strconv.Itoa(pc.greaterMid)) if err != nil { return SessionDescription{}, err } } } if pc.currentRemoteDescription == nil { d, err = pc.generateUnmatchedSDP(currentTransceivers, useIdentity) } else { d, err = pc.generateMatchedSDP(currentTransceivers, useIdentity, true /*includeUnmatched */, connectionRoleFromDtlsRole(defaultDtlsRoleOffer)) } if err != nil { return SessionDescription{}, err } updateSDPOrigin(&pc.sdpOrigin, d) sdpBytes, err := d.Marshal() if err != nil { return SessionDescription{}, err } offer = SessionDescription{ Type: SDPTypeOffer, SDP: string(sdpBytes), parsed: d, } // Verify local media hasn't changed during offer // generation. Recompute if necessary if isPlanB || !pc.hasLocalDescriptionChanged(&offer) { break } count++ if count >= 128 { return SessionDescription{}, errExcessiveRetries } } pc.lastOffer = offer.SDP return offer, nil } func (pc *PeerConnection) createICEGatherer() (*ICEGatherer, error) { g, err := pc.api.NewICEGatherer(ICEGatherOptions{ ICEServers: pc.configuration.getICEServers(), ICEGatherPolicy: pc.configuration.ICETransportPolicy, }) if err != nil { return nil, err } return g, nil } // Update the PeerConnectionState given the state of relevant transports // https://www.w3.org/TR/webrtc/#rtcpeerconnectionstate-enum func (pc *PeerConnection) updateConnectionState(iceConnectionState ICEConnectionState, dtlsTransportState DTLSTransportState) { connectionState := PeerConnectionStateNew switch { // The RTCPeerConnection object's [[IsClosed]] slot is true. case pc.isClosed.get(): connectionState = PeerConnectionStateClosed // Any of the RTCIceTransports or RTCDtlsTransports are in a "failed" state. case iceConnectionState == ICEConnectionStateFailed || dtlsTransportState == DTLSTransportStateFailed: connectionState = PeerConnectionStateFailed // Any of the RTCIceTransports or RTCDtlsTransports are in the "disconnected" // state and none of them are in the "failed" or "connecting" or "checking" state. */ case iceConnectionState == ICEConnectionStateDisconnected: connectionState = PeerConnectionStateDisconnected // All RTCIceTransports and RTCDtlsTransports are in the "connected", "completed" or "closed" // state and at least one of them is in the "connected" or "completed" state. case iceConnectionState == ICEConnectionStateConnected && dtlsTransportState == DTLSTransportStateConnected: connectionState = PeerConnectionStateConnected // Any of the RTCIceTransports or RTCDtlsTransports are in the "connecting" or // "checking" state and none of them is in the "failed" state. case iceConnectionState == ICEConnectionStateChecking && dtlsTransportState == DTLSTransportStateConnecting: connectionState = PeerConnectionStateConnecting } if pc.connectionState.Load() == connectionState { return } pc.onConnectionStateChange(connectionState) } func (pc *PeerConnection) createICETransport() *ICETransport { t := pc.api.NewICETransport(pc.iceGatherer) t.internalOnConnectionStateChangeHandler.Store(func(state ICETransportState) { var cs ICEConnectionState switch state { case ICETransportStateNew: cs = ICEConnectionStateNew case ICETransportStateChecking: cs = ICEConnectionStateChecking case ICETransportStateConnected: cs = ICEConnectionStateConnected case ICETransportStateCompleted: cs = ICEConnectionStateCompleted case ICETransportStateFailed: cs = ICEConnectionStateFailed case ICETransportStateDisconnected: cs = ICEConnectionStateDisconnected case ICETransportStateClosed: cs = ICEConnectionStateClosed default: pc.log.Warnf("OnConnectionStateChange: unhandled ICE state: %s", state) return } pc.onICEConnectionStateChange(cs) pc.updateConnectionState(cs, pc.dtlsTransport.State()) }) return t } // CreateAnswer starts the PeerConnection and generates the localDescription func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (SessionDescription, error) { useIdentity := pc.idpLoginURL != nil remoteDesc := pc.RemoteDescription() switch { case remoteDesc == nil: return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrNoRemoteDescription} case useIdentity: return SessionDescription{}, errIdentityProviderNotImplemented case pc.isClosed.get(): return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrConnectionClosed} case pc.signalingState.Get() != SignalingStateHaveRemoteOffer && pc.signalingState.Get() != SignalingStateHaveLocalPranswer: return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrIncorrectSignalingState} } connectionRole := connectionRoleFromDtlsRole(pc.api.settingEngine.answeringDTLSRole) if connectionRole == sdp.ConnectionRole(0) { connectionRole = connectionRoleFromDtlsRole(defaultDtlsRoleAnswer) // If one of the agents is lite and the other one is not, the lite agent must be the controlling agent. // If both or neither agents are lite the offering agent is controlling. // RFC 8445 S6.1.1 if isIceLiteSet(remoteDesc.parsed) && !pc.api.settingEngine.candidates.ICELite { connectionRole = connectionRoleFromDtlsRole(DTLSRoleServer) } } pc.mu.Lock() defer pc.mu.Unlock() d, err := pc.generateMatchedSDP(pc.rtpTransceivers, useIdentity, false /*includeUnmatched */, connectionRole) if err != nil { return SessionDescription{}, err } updateSDPOrigin(&pc.sdpOrigin, d) sdpBytes, err := d.Marshal() if err != nil { return SessionDescription{}, err } desc := SessionDescription{ Type: SDPTypeAnswer, SDP: string(sdpBytes), parsed: d, } pc.lastAnswer = desc.SDP return desc, nil } // 4.4.1.6 Set the SessionDescription func (pc *PeerConnection) setDescription(sd *SessionDescription, op stateChangeOp) error { //nolint:gocognit switch { case pc.isClosed.get(): return &rtcerr.InvalidStateError{Err: ErrConnectionClosed} case NewSDPType(sd.Type.String()) == SDPType(Unknown): return &rtcerr.TypeError{Err: fmt.Errorf("%w: '%d' is not a valid enum value of type SDPType", errPeerConnSDPTypeInvalidValue, sd.Type)} } nextState, err := func() (SignalingState, error) { pc.mu.Lock() defer pc.mu.Unlock() cur := pc.SignalingState() setLocal := stateChangeOpSetLocal setRemote := stateChangeOpSetRemote newSDPDoesNotMatchOffer := &rtcerr.InvalidModificationError{Err: errSDPDoesNotMatchOffer} newSDPDoesNotMatchAnswer := &rtcerr.InvalidModificationError{Err: errSDPDoesNotMatchAnswer} var nextState SignalingState var err error switch op { case setLocal: switch sd.Type { // stable->SetLocal(offer)->have-local-offer case SDPTypeOffer: if sd.SDP != pc.lastOffer { return nextState, newSDPDoesNotMatchOffer } nextState, err = checkNextSignalingState(cur, SignalingStateHaveLocalOffer, setLocal, sd.Type) if err == nil { pc.pendingLocalDescription = sd } // have-remote-offer->SetLocal(answer)->stable // have-local-pranswer->SetLocal(answer)->stable case SDPTypeAnswer: if sd.SDP != pc.lastAnswer { return nextState, newSDPDoesNotMatchAnswer } nextState, err = checkNextSignalingState(cur, SignalingStateStable, setLocal, sd.Type) if err == nil { pc.currentLocalDescription = sd pc.currentRemoteDescription = pc.pendingRemoteDescription pc.pendingRemoteDescription = nil pc.pendingLocalDescription = nil } case SDPTypeRollback: nextState, err = checkNextSignalingState(cur, SignalingStateStable, setLocal, sd.Type) if err == nil { pc.pendingLocalDescription = nil } // have-remote-offer->SetLocal(pranswer)->have-local-pranswer case SDPTypePranswer: if sd.SDP != pc.lastAnswer { return nextState, newSDPDoesNotMatchAnswer } nextState, err = checkNextSignalingState(cur, SignalingStateHaveLocalPranswer, setLocal, sd.Type) if err == nil { pc.pendingLocalDescription = sd } default: return nextState, &rtcerr.OperationError{Err: fmt.Errorf("%w: %s(%s)", errPeerConnStateChangeInvalid, op, sd.Type)} } case setRemote: switch sd.Type { // stable->SetRemote(offer)->have-remote-offer case SDPTypeOffer: nextState, err = checkNextSignalingState(cur, SignalingStateHaveRemoteOffer, setRemote, sd.Type) if err == nil { pc.pendingRemoteDescription = sd } // have-local-offer->SetRemote(answer)->stable // have-remote-pranswer->SetRemote(answer)->stable case SDPTypeAnswer: nextState, err = checkNextSignalingState(cur, SignalingStateStable, setRemote, sd.Type) if err == nil { pc.currentRemoteDescription = sd pc.currentLocalDescription = pc.pendingLocalDescription pc.pendingRemoteDescription = nil pc.pendingLocalDescription = nil } case SDPTypeRollback: nextState, err = checkNextSignalingState(cur, SignalingStateStable, setRemote, sd.Type) if err == nil { pc.pendingRemoteDescription = nil } // have-local-offer->SetRemote(pranswer)->have-remote-pranswer case SDPTypePranswer: nextState, err = checkNextSignalingState(cur, SignalingStateHaveRemotePranswer, setRemote, sd.Type) if err == nil { pc.pendingRemoteDescription = sd } default: return nextState, &rtcerr.OperationError{Err: fmt.Errorf("%w: %s(%s)", errPeerConnStateChangeInvalid, op, sd.Type)} } default: return nextState, &rtcerr.OperationError{Err: fmt.Errorf("%w: %q", errPeerConnStateChangeUnhandled, op)} } return nextState, err }() if err == nil { pc.signalingState.Set(nextState) if pc.signalingState.Get() == SignalingStateStable { pc.isNegotiationNeeded.set(false) pc.mu.Lock() pc.onNegotiationNeeded() pc.mu.Unlock() } pc.onSignalingStateChange(nextState) } return err } // SetLocalDescription sets the SessionDescription of the local peer func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) error { if pc.isClosed.get() { return &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } haveLocalDescription := pc.currentLocalDescription != nil // JSEP 5.4 if desc.SDP == "" { switch desc.Type { case SDPTypeAnswer, SDPTypePranswer: desc.SDP = pc.lastAnswer case SDPTypeOffer: desc.SDP = pc.lastOffer default: return &rtcerr.InvalidModificationError{ Err: fmt.Errorf("%w: %s", errPeerConnSDPTypeInvalidValueSetLocalDescription, desc.Type), } } } desc.parsed = &sdp.SessionDescription{} if err := desc.parsed.Unmarshal([]byte(desc.SDP)); err != nil { return err } if err := pc.setDescription(&desc, stateChangeOpSetLocal); err != nil { return err } currentTransceivers := append([]*RTPTransceiver{}, pc.GetTransceivers()...) weAnswer := desc.Type == SDPTypeAnswer remoteDesc := pc.RemoteDescription() if weAnswer && remoteDesc != nil { _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, false) if err := pc.startRTPSenders(currentTransceivers); err != nil { return err } pc.configureRTPReceivers(haveLocalDescription, remoteDesc, currentTransceivers) pc.ops.Enqueue(func() { pc.startRTP(haveLocalDescription, remoteDesc, currentTransceivers) }) } if pc.iceGatherer.State() == ICEGathererStateNew { return pc.iceGatherer.Gather() } return nil } // LocalDescription returns PendingLocalDescription if it is not null and // otherwise it returns CurrentLocalDescription. This property is used to // determine if SetLocalDescription has already been called. // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-localdescription func (pc *PeerConnection) LocalDescription() *SessionDescription { if pendingLocalDescription := pc.PendingLocalDescription(); pendingLocalDescription != nil { return pendingLocalDescription } return pc.CurrentLocalDescription() } // SetRemoteDescription sets the SessionDescription of the remote peer // nolint: gocyclo func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error { //nolint:gocognit if pc.isClosed.get() { return &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } isRenegotation := pc.currentRemoteDescription != nil if _, err := desc.Unmarshal(); err != nil { return err } if err := pc.setDescription(&desc, stateChangeOpSetRemote); err != nil { return err } if err := pc.api.mediaEngine.updateFromRemoteDescription(*desc.parsed); err != nil { return err } var t *RTPTransceiver localTransceivers := append([]*RTPTransceiver{}, pc.GetTransceivers()...) detectedPlanB := descriptionIsPlanB(pc.RemoteDescription(), pc.log) if pc.configuration.SDPSemantics != SDPSemanticsUnifiedPlan { detectedPlanB = descriptionPossiblyPlanB(pc.RemoteDescription()) } weOffer := desc.Type == SDPTypeAnswer if !weOffer && !detectedPlanB { for _, media := range pc.RemoteDescription().parsed.MediaDescriptions { midValue := getMidValue(media) if midValue == "" { return errPeerConnRemoteDescriptionWithoutMidValue } if media.MediaName.Media == mediaSectionApplication { continue } kind := NewRTPCodecType(media.MediaName.Media) direction := getPeerDirection(media) if kind == 0 || direction == RTPTransceiverDirection(Unknown) { continue } t, localTransceivers = findByMid(midValue, localTransceivers) if t == nil { t, localTransceivers = satisfyTypeAndDirection(kind, direction, localTransceivers) } else if direction == RTPTransceiverDirectionInactive { if err := t.Stop(); err != nil { return err } } switch { case t == nil: receiver, err := pc.api.NewRTPReceiver(kind, pc.dtlsTransport) if err != nil { return err } localDirection := RTPTransceiverDirectionRecvonly if direction == RTPTransceiverDirectionRecvonly { localDirection = RTPTransceiverDirectionSendonly } else if direction == RTPTransceiverDirectionInactive { localDirection = RTPTransceiverDirectionInactive } t = newRTPTransceiver(receiver, nil, localDirection, kind, pc.api) pc.mu.Lock() pc.addRTPTransceiver(t) pc.mu.Unlock() // if transceiver is create by remote sdp, set prefer codec same as remote peer if codecs, err := codecsFromMediaDescription(media); err == nil { filteredCodecs := []RTPCodecParameters{} for _, codec := range codecs { if c, matchType := codecParametersFuzzySearch(codec, pc.api.mediaEngine.getCodecsByKind(kind)); matchType == codecMatchExact { // if codec match exact, use payloadtype register to mediaengine codec.PayloadType = c.PayloadType filteredCodecs = append(filteredCodecs, codec) } } _ = t.SetCodecPreferences(filteredCodecs) } case direction == RTPTransceiverDirectionRecvonly: if t.Direction() == RTPTransceiverDirectionSendrecv { t.setDirection(RTPTransceiverDirectionSendonly) } case direction == RTPTransceiverDirectionSendrecv: if t.Direction() == RTPTransceiverDirectionSendonly { t.setDirection(RTPTransceiverDirectionSendrecv) } } if t.Mid() == "" { if err := t.SetMid(midValue); err != nil { return err } } } } remoteUfrag, remotePwd, candidates, err := extractICEDetails(desc.parsed, pc.log) if err != nil { return err } if isRenegotation && pc.iceTransport.haveRemoteCredentialsChange(remoteUfrag, remotePwd) { // An ICE Restart only happens implicitly for a SetRemoteDescription of type offer if !weOffer { if err = pc.iceTransport.restart(); err != nil { return err } } if err = pc.iceTransport.setRemoteCredentials(remoteUfrag, remotePwd); err != nil { return err } } for i := range candidates { if err = pc.iceTransport.AddRemoteCandidate(&candidates[i]); err != nil { return err } } currentTransceivers := append([]*RTPTransceiver{}, pc.GetTransceivers()...) if isRenegotation { if weOffer { _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, true) if err = pc.startRTPSenders(currentTransceivers); err != nil { return err } pc.configureRTPReceivers(true, &desc, currentTransceivers) pc.ops.Enqueue(func() { pc.startRTP(true, &desc, currentTransceivers) }) } return nil } remoteIsLite := isIceLiteSet(desc.parsed) fingerprint, fingerprintHash, err := extractFingerprint(desc.parsed) if err != nil { return err } iceRole := ICERoleControlled // If one of the agents is lite and the other one is not, the lite agent must be the controlling agent. // If both or neither agents are lite the offering agent is controlling. // RFC 8445 S6.1.1 if (weOffer && remoteIsLite == pc.api.settingEngine.candidates.ICELite) || (remoteIsLite && !pc.api.settingEngine.candidates.ICELite) { iceRole = ICERoleControlling } // Start the networking in a new routine since it will block until // the connection is actually established. if weOffer { _ = setRTPTransceiverCurrentDirection(&desc, currentTransceivers, true) if err := pc.startRTPSenders(currentTransceivers); err != nil { return err } pc.configureRTPReceivers(false, &desc, currentTransceivers) } pc.ops.Enqueue(func() { pc.startTransports(iceRole, dtlsRoleFromRemoteSDP(desc.parsed), remoteUfrag, remotePwd, fingerprint, fingerprintHash) if weOffer { pc.startRTP(false, &desc, currentTransceivers) } }) return nil } func (pc *PeerConnection) configureReceiver(incoming trackDetails, receiver *RTPReceiver) { receiver.configureReceive(trackDetailsToRTPReceiveParameters(&incoming)) // set track id and label early so they can be set as new track information // is received from the SDP. for i := range receiver.tracks { receiver.tracks[i].track.mu.Lock() receiver.tracks[i].track.id = incoming.id receiver.tracks[i].track.streamID = incoming.streamID receiver.tracks[i].track.mu.Unlock() } } func (pc *PeerConnection) startReceiver(incoming trackDetails, receiver *RTPReceiver) { if err := receiver.startReceive(trackDetailsToRTPReceiveParameters(&incoming)); err != nil { pc.log.Warnf("RTPReceiver Receive failed %s", err) return } for _, t := range receiver.Tracks() { if t.SSRC() == 0 || t.RID() != "" { return } go func(track *TrackRemote) { b := make([]byte, pc.api.settingEngine.getReceiveMTU()) n, _, err := track.peek(b) if err != nil { pc.log.Warnf("Could not determine PayloadType for SSRC %d (%s)", track.SSRC(), err) return } if err = track.checkAndUpdateTrack(b[:n]); err != nil { pc.log.Warnf("Failed to set codec settings for track SSRC %d (%s)", track.SSRC(), err) return } pc.onTrack(track, receiver) }(t) } } func setRTPTransceiverCurrentDirection(answer *SessionDescription, currentTransceivers []*RTPTransceiver, weOffer bool) error { currentTransceivers = append([]*RTPTransceiver{}, currentTransceivers...) for _, media := range answer.parsed.MediaDescriptions { midValue := getMidValue(media) if midValue == "" { return errPeerConnRemoteDescriptionWithoutMidValue } if media.MediaName.Media == mediaSectionApplication { continue } var t *RTPTransceiver t, currentTransceivers = findByMid(midValue, currentTransceivers) if t == nil { return fmt.Errorf("%w: %q", errPeerConnTranscieverMidNil, midValue) } direction := getPeerDirection(media) if direction == RTPTransceiverDirection(Unknown) { continue } // reverse direction if it was a remote answer if weOffer { switch direction { case RTPTransceiverDirectionSendonly: direction = RTPTransceiverDirectionRecvonly case RTPTransceiverDirectionRecvonly: direction = RTPTransceiverDirectionSendonly default: } } // If a transceiver is created by applying a remote description that has recvonly transceiver, // it will have no sender. In this case, the transceiver's current direction is set to inactive so // that the transceiver can be reused by next AddTrack. if direction == RTPTransceiverDirectionSendonly && t.Sender() == nil { direction = RTPTransceiverDirectionInactive } t.setCurrentDirection(direction) } return nil } func runIfNewReceiver( incomingTrack trackDetails, transceivers []*RTPTransceiver, f func(incomingTrack trackDetails, receiver *RTPReceiver), ) bool { for _, t := range transceivers { if t.Mid() != incomingTrack.mid { continue } receiver := t.Receiver() if (incomingTrack.kind != t.Kind()) || (t.Direction() != RTPTransceiverDirectionRecvonly && t.Direction() != RTPTransceiverDirectionSendrecv) || receiver == nil || (receiver.haveReceived()) { continue } f(incomingTrack, receiver) return true } return false } // configurepRTPReceivers opens knows inbound SRTP streams from the RemoteDescription func (pc *PeerConnection) configureRTPReceivers(isRenegotiation bool, remoteDesc *SessionDescription, currentTransceivers []*RTPTransceiver) { //nolint:gocognit incomingTracks := trackDetailsFromSDP(pc.log, remoteDesc.parsed) if isRenegotiation { for _, t := range currentTransceivers { receiver := t.Receiver() if receiver == nil { continue } tracks := t.Receiver().Tracks() if len(tracks) == 0 { continue } receiverNeedsStopped := false func() { for _, t := range tracks { t.mu.Lock() defer t.mu.Unlock() if t.rid != "" { if details := trackDetailsForRID(incomingTracks, t.rid); details != nil { t.id = details.id t.streamID = details.streamID continue } } else if t.ssrc != 0 { if details := trackDetailsForSSRC(incomingTracks, t.ssrc); details != nil { t.id = details.id t.streamID = details.streamID continue } } receiverNeedsStopped = true } }() if !receiverNeedsStopped { continue } if err := receiver.Stop(); err != nil { pc.log.Warnf("Failed to stop RtpReceiver: %s", err) continue } receiver, err := pc.api.NewRTPReceiver(receiver.kind, pc.dtlsTransport) if err != nil { pc.log.Warnf("Failed to create new RtpReceiver: %s", err) continue } t.setReceiver(receiver) } } localTransceivers := append([]*RTPTransceiver{}, currentTransceivers...) // Ensure we haven't already started a transceiver for this ssrc filteredTracks := append([]trackDetails{}, incomingTracks...) for _, incomingTrack := range incomingTracks { // If we already have a TrackRemote for a given SSRC don't handle it again for _, t := range localTransceivers { if receiver := t.Receiver(); receiver != nil { for _, track := range receiver.Tracks() { for _, ssrc := range incomingTrack.ssrcs { if ssrc == track.SSRC() { filteredTracks = filterTrackWithSSRC(filteredTracks, track.SSRC()) } } } } } } for _, incomingTrack := range filteredTracks { _ = runIfNewReceiver(incomingTrack, localTransceivers, pc.configureReceiver) } } // startRTPReceivers opens knows inbound SRTP streams from the RemoteDescription func (pc *PeerConnection) startRTPReceivers(remoteDesc *SessionDescription, currentTransceivers []*RTPTransceiver) { incomingTracks := trackDetailsFromSDP(pc.log, remoteDesc.parsed) if len(incomingTracks) == 0 { return } localTransceivers := append([]*RTPTransceiver{}, currentTransceivers...) unhandledTracks := incomingTracks[:0] for _, incomingTrack := range incomingTracks { trackHandled := runIfNewReceiver(incomingTrack, localTransceivers, pc.startReceiver) if !trackHandled { unhandledTracks = append(unhandledTracks, incomingTrack) } } remoteIsPlanB := false switch pc.configuration.SDPSemantics { case SDPSemanticsPlanB: remoteIsPlanB = true case SDPSemanticsUnifiedPlanWithFallback: remoteIsPlanB = descriptionPossiblyPlanB(pc.RemoteDescription()) default: // none } if remoteIsPlanB { for _, incomingTrack := range unhandledTracks { t, err := pc.AddTransceiverFromKind(incomingTrack.kind, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendrecv, }) if err != nil { pc.log.Warnf("Could not add transceiver for remote SSRC %d: %s", incomingTrack.ssrcs[0], err) continue } pc.configureReceiver(incomingTrack, t.Receiver()) pc.startReceiver(incomingTrack, t.Receiver()) } } } // startRTPSenders starts all outbound RTP streams func (pc *PeerConnection) startRTPSenders(currentTransceivers []*RTPTransceiver) error { for _, transceiver := range currentTransceivers { if sender := transceiver.Sender(); sender != nil && sender.isNegotiated() && !sender.hasSent() { err := sender.Send(sender.GetParameters()) if err != nil { return err } } } return nil } // Start SCTP subsystem func (pc *PeerConnection) startSCTP() { // Start sctp if err := pc.sctpTransport.Start(SCTPCapabilities{ MaxMessageSize: 0, }); err != nil { pc.log.Warnf("Failed to start SCTP: %s", err) if err = pc.sctpTransport.Stop(); err != nil { pc.log.Warnf("Failed to stop SCTPTransport: %s", err) } return } } func (pc *PeerConnection) handleUndeclaredSSRC(ssrc SSRC, remoteDescription *SessionDescription) (handled bool, err error) { if len(remoteDescription.parsed.MediaDescriptions) != 1 { return false, nil } onlyMediaSection := remoteDescription.parsed.MediaDescriptions[0] streamID := "" id := "" for _, a := range onlyMediaSection.Attributes { switch a.Key { case sdp.AttrKeyMsid: if split := strings.Split(a.Value, " "); len(split) == 2 { streamID = split[0] id = split[1] } case sdp.AttrKeySSRC: return false, errPeerConnSingleMediaSectionHasExplicitSSRC case sdpAttributeRid: return false, nil } } incoming := trackDetails{ ssrcs: []SSRC{ssrc}, kind: RTPCodecTypeVideo, streamID: streamID, id: id, } if onlyMediaSection.MediaName.Media == RTPCodecTypeAudio.String() { incoming.kind = RTPCodecTypeAudio } t, err := pc.AddTransceiverFromKind(incoming.kind, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendrecv, }) if err != nil { return false, fmt.Errorf("%w: %d: %s", errPeerConnRemoteSSRCAddTransceiver, ssrc, err) } pc.configureReceiver(incoming, t.Receiver()) pc.startReceiver(incoming, t.Receiver()) return true, nil } func (pc *PeerConnection) handleIncomingSSRC(rtpStream io.Reader, ssrc SSRC) error { //nolint:gocognit remoteDescription := pc.RemoteDescription() if remoteDescription == nil { return errPeerConnRemoteDescriptionNil } // If a SSRC already exists in the RemoteDescription don't perform heuristics upon it for _, track := range trackDetailsFromSDP(pc.log, remoteDescription.parsed) { if track.repairSsrc != nil && ssrc == *track.repairSsrc { return nil } for _, trackSsrc := range track.ssrcs { if ssrc == trackSsrc { return nil } } } // If the remote SDP was only one media section the ssrc doesn't have to be explicitly declared if handled, err := pc.handleUndeclaredSSRC(ssrc, remoteDescription); handled || err != nil { return err } midExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI}) if !audioSupported && !videoSupported { return errPeerConnSimulcastMidRTPExtensionRequired } streamIDExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESRTPStreamIDURI}) if !audioSupported && !videoSupported { return errPeerConnSimulcastStreamIDRTPExtensionRequired } repairStreamIDExtensionID, _, _ := pc.api.mediaEngine.getHeaderExtensionID(RTPHeaderExtensionCapability{sdesRepairRTPStreamIDURI}) b := make([]byte, pc.api.settingEngine.getReceiveMTU()) i, err := rtpStream.Read(b) if err != nil { return err } var mid, rid, rsid string payloadType, err := handleUnknownRTPPacket(b[:i], uint8(midExtensionID), uint8(streamIDExtensionID), uint8(repairStreamIDExtensionID), &mid, &rid, &rsid) if err != nil { return err } params, err := pc.api.mediaEngine.getRTPParametersByPayloadType(payloadType) if err != nil { return err } streamInfo := createStreamInfo("", ssrc, params.Codecs[0].PayloadType, params.Codecs[0].RTPCodecCapability, params.HeaderExtensions) readStream, interceptor, rtcpReadStream, rtcpInterceptor, err := pc.dtlsTransport.streamsForSSRC(ssrc, *streamInfo) if err != nil { return err } for readCount := 0; readCount <= simulcastProbeCount; readCount++ { if mid == "" || (rid == "" && rsid == "") { i, _, err := interceptor.Read(b, nil) if err != nil { return err } if _, err = handleUnknownRTPPacket(b[:i], uint8(midExtensionID), uint8(streamIDExtensionID), uint8(repairStreamIDExtensionID), &mid, &rid, &rsid); err != nil { return err } continue } for _, t := range pc.GetTransceivers() { receiver := t.Receiver() if t.Mid() != mid || receiver == nil { continue } if rsid != "" { receiver.mu.Lock() defer receiver.mu.Unlock() return receiver.receiveForRtx(SSRC(0), rsid, streamInfo, readStream, interceptor, rtcpReadStream, rtcpInterceptor) } track, err := receiver.receiveForRid(rid, params, streamInfo, readStream, interceptor, rtcpReadStream, rtcpInterceptor) if err != nil { return err } pc.onTrack(track, receiver) return nil } } pc.api.interceptor.UnbindRemoteStream(streamInfo) return errPeerConnSimulcastIncomingSSRCFailed } // undeclaredMediaProcessor handles RTP/RTCP packets that don't match any a:ssrc lines func (pc *PeerConnection) undeclaredMediaProcessor() { go pc.undeclaredRTPMediaProcessor() go pc.undeclaredRTCPMediaProcessor() } func (pc *PeerConnection) undeclaredRTPMediaProcessor() { var simulcastRoutineCount uint64 for { srtpSession, err := pc.dtlsTransport.getSRTPSession() if err != nil { pc.log.Warnf("undeclaredMediaProcessor failed to open SrtpSession: %v", err) return } stream, ssrc, err := srtpSession.AcceptStream() if err != nil { pc.log.Warnf("Failed to accept RTP %v", err) return } if pc.isClosed.get() { if err = stream.Close(); err != nil { pc.log.Warnf("Failed to close RTP stream %v", err) } continue } if atomic.AddUint64(&simulcastRoutineCount, 1) >= simulcastMaxProbeRoutines { atomic.AddUint64(&simulcastRoutineCount, ^uint64(0)) pc.log.Warn(ErrSimulcastProbeOverflow.Error()) pc.dtlsTransport.storeSimulcastStream(stream) continue } go func(rtpStream io.Reader, ssrc SSRC) { if err := pc.handleIncomingSSRC(rtpStream, ssrc); err != nil { pc.log.Errorf(incomingUnhandledRTPSsrc, ssrc, err) pc.dtlsTransport.storeSimulcastStream(stream) } atomic.AddUint64(&simulcastRoutineCount, ^uint64(0)) }(stream, SSRC(ssrc)) } } func (pc *PeerConnection) undeclaredRTCPMediaProcessor() { var unhandledStreams []*srtp.ReadStreamSRTCP defer func() { for _, s := range unhandledStreams { _ = s.Close() } }() for { srtcpSession, err := pc.dtlsTransport.getSRTCPSession() if err != nil { pc.log.Warnf("undeclaredMediaProcessor failed to open SrtcpSession: %v", err) return } stream, ssrc, err := srtcpSession.AcceptStream() if err != nil { pc.log.Warnf("Failed to accept RTCP %v", err) return } pc.log.Warnf("Incoming unhandled RTCP ssrc(%d), OnTrack will not be fired", ssrc) unhandledStreams = append(unhandledStreams, stream) } } // RemoteDescription returns pendingRemoteDescription if it is not null and // otherwise it returns currentRemoteDescription. This property is used to // determine if setRemoteDescription has already been called. // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-remotedescription func (pc *PeerConnection) RemoteDescription() *SessionDescription { pc.mu.RLock() defer pc.mu.RUnlock() if pc.pendingRemoteDescription != nil { return pc.pendingRemoteDescription } return pc.currentRemoteDescription } // AddICECandidate accepts an ICE candidate string and adds it // to the existing set of candidates. func (pc *PeerConnection) AddICECandidate(candidate ICECandidateInit) error { if pc.RemoteDescription() == nil { return &rtcerr.InvalidStateError{Err: ErrNoRemoteDescription} } candidateValue := strings.TrimPrefix(candidate.Candidate, "candidate:") var iceCandidate *ICECandidate if candidateValue != "" { candidate, err := ice.UnmarshalCandidate(candidateValue) if err != nil { if errors.Is(err, ice.ErrUnknownCandidateTyp) || errors.Is(err, ice.ErrDetermineNetworkType) { pc.log.Warnf("Discarding remote candidate: %s", err) return nil } return err } c, err := newICECandidateFromICE(candidate) if err != nil { return err } iceCandidate = &c } return pc.iceTransport.AddRemoteCandidate(iceCandidate) } // ICEConnectionState returns the ICE connection state of the // PeerConnection instance. func (pc *PeerConnection) ICEConnectionState() ICEConnectionState { if state, ok := pc.iceConnectionState.Load().(ICEConnectionState); ok { return state } return ICEConnectionState(0) } // GetSenders returns the RTPSender that are currently attached to this PeerConnection func (pc *PeerConnection) GetSenders() (result []*RTPSender) { pc.mu.Lock() defer pc.mu.Unlock() for _, transceiver := range pc.rtpTransceivers { if sender := transceiver.Sender(); sender != nil { result = append(result, sender) } } return result } // GetReceivers returns the RTPReceivers that are currently attached to this PeerConnection func (pc *PeerConnection) GetReceivers() (receivers []*RTPReceiver) { pc.mu.Lock() defer pc.mu.Unlock() for _, transceiver := range pc.rtpTransceivers { if receiver := transceiver.Receiver(); receiver != nil { receivers = append(receivers, receiver) } } return } // GetTransceivers returns the RtpTransceiver that are currently attached to this PeerConnection func (pc *PeerConnection) GetTransceivers() []*RTPTransceiver { pc.mu.Lock() defer pc.mu.Unlock() return pc.rtpTransceivers } // AddTrack adds a Track to the PeerConnection func (pc *PeerConnection) AddTrack(track TrackLocal) (*RTPSender, error) { if pc.isClosed.get() { return nil, &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } pc.mu.Lock() defer pc.mu.Unlock() for _, t := range pc.rtpTransceivers { currentDirection := t.getCurrentDirection() // According to https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-addtrack, if the // transceiver can be reused only if it's currentDirection never be sendrecv or sendonly. // But that will cause sdp inflate. So we only check currentDirection's current value, // that's worked for all browsers. if !t.stopped && t.kind == track.Kind() && t.Sender() == nil && !(currentDirection == RTPTransceiverDirectionSendrecv || currentDirection == RTPTransceiverDirectionSendonly) { sender, err := pc.api.NewRTPSender(track, pc.dtlsTransport) if err == nil { err = t.SetSender(sender, track) if err != nil { _ = sender.Stop() t.setSender(nil) } } if err != nil { return nil, err } pc.onNegotiationNeeded() return sender, nil } } transceiver, err := pc.newTransceiverFromTrack(RTPTransceiverDirectionSendrecv, track) if err != nil { return nil, err } pc.addRTPTransceiver(transceiver) return transceiver.Sender(), nil } // RemoveTrack removes a Track from the PeerConnection func (pc *PeerConnection) RemoveTrack(sender *RTPSender) (err error) { if pc.isClosed.get() { return &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } var transceiver *RTPTransceiver pc.mu.Lock() defer pc.mu.Unlock() for _, t := range pc.rtpTransceivers { if t.Sender() == sender { transceiver = t break } } if transceiver == nil { return &rtcerr.InvalidAccessError{Err: ErrSenderNotCreatedByConnection} } else if err = sender.Stop(); err == nil { err = transceiver.setSendingTrack(nil) if err == nil { pc.onNegotiationNeeded() } } return } func (pc *PeerConnection) newTransceiverFromTrack(direction RTPTransceiverDirection, track TrackLocal) (t *RTPTransceiver, err error) { var ( r *RTPReceiver s *RTPSender ) switch direction { case RTPTransceiverDirectionSendrecv: r, err = pc.api.NewRTPReceiver(track.Kind(), pc.dtlsTransport) if err != nil { return } s, err = pc.api.NewRTPSender(track, pc.dtlsTransport) case RTPTransceiverDirectionSendonly: s, err = pc.api.NewRTPSender(track, pc.dtlsTransport) default: err = errPeerConnAddTransceiverFromTrackSupport } if err != nil { return } return newRTPTransceiver(r, s, direction, track.Kind(), pc.api), nil } // AddTransceiverFromKind Create a new RtpTransceiver and adds it to the set of transceivers. func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RTPTransceiverInit) (t *RTPTransceiver, err error) { if pc.isClosed.get() { return nil, &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } direction := RTPTransceiverDirectionSendrecv if len(init) > 1 { return nil, errPeerConnAddTransceiverFromKindOnlyAcceptsOne } else if len(init) == 1 { direction = init[0].Direction } switch direction { case RTPTransceiverDirectionSendonly, RTPTransceiverDirectionSendrecv: codecs := pc.api.mediaEngine.getCodecsByKind(kind) if len(codecs) == 0 { return nil, ErrNoCodecsAvailable } track, err := NewTrackLocalStaticSample(codecs[0].RTPCodecCapability, util.MathRandAlpha(16), util.MathRandAlpha(16)) if err != nil { return nil, err } t, err = pc.newTransceiverFromTrack(direction, track) if err != nil { return nil, err } case RTPTransceiverDirectionRecvonly: receiver, err := pc.api.NewRTPReceiver(kind, pc.dtlsTransport) if err != nil { return nil, err } t = newRTPTransceiver(receiver, nil, RTPTransceiverDirectionRecvonly, kind, pc.api) default: return nil, errPeerConnAddTransceiverFromKindSupport } pc.mu.Lock() pc.addRTPTransceiver(t) pc.mu.Unlock() return t, nil } // AddTransceiverFromTrack Create a new RtpTransceiver(SendRecv or SendOnly) and add it to the set of transceivers. func (pc *PeerConnection) AddTransceiverFromTrack(track TrackLocal, init ...RTPTransceiverInit) (t *RTPTransceiver, err error) { if pc.isClosed.get() { return nil, &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } direction := RTPTransceiverDirectionSendrecv if len(init) > 1 { return nil, errPeerConnAddTransceiverFromTrackOnlyAcceptsOne } else if len(init) == 1 { direction = init[0].Direction } t, err = pc.newTransceiverFromTrack(direction, track) if err == nil { pc.mu.Lock() pc.addRTPTransceiver(t) pc.mu.Unlock() } return } // CreateDataChannel creates a new DataChannel object with the given label // and optional DataChannelInit used to configure properties of the // underlying channel such as data reliability. func (pc *PeerConnection) CreateDataChannel(label string, options *DataChannelInit) (*DataChannel, error) { // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #2) if pc.isClosed.get() { return nil, &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } params := &DataChannelParameters{ Label: label, Ordered: true, } // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #19) if options != nil { params.ID = options.ID } if options != nil { // Ordered indicates if data is allowed to be delivered out of order. The // default value of true, guarantees that data will be delivered in order. // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #9) if options.Ordered != nil { params.Ordered = *options.Ordered } // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #7) if options.MaxPacketLifeTime != nil { params.MaxPacketLifeTime = options.MaxPacketLifeTime } // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #8) if options.MaxRetransmits != nil { params.MaxRetransmits = options.MaxRetransmits } // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #10) if options.Protocol != nil { params.Protocol = *options.Protocol } // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #11) if len(params.Protocol) > 65535 { return nil, &rtcerr.TypeError{Err: ErrProtocolTooLarge} } // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #12) if options.Negotiated != nil { params.Negotiated = *options.Negotiated } } d, err := pc.api.newDataChannel(params, pc.log) if err != nil { return nil, err } // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #16) if d.maxPacketLifeTime != nil && d.maxRetransmits != nil { return nil, &rtcerr.TypeError{Err: ErrRetransmitsOrPacketLifeTime} } pc.sctpTransport.lock.Lock() pc.sctpTransport.dataChannels = append(pc.sctpTransport.dataChannels, d) pc.sctpTransport.dataChannelsRequested++ pc.sctpTransport.lock.Unlock() // If SCTP already connected open all the channels if pc.sctpTransport.State() == SCTPTransportStateConnected { if err = d.open(pc.sctpTransport); err != nil { return nil, err } } pc.mu.Lock() pc.onNegotiationNeeded() pc.mu.Unlock() return d, nil } // SetIdentityProvider is used to configure an identity provider to generate identity assertions func (pc *PeerConnection) SetIdentityProvider(provider string) error { return errPeerConnSetIdentityProviderNotImplemented } // WriteRTCP sends a user provided RTCP packet to the connected peer. If no peer is connected the // packet is discarded. It also runs any configured interceptors. func (pc *PeerConnection) WriteRTCP(pkts []rtcp.Packet) error { _, err := pc.interceptorRTCPWriter.Write(pkts, make(interceptor.Attributes)) return err } func (pc *PeerConnection) writeRTCP(pkts []rtcp.Packet, _ interceptor.Attributes) (int, error) { return pc.dtlsTransport.WriteRTCP(pkts) } // Close ends the PeerConnection func (pc *PeerConnection) Close() error { // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #1) // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #2) if pc.isClosed.swap(true) { return nil } // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #3) pc.signalingState.Set(SignalingStateClosed) // Try closing everything and collect the errors // Shutdown strategy: // 1. All Conn close by closing their underlying Conn. // 2. A Mux stops this chain. It won't close the underlying // Conn if one of the endpoints is closed down. To // continue the chain the Mux has to be closed. closeErrs := make([]error, 4) closeErrs = append(closeErrs, pc.api.interceptor.Close()) // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #4) pc.mu.Lock() for _, t := range pc.rtpTransceivers { if !t.stopped { closeErrs = append(closeErrs, t.Stop()) } } pc.mu.Unlock() // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #5) pc.sctpTransport.lock.Lock() for _, d := range pc.sctpTransport.dataChannels { d.setReadyState(DataChannelStateClosed) } pc.sctpTransport.lock.Unlock() // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #6) if pc.sctpTransport != nil { closeErrs = append(closeErrs, pc.sctpTransport.Stop()) } // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #7) closeErrs = append(closeErrs, pc.dtlsTransport.Stop()) // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #8, #9, #10) if pc.iceTransport != nil { closeErrs = append(closeErrs, pc.iceTransport.Stop()) } // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #11) pc.updateConnectionState(pc.ICEConnectionState(), pc.dtlsTransport.State()) return util.FlattenErrs(closeErrs) } // addRTPTransceiver appends t into rtpTransceivers // and fires onNegotiationNeeded; // caller of this method should hold `pc.mu` lock func (pc *PeerConnection) addRTPTransceiver(t *RTPTransceiver) { pc.rtpTransceivers = append(pc.rtpTransceivers, t) pc.onNegotiationNeeded() } // CurrentLocalDescription represents the local description that was // successfully negotiated the last time the PeerConnection transitioned // into the stable state plus any local candidates that have been generated // by the ICEAgent since the offer or answer was created. func (pc *PeerConnection) CurrentLocalDescription() *SessionDescription { pc.mu.Lock() localDescription := pc.currentLocalDescription iceGather := pc.iceGatherer iceGatheringState := pc.ICEGatheringState() pc.mu.Unlock() return populateLocalCandidates(localDescription, iceGather, iceGatheringState) } // PendingLocalDescription represents a local description that is in the // process of being negotiated plus any local candidates that have been // generated by the ICEAgent since the offer or answer was created. If the // PeerConnection is in the stable state, the value is null. func (pc *PeerConnection) PendingLocalDescription() *SessionDescription { pc.mu.Lock() localDescription := pc.pendingLocalDescription iceGather := pc.iceGatherer iceGatheringState := pc.ICEGatheringState() pc.mu.Unlock() return populateLocalCandidates(localDescription, iceGather, iceGatheringState) } // CurrentRemoteDescription represents the last remote description that was // successfully negotiated the last time the PeerConnection transitioned // into the stable state plus any remote candidates that have been supplied // via AddICECandidate() since the offer or answer was created. func (pc *PeerConnection) CurrentRemoteDescription() *SessionDescription { pc.mu.RLock() defer pc.mu.RUnlock() return pc.currentRemoteDescription } // PendingRemoteDescription represents a remote description that is in the // process of being negotiated, complete with any remote candidates that // have been supplied via AddICECandidate() since the offer or answer was // created. If the PeerConnection is in the stable state, the value is // null. func (pc *PeerConnection) PendingRemoteDescription() *SessionDescription { pc.mu.RLock() defer pc.mu.RUnlock() return pc.pendingRemoteDescription } // SignalingState attribute returns the signaling state of the // PeerConnection instance. func (pc *PeerConnection) SignalingState() SignalingState { return pc.signalingState.Get() } // ICEGatheringState attribute returns the ICE gathering state of the // PeerConnection instance. func (pc *PeerConnection) ICEGatheringState() ICEGatheringState { if pc.iceGatherer == nil { return ICEGatheringStateNew } switch pc.iceGatherer.State() { case ICEGathererStateNew: return ICEGatheringStateNew case ICEGathererStateGathering: return ICEGatheringStateGathering default: return ICEGatheringStateComplete } } // ConnectionState attribute returns the connection state of the // PeerConnection instance. func (pc *PeerConnection) ConnectionState() PeerConnectionState { if state, ok := pc.connectionState.Load().(PeerConnectionState); ok { return state } return PeerConnectionState(0) } // GetStats return data providing statistics about the overall connection func (pc *PeerConnection) GetStats() StatsReport { var ( dataChannelsAccepted uint32 dataChannelsClosed uint32 dataChannelsOpened uint32 dataChannelsRequested uint32 ) statsCollector := newStatsReportCollector() statsCollector.Collecting() pc.mu.Lock() if pc.iceGatherer != nil { pc.iceGatherer.collectStats(statsCollector) } if pc.iceTransport != nil { pc.iceTransport.collectStats(statsCollector) } pc.sctpTransport.lock.Lock() dataChannels := append([]*DataChannel{}, pc.sctpTransport.dataChannels...) dataChannelsAccepted = pc.sctpTransport.dataChannelsAccepted dataChannelsOpened = pc.sctpTransport.dataChannelsOpened dataChannelsRequested = pc.sctpTransport.dataChannelsRequested pc.sctpTransport.lock.Unlock() for _, d := range dataChannels { state := d.ReadyState() if state != DataChannelStateConnecting && state != DataChannelStateOpen { dataChannelsClosed++ } d.collectStats(statsCollector) } pc.sctpTransport.collectStats(statsCollector) stats := PeerConnectionStats{ Timestamp: statsTimestampNow(), Type: StatsTypePeerConnection, ID: pc.statsID, DataChannelsAccepted: dataChannelsAccepted, DataChannelsClosed: dataChannelsClosed, DataChannelsOpened: dataChannelsOpened, DataChannelsRequested: dataChannelsRequested, } statsCollector.Collect(stats.ID, stats) certificates := pc.configuration.Certificates for _, certificate := range certificates { if err := certificate.collectStats(statsCollector); err != nil { continue } } pc.mu.Unlock() pc.api.mediaEngine.collectStats(statsCollector) return statsCollector.Ready() } // Start all transports. PeerConnection now has enough state func (pc *PeerConnection) startTransports(iceRole ICERole, dtlsRole DTLSRole, remoteUfrag, remotePwd, fingerprint, fingerprintHash string) { // Start the ice transport err := pc.iceTransport.Start( pc.iceGatherer, ICEParameters{ UsernameFragment: remoteUfrag, Password: remotePwd, ICELite: false, }, &iceRole, ) if err != nil { pc.log.Warnf("Failed to start manager: %s", err) return } // Start the dtls transport err = pc.dtlsTransport.Start(DTLSParameters{ Role: dtlsRole, Fingerprints: []DTLSFingerprint{{Algorithm: fingerprintHash, Value: fingerprint}}, }) pc.updateConnectionState(pc.ICEConnectionState(), pc.dtlsTransport.State()) if err != nil { pc.log.Warnf("Failed to start manager: %s", err) return } } // nolint: gocognit func (pc *PeerConnection) startRTP(isRenegotiation bool, remoteDesc *SessionDescription, currentTransceivers []*RTPTransceiver) { if !isRenegotiation { pc.undeclaredMediaProcessor() } pc.startRTPReceivers(remoteDesc, currentTransceivers) if haveApplicationMediaSection(remoteDesc.parsed) { pc.startSCTP() } } // generateUnmatchedSDP generates an SDP that doesn't take remote state into account // This is used for the initial call for CreateOffer func (pc *PeerConnection) generateUnmatchedSDP(transceivers []*RTPTransceiver, useIdentity bool) (*sdp.SessionDescription, error) { d, err := sdp.NewJSEPSessionDescription(useIdentity) if err != nil { return nil, err } iceParams, err := pc.iceGatherer.GetLocalParameters() if err != nil { return nil, err } candidates, err := pc.iceGatherer.GetLocalCandidates() if err != nil { return nil, err } isPlanB := pc.configuration.SDPSemantics == SDPSemanticsPlanB mediaSections := []mediaSection{} // Needed for pc.sctpTransport.dataChannelsRequested pc.sctpTransport.lock.Lock() defer pc.sctpTransport.lock.Unlock() if isPlanB { video := make([]*RTPTransceiver, 0) audio := make([]*RTPTransceiver, 0) for _, t := range transceivers { if t.kind == RTPCodecTypeVideo { video = append(video, t) } else if t.kind == RTPCodecTypeAudio { audio = append(audio, t) } if sender := t.Sender(); sender != nil { sender.setNegotiated() } } if len(video) > 0 { mediaSections = append(mediaSections, mediaSection{id: "video", transceivers: video}) } if len(audio) > 0 { mediaSections = append(mediaSections, mediaSection{id: "audio", transceivers: audio}) } if pc.sctpTransport.dataChannelsRequested != 0 { mediaSections = append(mediaSections, mediaSection{id: "data", data: true}) } } else { for _, t := range transceivers { if sender := t.Sender(); sender != nil { sender.setNegotiated() } mediaSections = append(mediaSections, mediaSection{id: t.Mid(), transceivers: []*RTPTransceiver{t}}) } if pc.sctpTransport.dataChannelsRequested != 0 { mediaSections = append(mediaSections, mediaSection{id: strconv.Itoa(len(mediaSections)), data: true}) } } dtlsFingerprints, err := pc.configuration.Certificates[0].GetFingerprints() if err != nil { return nil, err } return populateSDP(d, isPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, true, pc.api.mediaEngine, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), candidates, iceParams, mediaSections, pc.ICEGatheringState()) } // generateMatchedSDP generates a SDP and takes the remote state into account // this is used everytime we have a RemoteDescription // nolint: gocyclo func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, useIdentity bool, includeUnmatched bool, connectionRole sdp.ConnectionRole) (*sdp.SessionDescription, error) { //nolint:gocognit d, err := sdp.NewJSEPSessionDescription(useIdentity) if err != nil { return nil, err } iceParams, err := pc.iceGatherer.GetLocalParameters() if err != nil { return nil, err } candidates, err := pc.iceGatherer.GetLocalCandidates() if err != nil { return nil, err } var t *RTPTransceiver remoteDescription := pc.currentRemoteDescription if pc.pendingRemoteDescription != nil { remoteDescription = pc.pendingRemoteDescription } isExtmapAllowMixed := isExtMapAllowMixedSet(remoteDescription.parsed) localTransceivers := append([]*RTPTransceiver{}, transceivers...) detectedPlanB := descriptionIsPlanB(remoteDescription, pc.log) if pc.configuration.SDPSemantics != SDPSemanticsUnifiedPlan { detectedPlanB = descriptionPossiblyPlanB(remoteDescription) } mediaSections := []mediaSection{} alreadyHaveApplicationMediaSection := false for _, media := range remoteDescription.parsed.MediaDescriptions { midValue := getMidValue(media) if midValue == "" { return nil, errPeerConnRemoteDescriptionWithoutMidValue } if media.MediaName.Media == mediaSectionApplication { mediaSections = append(mediaSections, mediaSection{id: midValue, data: true}) alreadyHaveApplicationMediaSection = true continue } kind := NewRTPCodecType(media.MediaName.Media) direction := getPeerDirection(media) if kind == 0 || direction == RTPTransceiverDirection(Unknown) { continue } sdpSemantics := pc.configuration.SDPSemantics switch { case sdpSemantics == SDPSemanticsPlanB || sdpSemantics == SDPSemanticsUnifiedPlanWithFallback && detectedPlanB: if !detectedPlanB { return nil, &rtcerr.TypeError{Err: fmt.Errorf("%w: Expected PlanB, but RemoteDescription is UnifiedPlan", ErrIncorrectSDPSemantics)} } // If we're responding to a plan-b offer, then we should try to fill up this // media entry with all matching local transceivers mediaTransceivers := []*RTPTransceiver{} for { // keep going until we can't get any more t, localTransceivers = satisfyTypeAndDirection(kind, direction, localTransceivers) if t == nil { if len(mediaTransceivers) == 0 { t = &RTPTransceiver{kind: kind, api: pc.api, codecs: pc.api.mediaEngine.getCodecsByKind(kind)} t.setDirection(RTPTransceiverDirectionInactive) mediaTransceivers = append(mediaTransceivers, t) } break } if sender := t.Sender(); sender != nil { sender.setNegotiated() } mediaTransceivers = append(mediaTransceivers, t) } mediaSections = append(mediaSections, mediaSection{id: midValue, transceivers: mediaTransceivers}) case sdpSemantics == SDPSemanticsUnifiedPlan || sdpSemantics == SDPSemanticsUnifiedPlanWithFallback: if detectedPlanB { return nil, &rtcerr.TypeError{Err: fmt.Errorf("%w: Expected UnifiedPlan, but RemoteDescription is PlanB", ErrIncorrectSDPSemantics)} } t, localTransceivers = findByMid(midValue, localTransceivers) if t == nil { return nil, fmt.Errorf("%w: %q", errPeerConnTranscieverMidNil, midValue) } if sender := t.Sender(); sender != nil { sender.setNegotiated() } mediaTransceivers := []*RTPTransceiver{t} mediaSections = append(mediaSections, mediaSection{id: midValue, transceivers: mediaTransceivers, ridMap: getRids(media)}) } } // If we are offering also include unmatched local transceivers if includeUnmatched { if !detectedPlanB { for _, t := range localTransceivers { if sender := t.Sender(); sender != nil { sender.setNegotiated() } mediaSections = append(mediaSections, mediaSection{id: t.Mid(), transceivers: []*RTPTransceiver{t}}) } } if pc.sctpTransport.dataChannelsRequested != 0 && !alreadyHaveApplicationMediaSection { if detectedPlanB { mediaSections = append(mediaSections, mediaSection{id: "data", data: true}) } else { mediaSections = append(mediaSections, mediaSection{id: strconv.Itoa(len(mediaSections)), data: true}) } } } if pc.configuration.SDPSemantics == SDPSemanticsUnifiedPlanWithFallback && detectedPlanB { pc.log.Info("Plan-B Offer detected; responding with Plan-B Answer") } dtlsFingerprints, err := pc.configuration.Certificates[0].GetFingerprints() if err != nil { return nil, err } return populateSDP(d, detectedPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, isExtmapAllowMixed, pc.api.mediaEngine, connectionRole, candidates, iceParams, mediaSections, pc.ICEGatheringState()) } func (pc *PeerConnection) setGatherCompleteHandler(handler func()) { pc.iceGatherer.onGatheringCompleteHandler.Store(handler) } // SCTP returns the SCTPTransport for this PeerConnection // // The SCTP transport over which SCTP data is sent and received. If SCTP has not been negotiated, the value is nil. // https://www.w3.org/TR/webrtc/#attributes-15 func (pc *PeerConnection) SCTP() *SCTPTransport { return pc.sctpTransport } webrtc-3.1.56/peerconnection_close_test.go000066400000000000000000000071711437620512100206710ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "testing" "time" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) func TestPeerConnection_Close(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 20) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } awaitSetup := make(chan struct{}) pcAnswer.OnDataChannel(func(d *DataChannel) { // Make sure this is the data channel we were looking for. (Not the one // created in signalPair). if d.Label() != "data" { return } close(awaitSetup) }) awaitICEClosed := make(chan struct{}) pcAnswer.OnICEConnectionStateChange(func(i ICEConnectionState) { if i == ICEConnectionStateClosed { close(awaitICEClosed) } }) _, err = pcOffer.CreateDataChannel("data", nil) if err != nil { t.Fatal(err) } err = signalPair(pcOffer, pcAnswer) if err != nil { t.Fatal(err) } <-awaitSetup closePairNow(t, pcOffer, pcAnswer) <-awaitICEClosed } // Assert that a PeerConnection that is shutdown before ICE starts doesn't leak func TestPeerConnection_Close_PreICE(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } _, err = pcOffer.CreateDataChannel("test-channel", nil) if err != nil { t.Fatal(err) } answer, err := pcOffer.CreateOffer(nil) if err != nil { t.Fatal(err) } assert.NoError(t, pcOffer.Close()) if err = pcAnswer.SetRemoteDescription(answer); err != nil { t.Fatal(err) } for { if pcAnswer.iceTransport.State() == ICETransportStateChecking { break } time.Sleep(time.Second / 4) } assert.NoError(t, pcAnswer.Close()) // Assert that ICETransport is shutdown, test timeout will prevent deadlock for { if pcAnswer.iceTransport.State() == ICETransportStateClosed { return } time.Sleep(time.Second / 4) } } func TestPeerConnection_Close_DuringICE(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } closedOffer := make(chan struct{}) closedAnswer := make(chan struct{}) pcAnswer.OnICEConnectionStateChange(func(iceState ICEConnectionState) { if iceState == ICEConnectionStateConnected { go func() { assert.NoError(t, pcAnswer.Close()) close(closedAnswer) assert.NoError(t, pcOffer.Close()) close(closedOffer) }() } }) _, err = pcOffer.CreateDataChannel("test-channel", nil) if err != nil { t.Fatal(err) } offer, err := pcOffer.CreateOffer(nil) if err != nil { t.Fatal(err) } offerGatheringComplete := GatheringCompletePromise(pcOffer) if err = pcOffer.SetLocalDescription(offer); err != nil { t.Fatal(err) } <-offerGatheringComplete if err = pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()); err != nil { t.Fatal(err) } answer, err := pcAnswer.CreateAnswer(nil) if err != nil { t.Fatal(err) } answerGatheringComplete := GatheringCompletePromise(pcAnswer) if err = pcAnswer.SetLocalDescription(answer); err != nil { t.Fatal(err) } <-answerGatheringComplete if err = pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()); err != nil { t.Fatal(err) } select { case <-closedAnswer: case <-time.After(5 * time.Second): t.Error("pcAnswer.Close() Timeout") } select { case <-closedOffer: case <-time.After(5 * time.Second): t.Error("pcOffer.Close() Timeout") } } webrtc-3.1.56/peerconnection_go_test.go000066400000000000000000001344111437620512100201670ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "bufio" "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "fmt" "math/big" "reflect" "regexp" "strings" "sync" "testing" "time" "github.com/pion/ice/v2" "github.com/pion/rtp" "github.com/pion/transport/v2/test" "github.com/pion/transport/v2/vnet" "github.com/pion/webrtc/v3/internal/util" "github.com/pion/webrtc/v3/pkg/rtcerr" "github.com/stretchr/testify/assert" ) // newPair creates two new peer connections (an offerer and an answerer) using // the api. func (api *API) newPair(cfg Configuration) (pcOffer *PeerConnection, pcAnswer *PeerConnection, err error) { pca, err := api.NewPeerConnection(cfg) if err != nil { return nil, nil, err } pcb, err := api.NewPeerConnection(cfg) if err != nil { return nil, nil, err } return pca, pcb, nil } func TestNew_Go(t *testing.T) { report := test.CheckRoutines(t) defer report() api := NewAPI() t.Run("Success", func(t *testing.T) { secretKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) certificate, err := GenerateCertificate(secretKey) assert.Nil(t, err) pc, err := api.NewPeerConnection(Configuration{ ICEServers: []ICEServer{ { URLs: []string{ "stun:stun.l.google.com:19302", "turns:google.de?transport=tcp", }, Username: "unittest", Credential: OAuthCredential{ MACKey: "WmtzanB3ZW9peFhtdm42NzUzNG0=", AccessToken: "AAwg3kPHWPfvk9bDFL936wYvkoctMADzQ==", }, CredentialType: ICECredentialTypeOauth, }, }, ICETransportPolicy: ICETransportPolicyRelay, BundlePolicy: BundlePolicyMaxCompat, RTCPMuxPolicy: RTCPMuxPolicyNegotiate, PeerIdentity: "unittest", Certificates: []Certificate{*certificate}, ICECandidatePoolSize: 5, }) assert.Nil(t, err) assert.NotNil(t, pc) assert.NoError(t, pc.Close()) }) t.Run("Failure", func(t *testing.T) { testCases := []struct { initialize func() (*PeerConnection, error) expectedErr error }{ {func() (*PeerConnection, error) { secretKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) certificate, err := NewCertificate(secretKey, x509.Certificate{ Version: 2, SerialNumber: big.NewInt(1653), NotBefore: time.Now().AddDate(0, -2, 0), NotAfter: time.Now().AddDate(0, -1, 0), }) assert.Nil(t, err) return api.NewPeerConnection(Configuration{ Certificates: []Certificate{*certificate}, }) }, &rtcerr.InvalidAccessError{Err: ErrCertificateExpired}}, {func() (*PeerConnection, error) { return api.NewPeerConnection(Configuration{ ICEServers: []ICEServer{ { URLs: []string{ "stun:stun.l.google.com:19302", "turns:google.de?transport=tcp", }, Username: "unittest", }, }, }) }, &rtcerr.InvalidAccessError{Err: ErrNoTurnCredentials}}, } for i, testCase := range testCases { pc, err := testCase.initialize() assert.EqualError(t, err, testCase.expectedErr.Error(), "testCase: %d %v", i, testCase, ) if pc != nil { assert.NoError(t, pc.Close()) } } }) t.Run("ICEServers_Copy", func(t *testing.T) { const expectedURL = "stun:stun.l.google.com:19302?foo=bar" const expectedUsername = "username" const expectedPassword = "password" cfg := Configuration{ ICEServers: []ICEServer{ { URLs: []string{expectedURL}, Username: expectedUsername, Credential: expectedPassword, }, }, } pc, err := api.NewPeerConnection(cfg) assert.NoError(t, err) assert.NotNil(t, pc) pc.configuration.ICEServers[0].Username = util.MathRandAlpha(15) // Tests doesn't need crypto random pc.configuration.ICEServers[0].Credential = util.MathRandAlpha(15) pc.configuration.ICEServers[0].URLs[0] = util.MathRandAlpha(15) assert.Equal(t, expectedUsername, cfg.ICEServers[0].Username) assert.Equal(t, expectedPassword, cfg.ICEServers[0].Credential) assert.Equal(t, expectedURL, cfg.ICEServers[0].URLs[0]) assert.NoError(t, pc.Close()) }) } func TestPeerConnection_SetConfiguration_Go(t *testing.T) { // Note: this test includes all SetConfiguration features that are supported // by Go but not the WASM bindings, namely: ICEServer.Credential, // ICEServer.CredentialType, and Certificates. report := test.CheckRoutines(t) defer report() api := NewAPI() secretKey1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) certificate1, err := GenerateCertificate(secretKey1) assert.Nil(t, err) secretKey2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(t, err) certificate2, err := GenerateCertificate(secretKey2) assert.Nil(t, err) for _, test := range []struct { name string init func() (*PeerConnection, error) config Configuration wantErr error }{ { name: "valid", init: func() (*PeerConnection, error) { pc, err := api.NewPeerConnection(Configuration{ PeerIdentity: "unittest", Certificates: []Certificate{*certificate1}, ICECandidatePoolSize: 5, }) if err != nil { return pc, err } err = pc.SetConfiguration(Configuration{ ICEServers: []ICEServer{ { URLs: []string{ "stun:stun.l.google.com:19302", "turns:google.de?transport=tcp", }, Username: "unittest", Credential: OAuthCredential{ MACKey: "WmtzanB3ZW9peFhtdm42NzUzNG0=", AccessToken: "AAwg3kPHWPfvk9bDFL936wYvkoctMADzQ==", }, CredentialType: ICECredentialTypeOauth, }, }, ICETransportPolicy: ICETransportPolicyAll, BundlePolicy: BundlePolicyBalanced, RTCPMuxPolicy: RTCPMuxPolicyRequire, PeerIdentity: "unittest", Certificates: []Certificate{*certificate1}, ICECandidatePoolSize: 5, }) if err != nil { return pc, err } return pc, nil }, config: Configuration{}, wantErr: nil, }, { name: "update multiple certificates", init: func() (*PeerConnection, error) { return api.NewPeerConnection(Configuration{}) }, config: Configuration{ Certificates: []Certificate{*certificate1, *certificate2}, }, wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}, }, { name: "update certificate", init: func() (*PeerConnection, error) { return api.NewPeerConnection(Configuration{}) }, config: Configuration{ Certificates: []Certificate{*certificate1}, }, wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}, }, { name: "update ICEServers, no TURN credentials", init: func() (*PeerConnection, error) { return NewPeerConnection(Configuration{}) }, config: Configuration{ ICEServers: []ICEServer{ { URLs: []string{ "stun:stun.l.google.com:19302", "turns:google.de?transport=tcp", }, Username: "unittest", }, }, }, wantErr: &rtcerr.InvalidAccessError{Err: ErrNoTurnCredentials}, }, } { pc, err := test.init() if err != nil { t.Errorf("SetConfiguration %q: init failed: %v", test.name, err) } err = pc.SetConfiguration(test.config) if got, want := err, test.wantErr; !reflect.DeepEqual(got, want) { t.Errorf("SetConfiguration %q: err = %v, want %v", test.name, got, want) } assert.NoError(t, pc.Close()) } } func TestPeerConnection_EventHandlers_Go(t *testing.T) { lim := test.TimeOut(time.Second * 5) defer lim.Stop() report := test.CheckRoutines(t) defer report() // Note: When testing the Go event handlers we peer into the state a bit more // than what is possible for the environment agnostic (Go or WASM/JavaScript) // EventHandlers test. api := NewAPI() pc, err := api.NewPeerConnection(Configuration{}) assert.Nil(t, err) onTrackCalled := make(chan struct{}) onICEConnectionStateChangeCalled := make(chan struct{}) onDataChannelCalled := make(chan struct{}) // Verify that the noop case works assert.NotPanics(t, func() { pc.onTrack(nil, nil) }) assert.NotPanics(t, func() { pc.onICEConnectionStateChange(ice.ConnectionStateNew) }) pc.OnTrack(func(t *TrackRemote, r *RTPReceiver) { close(onTrackCalled) }) pc.OnICEConnectionStateChange(func(cs ICEConnectionState) { close(onICEConnectionStateChangeCalled) }) pc.OnDataChannel(func(dc *DataChannel) { // Questions: // (1) How come this callback is made with dc being nil? // (2) How come this callback is made without CreateDataChannel? if dc != nil { close(onDataChannelCalled) } }) // Verify that the handlers deal with nil inputs assert.NotPanics(t, func() { pc.onTrack(nil, nil) }) assert.NotPanics(t, func() { go pc.onDataChannelHandler(nil) }) // Verify that the set handlers are called assert.NotPanics(t, func() { pc.onTrack(&TrackRemote{}, &RTPReceiver{}) }) assert.NotPanics(t, func() { pc.onICEConnectionStateChange(ice.ConnectionStateNew) }) assert.NotPanics(t, func() { go pc.onDataChannelHandler(&DataChannel{api: api}) }) <-onTrackCalled <-onICEConnectionStateChangeCalled <-onDataChannelCalled assert.NoError(t, pc.Close()) } // This test asserts that nothing deadlocks we try to shutdown when DTLS is in flight // We ensure that DTLS is in flight by removing the mux func for it, so all inbound DTLS is lost func TestPeerConnection_ShutdownNoDTLS(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() api := NewAPI() offerPC, answerPC, err := api.newPair(Configuration{}) if err != nil { t.Fatal(err) } // Drop all incoming DTLS traffic dropAllDTLS := func([]byte) bool { return false } offerPC.dtlsTransport.dtlsMatcher = dropAllDTLS answerPC.dtlsTransport.dtlsMatcher = dropAllDTLS if err = signalPair(offerPC, answerPC); err != nil { t.Fatal(err) } iceComplete := make(chan interface{}) answerPC.OnICEConnectionStateChange(func(iceState ICEConnectionState) { if iceState == ICEConnectionStateConnected { time.Sleep(time.Second) // Give time for DTLS to start select { case <-iceComplete: default: close(iceComplete) } } }) <-iceComplete closePairNow(t, offerPC, answerPC) } func TestPeerConnection_PropertyGetters(t *testing.T) { pc := &PeerConnection{ currentLocalDescription: &SessionDescription{}, pendingLocalDescription: &SessionDescription{}, currentRemoteDescription: &SessionDescription{}, pendingRemoteDescription: &SessionDescription{}, signalingState: SignalingStateHaveLocalOffer, } pc.iceConnectionState.Store(ICEConnectionStateChecking) pc.connectionState.Store(PeerConnectionStateConnecting) assert.Equal(t, pc.currentLocalDescription, pc.CurrentLocalDescription(), "should match") assert.Equal(t, pc.pendingLocalDescription, pc.PendingLocalDescription(), "should match") assert.Equal(t, pc.currentRemoteDescription, pc.CurrentRemoteDescription(), "should match") assert.Equal(t, pc.pendingRemoteDescription, pc.PendingRemoteDescription(), "should match") assert.Equal(t, pc.signalingState, pc.SignalingState(), "should match") assert.Equal(t, pc.iceConnectionState.Load(), pc.ICEConnectionState(), "should match") assert.Equal(t, pc.connectionState.Load(), pc.ConnectionState(), "should match") } func TestPeerConnection_AnswerWithoutOffer(t *testing.T) { report := test.CheckRoutines(t) defer report() pc, err := NewPeerConnection(Configuration{}) if err != nil { t.Errorf("New PeerConnection: got error: %v", err) } _, err = pc.CreateAnswer(nil) if !reflect.DeepEqual(&rtcerr.InvalidStateError{Err: ErrNoRemoteDescription}, err) { t.Errorf("CreateAnswer without RemoteDescription: got error: %v", err) } assert.NoError(t, pc.Close()) } func TestPeerConnection_AnswerWithClosedConnection(t *testing.T) { report := test.CheckRoutines(t) defer report() offerPeerConn, answerPeerConn, err := newPair() assert.NoError(t, err) inChecking, inCheckingCancel := context.WithCancel(context.Background()) answerPeerConn.OnICEConnectionStateChange(func(i ICEConnectionState) { if i == ICEConnectionStateChecking { inCheckingCancel() } }) _, err = offerPeerConn.CreateDataChannel("test-channel", nil) assert.NoError(t, err) offer, err := offerPeerConn.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, offerPeerConn.SetLocalDescription(offer)) assert.NoError(t, offerPeerConn.Close()) assert.NoError(t, answerPeerConn.SetRemoteDescription(offer)) <-inChecking.Done() assert.NoError(t, answerPeerConn.Close()) _, err = answerPeerConn.CreateAnswer(nil) assert.Equal(t, err, &rtcerr.InvalidStateError{Err: ErrConnectionClosed}) } func TestPeerConnection_satisfyTypeAndDirection(t *testing.T) { createTransceiver := func(kind RTPCodecType, direction RTPTransceiverDirection) *RTPTransceiver { r := &RTPTransceiver{kind: kind} r.setDirection(direction) return r } for _, test := range []struct { name string kinds []RTPCodecType directions []RTPTransceiverDirection localTransceivers []*RTPTransceiver want []*RTPTransceiver }{ { "Audio and Video Transceivers can not satisfy each other", []RTPCodecType{RTPCodecTypeVideo}, []RTPTransceiverDirection{RTPTransceiverDirectionSendrecv}, []*RTPTransceiver{createTransceiver(RTPCodecTypeAudio, RTPTransceiverDirectionSendrecv)}, []*RTPTransceiver{nil}, }, { "No local Transceivers, every remote should get nil", []RTPCodecType{RTPCodecTypeVideo, RTPCodecTypeAudio, RTPCodecTypeVideo, RTPCodecTypeVideo}, []RTPTransceiverDirection{RTPTransceiverDirectionSendrecv, RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly, RTPTransceiverDirectionInactive}, []*RTPTransceiver{}, []*RTPTransceiver{ nil, nil, nil, nil, }, }, { "Local Recv can satisfy remote SendRecv", []RTPCodecType{RTPCodecTypeVideo}, []RTPTransceiverDirection{RTPTransceiverDirectionSendrecv}, []*RTPTransceiver{createTransceiver(RTPCodecTypeVideo, RTPTransceiverDirectionRecvonly)}, []*RTPTransceiver{createTransceiver(RTPCodecTypeVideo, RTPTransceiverDirectionRecvonly)}, }, { "Don't satisfy a Sendonly with a SendRecv, later SendRecv will be marked as Inactive", []RTPCodecType{RTPCodecTypeVideo, RTPCodecTypeVideo}, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly, RTPTransceiverDirectionSendrecv}, []*RTPTransceiver{ createTransceiver(RTPCodecTypeVideo, RTPTransceiverDirectionSendrecv), createTransceiver(RTPCodecTypeVideo, RTPTransceiverDirectionRecvonly), }, []*RTPTransceiver{ createTransceiver(RTPCodecTypeVideo, RTPTransceiverDirectionRecvonly), createTransceiver(RTPCodecTypeVideo, RTPTransceiverDirectionSendrecv), }, }, } { if len(test.kinds) != len(test.directions) { t.Fatal("Kinds and Directions must be the same length") } got := []*RTPTransceiver{} for i := range test.kinds { res, filteredLocalTransceivers := satisfyTypeAndDirection(test.kinds[i], test.directions[i], test.localTransceivers) got = append(got, res) test.localTransceivers = filteredLocalTransceivers } if !reflect.DeepEqual(got, test.want) { gotStr := "" for _, t := range got { gotStr += fmt.Sprintf("%+v\n", t) } wantStr := "" for _, t := range test.want { wantStr += fmt.Sprintf("%+v\n", t) } t.Errorf("satisfyTypeAndDirection %q: \ngot\n%s \nwant\n%s", test.name, gotStr, wantStr) } } } func TestOneAttrKeyConnectionSetupPerMediaDescriptionInSDP(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = pc.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) _, err = pc.AddTransceiverFromKind(RTPCodecTypeAudio) assert.NoError(t, err) _, err = pc.AddTransceiverFromKind(RTPCodecTypeAudio) assert.NoError(t, err) _, err = pc.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) sdp, err := pc.CreateOffer(nil) assert.NoError(t, err) re := regexp.MustCompile(`a=setup:[[:alpha:]]+`) matches := re.FindAllStringIndex(sdp.SDP, -1) assert.Len(t, matches, 4) assert.NoError(t, pc.Close()) } func TestPeerConnection_IceLite(t *testing.T) { report := test.CheckRoutines(t) defer report() lim := test.TimeOut(time.Second * 10) defer lim.Stop() connectTwoAgents := func(offerIsLite, answerisLite bool) { offerSettingEngine := SettingEngine{} offerSettingEngine.SetLite(offerIsLite) offerPC, err := NewAPI(WithSettingEngine(offerSettingEngine)).NewPeerConnection(Configuration{}) if err != nil { t.Fatal(err) } answerSettingEngine := SettingEngine{} answerSettingEngine.SetLite(answerisLite) answerPC, err := NewAPI(WithSettingEngine(answerSettingEngine)).NewPeerConnection(Configuration{}) if err != nil { t.Fatal(err) } if err = signalPair(offerPC, answerPC); err != nil { t.Fatal(err) } dataChannelOpen := make(chan interface{}) answerPC.OnDataChannel(func(_ *DataChannel) { close(dataChannelOpen) }) <-dataChannelOpen closePairNow(t, offerPC, answerPC) } t.Run("Offerer", func(t *testing.T) { connectTwoAgents(true, false) }) t.Run("Answerer", func(t *testing.T) { connectTwoAgents(false, true) }) t.Run("Both", func(t *testing.T) { connectTwoAgents(true, true) }) } func TestOnICEGatheringStateChange(t *testing.T) { seenGathering := &atomicBool{} seenComplete := &atomicBool{} seenGatheringAndComplete := make(chan interface{}) seenClosed := make(chan interface{}) peerConn, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) var onStateChange func(s ICEGathererState) onStateChange = func(s ICEGathererState) { // Access to ICEGatherer in the callback must not cause dead lock. peerConn.OnICEGatheringStateChange(onStateChange) if state := peerConn.iceGatherer.State(); state != s { t.Errorf("State change callback argument (%s) and State() (%s) result differs", s, state, ) } switch s { // nolint:exhaustive case ICEGathererStateClosed: close(seenClosed) return case ICEGathererStateGathering: if seenComplete.get() { t.Error("Completed before gathering") } seenGathering.set(true) case ICEGathererStateComplete: seenComplete.set(true) } if seenGathering.get() && seenComplete.get() { close(seenGatheringAndComplete) } } peerConn.OnICEGatheringStateChange(onStateChange) offer, err := peerConn.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, peerConn.SetLocalDescription(offer)) select { case <-time.After(time.Second * 10): t.Fatal("Gathering and Complete were never seen") case <-seenClosed: t.Fatal("Closed before PeerConnection Close") case <-seenGatheringAndComplete: } assert.NoError(t, peerConn.Close()) select { case <-time.After(time.Second * 10): t.Fatal("Closed was never seen") case <-seenClosed: } } // Assert Trickle ICE behaviors func TestPeerConnectionTrickle(t *testing.T) { offerPC, answerPC, err := newPair() assert.NoError(t, err) _, err = offerPC.CreateDataChannel("test-channel", nil) assert.NoError(t, err) addOrCacheCandidate := func(pc *PeerConnection, c *ICECandidate, candidateCache []ICECandidateInit) []ICECandidateInit { if c == nil { return candidateCache } if pc.RemoteDescription() == nil { return append(candidateCache, c.ToJSON()) } assert.NoError(t, pc.AddICECandidate(c.ToJSON())) return candidateCache } candidateLock := sync.RWMutex{} var offerCandidateDone, answerCandidateDone bool cachedOfferCandidates := []ICECandidateInit{} offerPC.OnICECandidate(func(c *ICECandidate) { if offerCandidateDone { t.Error("Received OnICECandidate after finishing gathering") } if c == nil { offerCandidateDone = true } candidateLock.Lock() defer candidateLock.Unlock() cachedOfferCandidates = addOrCacheCandidate(answerPC, c, cachedOfferCandidates) }) cachedAnswerCandidates := []ICECandidateInit{} answerPC.OnICECandidate(func(c *ICECandidate) { if answerCandidateDone { t.Error("Received OnICECandidate after finishing gathering") } if c == nil { answerCandidateDone = true } candidateLock.Lock() defer candidateLock.Unlock() cachedAnswerCandidates = addOrCacheCandidate(offerPC, c, cachedAnswerCandidates) }) offerPCConnected, offerPCConnectedCancel := context.WithCancel(context.Background()) offerPC.OnICEConnectionStateChange(func(i ICEConnectionState) { if i == ICEConnectionStateConnected { offerPCConnectedCancel() } }) answerPCConnected, answerPCConnectedCancel := context.WithCancel(context.Background()) answerPC.OnICEConnectionStateChange(func(i ICEConnectionState) { if i == ICEConnectionStateConnected { answerPCConnectedCancel() } }) offer, err := offerPC.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, offerPC.SetLocalDescription(offer)) assert.NoError(t, answerPC.SetRemoteDescription(offer)) answer, err := answerPC.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, answerPC.SetLocalDescription(answer)) assert.NoError(t, offerPC.SetRemoteDescription(answer)) candidateLock.Lock() for _, c := range cachedAnswerCandidates { assert.NoError(t, offerPC.AddICECandidate(c)) } for _, c := range cachedOfferCandidates { assert.NoError(t, answerPC.AddICECandidate(c)) } candidateLock.Unlock() <-answerPCConnected.Done() <-offerPCConnected.Done() closePairNow(t, offerPC, answerPC) } // Issue #1121, assert populateLocalCandidates doesn't mutate func TestPopulateLocalCandidates(t *testing.T) { t.Run("PendingLocalDescription shouldn't add extra mutations", func(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) offer, err := pc.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(pc) assert.NoError(t, pc.SetLocalDescription(offer)) <-offerGatheringComplete assert.Equal(t, pc.PendingLocalDescription(), pc.PendingLocalDescription()) assert.NoError(t, pc.Close()) }) t.Run("end-of-candidates only when gathering is complete", func(t *testing.T) { pc, err := NewAPI().NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = pc.CreateDataChannel("test-channel", nil) assert.NoError(t, err) offer, err := pc.CreateOffer(nil) assert.NoError(t, err) assert.NotContains(t, offer.SDP, "a=candidate") assert.NotContains(t, offer.SDP, "a=end-of-candidates") offerGatheringComplete := GatheringCompletePromise(pc) assert.NoError(t, pc.SetLocalDescription(offer)) <-offerGatheringComplete assert.Contains(t, pc.PendingLocalDescription().SDP, "a=candidate") assert.Contains(t, pc.PendingLocalDescription().SDP, "a=end-of-candidates") assert.NoError(t, pc.Close()) }) } // Assert that two agents that only generate mDNS candidates can connect func TestMulticastDNSCandidates(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() s := SettingEngine{} s.SetICEMulticastDNSMode(ice.MulticastDNSModeQueryAndGather) pcOffer, pcAnswer, err := NewAPI(WithSettingEngine(s)).newPair(Configuration{}) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) onDataChannel, onDataChannelCancel := context.WithCancel(context.Background()) pcAnswer.OnDataChannel(func(d *DataChannel) { onDataChannelCancel() }) <-onDataChannel.Done() closePairNow(t, pcOffer, pcAnswer) } func TestICERestart(t *testing.T) { extractCandidates := func(sdp string) (candidates []string) { sc := bufio.NewScanner(strings.NewReader(sdp)) for sc.Scan() { if strings.HasPrefix(sc.Text(), "a=candidate:") { candidates = append(candidates, sc.Text()) } } return } lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() offerPC, answerPC, err := newPair() assert.NoError(t, err) var connectedWaitGroup sync.WaitGroup connectedWaitGroup.Add(2) offerPC.OnICEConnectionStateChange(func(state ICEConnectionState) { if state == ICEConnectionStateConnected { connectedWaitGroup.Done() } }) answerPC.OnICEConnectionStateChange(func(state ICEConnectionState) { if state == ICEConnectionStateConnected { connectedWaitGroup.Done() } }) // Connect two PeerConnections and block until ICEConnectionStateConnected assert.NoError(t, signalPair(offerPC, answerPC)) connectedWaitGroup.Wait() // Store candidates from first Offer/Answer, compare later to make sure we re-gathered firstOfferCandidates := extractCandidates(offerPC.LocalDescription().SDP) firstAnswerCandidates := extractCandidates(answerPC.LocalDescription().SDP) // Use Trickle ICE for ICE Restart offerPC.OnICECandidate(func(c *ICECandidate) { if c != nil { assert.NoError(t, answerPC.AddICECandidate(c.ToJSON())) } }) answerPC.OnICECandidate(func(c *ICECandidate) { if c != nil { assert.NoError(t, offerPC.AddICECandidate(c.ToJSON())) } }) // Re-signal with ICE Restart, block until ICEConnectionStateConnected connectedWaitGroup.Add(2) offer, err := offerPC.CreateOffer(&OfferOptions{ICERestart: true}) assert.NoError(t, err) assert.NoError(t, offerPC.SetLocalDescription(offer)) assert.NoError(t, answerPC.SetRemoteDescription(offer)) answer, err := answerPC.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, answerPC.SetLocalDescription(answer)) assert.NoError(t, offerPC.SetRemoteDescription(answer)) // Block until we have connected again connectedWaitGroup.Wait() // Compare ICE Candidates across each run, fail if they haven't changed assert.NotEqual(t, firstOfferCandidates, extractCandidates(offerPC.LocalDescription().SDP)) assert.NotEqual(t, firstAnswerCandidates, extractCandidates(answerPC.LocalDescription().SDP)) closePairNow(t, offerPC, answerPC) } // Assert error handling when an Agent is restart func TestICERestart_Error_Handling(t *testing.T) { iceStates := make(chan ICEConnectionState, 100) blockUntilICEState := func(wantedState ICEConnectionState) { stateCount := 0 for i := range iceStates { if i == wantedState { stateCount++ } if stateCount == 2 { return } } } connectWithICERestart := func(offerPeerConnection, answerPeerConnection *PeerConnection) { offer, err := offerPeerConnection.CreateOffer(&OfferOptions{ICERestart: true}) assert.NoError(t, err) assert.NoError(t, offerPeerConnection.SetLocalDescription(offer)) assert.NoError(t, answerPeerConnection.SetRemoteDescription(*offerPeerConnection.LocalDescription())) answer, err := answerPeerConnection.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, answerPeerConnection.SetLocalDescription(answer)) assert.NoError(t, offerPeerConnection.SetRemoteDescription(*answerPeerConnection.LocalDescription())) } lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() offerPeerConnection, answerPeerConnection, wan := createVNetPair(t) pushICEState := func(i ICEConnectionState) { iceStates <- i } offerPeerConnection.OnICEConnectionStateChange(pushICEState) answerPeerConnection.OnICEConnectionStateChange(pushICEState) keepPackets := &atomicBool{} keepPackets.set(true) // Add a filter that monitors the traffic on the router wan.AddChunkFilter(func(c vnet.Chunk) bool { return keepPackets.get() }) const testMessage = "testMessage" d, err := answerPeerConnection.CreateDataChannel("foo", nil) assert.NoError(t, err) dataChannelMessages := make(chan string, 100) d.OnMessage(func(m DataChannelMessage) { dataChannelMessages <- string(m.Data) }) dataChannelAnswerer := make(chan *DataChannel) offerPeerConnection.OnDataChannel(func(d *DataChannel) { d.OnOpen(func() { dataChannelAnswerer <- d }) }) // Connect and Assert we have connected assert.NoError(t, signalPair(offerPeerConnection, answerPeerConnection)) blockUntilICEState(ICEConnectionStateConnected) offerPeerConnection.OnICECandidate(func(c *ICECandidate) { if c != nil { assert.NoError(t, answerPeerConnection.AddICECandidate(c.ToJSON())) } }) answerPeerConnection.OnICECandidate(func(c *ICECandidate) { if c != nil { assert.NoError(t, offerPeerConnection.AddICECandidate(c.ToJSON())) } }) dataChannel := <-dataChannelAnswerer assert.NoError(t, dataChannel.SendText(testMessage)) assert.Equal(t, testMessage, <-dataChannelMessages) // Drop all packets, assert we have disconnected // and send a DataChannel message when disconnected keepPackets.set(false) blockUntilICEState(ICEConnectionStateFailed) assert.NoError(t, dataChannel.SendText(testMessage)) // ICE Restart and assert we have reconnected // block until our DataChannel message is delivered keepPackets.set(true) connectWithICERestart(offerPeerConnection, answerPeerConnection) blockUntilICEState(ICEConnectionStateConnected) assert.Equal(t, testMessage, <-dataChannelMessages) assert.NoError(t, wan.Stop()) closePairNow(t, offerPeerConnection, answerPeerConnection) } type trackRecords struct { mu sync.Mutex trackIDs map[string]struct{} receivedTrackIDs map[string]struct{} } func (r *trackRecords) newTrack() (*TrackLocalStaticRTP, error) { trackID := fmt.Sprintf("pion-track-%d", len(r.trackIDs)) track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, trackID, "pion") r.trackIDs[trackID] = struct{}{} return track, err } func (r *trackRecords) handleTrack(t *TrackRemote, _ *RTPReceiver) { r.mu.Lock() defer r.mu.Unlock() tID := t.ID() if _, exist := r.trackIDs[tID]; exist { r.receivedTrackIDs[tID] = struct{}{} } } func (r *trackRecords) remains() int { r.mu.Lock() defer r.mu.Unlock() return len(r.trackIDs) - len(r.receivedTrackIDs) } // This test assure that all track events emits. func TestPeerConnection_MassiveTracks(t *testing.T) { var ( api = NewAPI() tRecs = &trackRecords{ trackIDs: make(map[string]struct{}), receivedTrackIDs: make(map[string]struct{}), } tracks = []*TrackLocalStaticRTP{} trackCount = 256 pingInterval = 1 * time.Second noiseInterval = 100 * time.Microsecond timeoutDuration = 20 * time.Second rawPkt = []byte{ 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, } samplePkt = &rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: false, ExtensionProfile: 1, Version: 2, SequenceNumber: 27023, Timestamp: 3653407706, CSRC: []uint32{}, }, Payload: rawPkt[20:], } connected = make(chan struct{}) stopped = make(chan struct{}) ) assert.NoError(t, api.mediaEngine.RegisterDefaultCodecs()) offerPC, answerPC, err := api.newPair(Configuration{}) assert.NoError(t, err) // Create massive tracks. for range make([]struct{}, trackCount) { track, err := tRecs.newTrack() assert.NoError(t, err) _, err = offerPC.AddTrack(track) assert.NoError(t, err) tracks = append(tracks, track) } answerPC.OnTrack(tRecs.handleTrack) offerPC.OnICEConnectionStateChange(func(s ICEConnectionState) { if s == ICEConnectionStateConnected { close(connected) } }) // A routine to periodically call GetTransceivers. This action might cause // the deadlock and prevent track event to emit. go func() { for { answerPC.GetTransceivers() time.Sleep(noiseInterval) select { case <-stopped: return default: } } }() assert.NoError(t, signalPair(offerPC, answerPC)) // Send a RTP packets to each track to trigger track event after connected. <-connected time.Sleep(1 * time.Second) for _, track := range tracks { assert.NoError(t, track.WriteRTP(samplePkt)) } // Ping trackRecords to see if any track event not received yet. tooLong := time.After(timeoutDuration) for { remains := tRecs.remains() if remains == 0 { break } t.Log("remain tracks", remains) time.Sleep(pingInterval) select { case <-tooLong: t.Error("unable to receive all track events in time") default: } } close(stopped) closePairNow(t, offerPC, answerPC) } func TestEmptyCandidate(t *testing.T) { testCases := []struct { ICECandidate ICECandidateInit expectError bool }{ {ICECandidateInit{"", nil, nil, nil}, false}, {ICECandidateInit{ "211962667 1 udp 2122194687 10.0.3.1 40864 typ host generation 0", nil, nil, nil, }, false}, {ICECandidateInit{ "1234567", nil, nil, nil, }, true}, } for i, testCase := range testCases { peerConn, err := NewPeerConnection(Configuration{}) if err != nil { t.Errorf("Case %d: got error: %v", i, err) } err = peerConn.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: minimalOffer}) if err != nil { t.Errorf("Case %d: got error: %v", i, err) } if testCase.expectError { assert.Error(t, peerConn.AddICECandidate(testCase.ICECandidate)) } else { assert.NoError(t, peerConn.AddICECandidate(testCase.ICECandidate)) } assert.NoError(t, peerConn.Close()) } } const liteOffer = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 a=msid-semantic: WMS a=ice-lite m=application 47299 DTLS/SCTP 5000 c=IN IP4 192.168.20.129 a=ice-ufrag:1/MvHwjAyVf27aLu a=ice-pwd:3dBU7cFOBl120v33cynDvN1E a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 a=mid:data ` // this test asserts that if an ice-lite offer is received, // pion will take the ICE-CONTROLLING role func TestICELite(t *testing.T) { peerConnection, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) assert.NoError(t, peerConnection.SetRemoteDescription( SessionDescription{SDP: liteOffer, Type: SDPTypeOffer}, )) SDPAnswer, err := peerConnection.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, peerConnection.SetLocalDescription(SDPAnswer)) assert.Equal(t, ICERoleControlling, peerConnection.iceTransport.Role(), "pion did not set state to ICE-CONTROLLED against ice-light offer") assert.NoError(t, peerConnection.Close()) } func TestPeerConnection_TransceiverDirection(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() createTransceiver := func(pc *PeerConnection, dir RTPTransceiverDirection) error { // AddTransceiverFromKind() can't be used with sendonly if dir == RTPTransceiverDirectionSendonly { codecs := pc.api.mediaEngine.getCodecsByKind(RTPCodecTypeVideo) track, err := NewTrackLocalStaticSample(codecs[0].RTPCodecCapability, util.MathRandAlpha(16), util.MathRandAlpha(16)) if err != nil { return err } _, err = pc.AddTransceiverFromTrack(track, []RTPTransceiverInit{ {Direction: dir}, }...) return err } _, err := pc.AddTransceiverFromKind( RTPCodecTypeVideo, RTPTransceiverInit{Direction: dir}, ) return err } for _, test := range []struct { name string offerDirection RTPTransceiverDirection answerStartDirection RTPTransceiverDirection answerFinalDirections []RTPTransceiverDirection }{ { "offer sendrecv answer sendrecv", RTPTransceiverDirectionSendrecv, RTPTransceiverDirectionSendrecv, []RTPTransceiverDirection{RTPTransceiverDirectionSendrecv}, }, { "offer sendonly answer sendrecv", RTPTransceiverDirectionSendonly, RTPTransceiverDirectionSendrecv, []RTPTransceiverDirection{RTPTransceiverDirectionSendrecv}, }, { "offer recvonly answer sendrecv", RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendrecv, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}, }, { "offer sendrecv answer sendonly", RTPTransceiverDirectionSendrecv, RTPTransceiverDirectionSendonly, []RTPTransceiverDirection{RTPTransceiverDirectionSendrecv}, }, { "offer sendonly answer sendonly", RTPTransceiverDirectionSendonly, RTPTransceiverDirectionSendonly, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly, RTPTransceiverDirectionRecvonly}, }, { "offer recvonly answer sendonly", RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}, }, { "offer sendrecv answer recvonly", RTPTransceiverDirectionSendrecv, RTPTransceiverDirectionRecvonly, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}, }, { "offer sendonly answer recvonly", RTPTransceiverDirectionSendonly, RTPTransceiverDirectionRecvonly, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}, }, { "offer recvonly answer recvonly", RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionRecvonly, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly}, }, } { offerDirection := test.offerDirection answerStartDirection := test.answerStartDirection answerFinalDirections := test.answerFinalDirections t.Run(test.name, func(t *testing.T) { pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) err = createTransceiver(pcOffer, offerDirection) assert.NoError(t, err) offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) err = createTransceiver(pcAnswer, answerStartDirection) assert.NoError(t, err) assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) assert.Equal(t, len(answerFinalDirections), len(pcAnswer.GetTransceivers())) for i, tr := range pcAnswer.GetTransceivers() { assert.Equal(t, answerFinalDirections[i], tr.Direction()) } assert.NoError(t, pcOffer.Close()) assert.NoError(t, pcAnswer.Close()) }) } } func TestPeerConnection_SessionID(t *testing.T) { defer test.TimeOut(time.Second * 10).Stop() defer test.CheckRoutines(t)() pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) var offerSessionID uint64 var offerSessionVersion uint64 var answerSessionID uint64 var answerSessionVersion uint64 for i := 0; i < 10; i++ { assert.NoError(t, signalPair(pcOffer, pcAnswer)) offer := pcOffer.LocalDescription().parsed sessionID := offer.Origin.SessionID sessionVersion := offer.Origin.SessionVersion if offerSessionID == 0 { offerSessionID = sessionID offerSessionVersion = sessionVersion } else { if offerSessionID != sessionID { t.Errorf("offer[%v] session id mismatch: expected=%v, got=%v", i, offerSessionID, sessionID) } if offerSessionVersion+1 != sessionVersion { t.Errorf("offer[%v] session version mismatch: expected=%v, got=%v", i, offerSessionVersion+1, sessionVersion) } offerSessionVersion++ } answer := pcAnswer.LocalDescription().parsed sessionID = answer.Origin.SessionID sessionVersion = answer.Origin.SessionVersion if answerSessionID == 0 { answerSessionID = sessionID answerSessionVersion = sessionVersion } else { if answerSessionID != sessionID { t.Errorf("answer[%v] session id mismatch: expected=%v, got=%v", i, answerSessionID, sessionID) } if answerSessionVersion+1 != sessionVersion { t.Errorf("answer[%v] session version mismatch: expected=%v, got=%v", i, answerSessionVersion+1, sessionVersion) } answerSessionVersion++ } } closePairNow(t, pcOffer, pcAnswer) } func TestPeerConnectionNilCallback(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) pc.onSignalingStateChange(SignalingStateStable) pc.OnSignalingStateChange(func(ss SignalingState) { t.Error("OnSignalingStateChange called") }) pc.OnSignalingStateChange(nil) pc.onSignalingStateChange(SignalingStateStable) pc.onConnectionStateChange(PeerConnectionStateNew) pc.OnConnectionStateChange(func(pcs PeerConnectionState) { t.Error("OnConnectionStateChange called") }) pc.OnConnectionStateChange(nil) pc.onConnectionStateChange(PeerConnectionStateNew) pc.onICEConnectionStateChange(ICEConnectionStateNew) pc.OnICEConnectionStateChange(func(ics ICEConnectionState) { t.Error("OnConnectionStateChange called") }) pc.OnICEConnectionStateChange(nil) pc.onICEConnectionStateChange(ICEConnectionStateNew) pc.onNegotiationNeeded() pc.negotiationNeededOp() pc.OnNegotiationNeeded(func() { t.Error("OnNegotiationNeeded called") }) pc.OnNegotiationNeeded(nil) pc.onNegotiationNeeded() pc.negotiationNeededOp() assert.NoError(t, pc.Close()) } func TestTransceiverCreatedByRemoteSdpHasSameCodecOrderAsRemote(t *testing.T) { t.Run("Codec MatchExact", func(t *testing.T) { //nolint:dupl const remoteSdp = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=video 60323 UDP/TLS/RTP/SAVPF 98 94 106 a=ice-ufrag:1/MvHwjAyVf27aLu a=ice-pwd:3dBU7cFOBl120v33cynDvN1E a=ice-options:google-ice a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 a=mid:0 a=rtpmap:98 H264/90000 a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f a=rtpmap:94 VP8/90000 a=rtpmap:106 H264/90000 a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f a=sendonly m=video 60323 UDP/TLS/RTP/SAVPF 108 98 125 a=ice-ufrag:1/MvHwjAyVf27aLu a=ice-pwd:3dBU7cFOBl120v33cynDvN1E a=ice-options:google-ice a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 a=mid:1 a=rtpmap:98 H264/90000 a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f a=rtpmap:108 VP8/90000 a=sendonly a=rtpmap:125 H264/90000 a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f ` m := MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 94, }, RTPCodecTypeVideo)) assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", nil}, PayloadType: 98, }, RTPCodecTypeVideo)) api := NewAPI(WithMediaEngine(&m)) pc, err := api.NewPeerConnection(Configuration{}) assert.NoError(t, err) assert.NoError(t, pc.SetRemoteDescription(SessionDescription{ Type: SDPTypeOffer, SDP: remoteSdp, })) ans, _ := pc.CreateAnswer(nil) assert.NoError(t, pc.SetLocalDescription(ans)) codecOfTr1 := pc.GetTransceivers()[0].getCodecs()[0] codecs := pc.api.mediaEngine.getCodecsByKind(RTPCodecTypeVideo) _, matchType := codecParametersFuzzySearch(codecOfTr1, codecs) assert.Equal(t, codecMatchExact, matchType) codecOfTr2 := pc.GetTransceivers()[1].getCodecs()[0] _, matchType = codecParametersFuzzySearch(codecOfTr2, codecs) assert.Equal(t, codecMatchExact, matchType) assert.EqualValues(t, 94, codecOfTr2.PayloadType) assert.NoError(t, pc.Close()) }) t.Run("Codec PartialExact Only", func(t *testing.T) { //nolint:dupl const remoteSdp = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 m=video 60323 UDP/TLS/RTP/SAVPF 98 106 a=ice-ufrag:1/MvHwjAyVf27aLu a=ice-pwd:3dBU7cFOBl120v33cynDvN1E a=ice-options:google-ice a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 a=mid:0 a=rtpmap:98 H264/90000 a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f a=rtpmap:106 H264/90000 a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032 a=sendonly m=video 60323 UDP/TLS/RTP/SAVPF 125 98 a=ice-ufrag:1/MvHwjAyVf27aLu a=ice-pwd:3dBU7cFOBl120v33cynDvN1E a=ice-options:google-ice a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 a=mid:1 a=rtpmap:125 H264/90000 a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032 a=rtpmap:98 H264/90000 a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f a=sendonly ` m := MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 94, }, RTPCodecTypeVideo)) assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", nil}, PayloadType: 98, }, RTPCodecTypeVideo)) api := NewAPI(WithMediaEngine(&m)) pc, err := api.NewPeerConnection(Configuration{}) assert.NoError(t, err) assert.NoError(t, pc.SetRemoteDescription(SessionDescription{ Type: SDPTypeOffer, SDP: remoteSdp, })) ans, _ := pc.CreateAnswer(nil) assert.NoError(t, pc.SetLocalDescription(ans)) codecOfTr1 := pc.GetTransceivers()[0].getCodecs()[0] codecs := pc.api.mediaEngine.getCodecsByKind(RTPCodecTypeVideo) _, matchType := codecParametersFuzzySearch(codecOfTr1, codecs) assert.Equal(t, codecMatchExact, matchType) codecOfTr2 := pc.GetTransceivers()[1].getCodecs()[0] _, matchType = codecParametersFuzzySearch(codecOfTr2, codecs) assert.Equal(t, codecMatchExact, matchType) // h.264/profile-id=640032 should be remap to 106 as same as transceiver 1 assert.EqualValues(t, 106, codecOfTr2.PayloadType) assert.NoError(t, pc.Close()) }) } // Assert that remote candidates with an unknown transport are ignored and logged. // This allows us to accept SessionDescriptions with proprietary candidates // like `ssltcp`. func TestInvalidCandidateTransport(t *testing.T) { const ( sslTCPCandidate = `candidate:1 1 ssltcp 1 127.0.0.1 443 typ host generation 0` sslTCPOffer = `v=0 o=- 0 2 IN IP4 127.0.0.1 s=- t=0 0 a=msid-semantic: WMS m=application 9 DTLS/SCTP 5000 c=IN IP4 0.0.0.0 a=ice-ufrag:1/MvHwjAyVf27aLu a=ice-pwd:3dBU7cFOBl120v33cynDvN1E a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 a=mid:0 a=` + sslTCPCandidate + "\n" ) peerConnection, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) assert.NoError(t, peerConnection.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: sslTCPOffer})) assert.NoError(t, peerConnection.AddICECandidate(ICECandidateInit{Candidate: sslTCPCandidate})) assert.NoError(t, peerConnection.Close()) } func TestOfferWithInactiveDirection(t *testing.T) { const remoteSDP = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 a=fingerprint:sha-256 F7:BF:B4:42:5B:44:C0:B9:49:70:6D:26:D7:3E:E6:08:B1:5B:25:2E:32:88:50:B6:3C:BE:4E:18:A7:2C:85:7C a=group:BUNDLE 0 1 a=msid-semantic:WMS * m=video 9 UDP/TLS/RTP/SAVPF 97 c=IN IP4 0.0.0.0 a=inactive a=ice-pwd:05d682b2902af03db90d9a9a5f2f8d7f a=ice-ufrag:93cc7e4d a=mid:0 a=rtpmap:97 H264/90000 a=setup:actpass a=ssrc:1455629982 cname:{61fd3093-0326-4b12-8258-86bdc1fe677a} ` peerConnection, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) assert.NoError(t, peerConnection.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: remoteSDP})) assert.Equal(t, RTPTransceiverDirectionInactive, peerConnection.rtpTransceivers[0].direction.Load().(RTPTransceiverDirection)) //nolint:forcetypeassert assert.NoError(t, peerConnection.Close()) } webrtc-3.1.56/peerconnection_js.go000066400000000000000000000653011437620512100171400ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm // Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document. package webrtc import ( "syscall/js" "github.com/pion/ice/v2" "github.com/pion/webrtc/v3/pkg/rtcerr" ) // PeerConnection represents a WebRTC connection that establishes a // peer-to-peer communications with another PeerConnection instance in a // browser, or to another endpoint implementing the required protocols. type PeerConnection struct { // Pointer to the underlying JavaScript RTCPeerConnection object. underlying js.Value // Keep track of handlers/callbacks so we can call Release as required by the // syscall/js API. Initially nil. onSignalingStateChangeHandler *js.Func onDataChannelHandler *js.Func onNegotiationNeededHandler *js.Func onConnectionStateChangeHandler *js.Func onICEConnectionStateChangeHandler *js.Func onICECandidateHandler *js.Func onICEGatheringStateChangeHandler *js.Func // Used by GatheringCompletePromise onGatherCompleteHandler func() // A reference to the associated API state used by this connection api *API } // NewPeerConnection creates a peerconnection. func NewPeerConnection(configuration Configuration) (*PeerConnection, error) { api := NewAPI() return api.NewPeerConnection(configuration) } // NewPeerConnection creates a new PeerConnection with the provided configuration against the received API object func (api *API) NewPeerConnection(configuration Configuration) (_ *PeerConnection, err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() configMap := configurationToValue(configuration) underlying := js.Global().Get("window").Get("RTCPeerConnection").New(configMap) return &PeerConnection{ underlying: underlying, api: api, }, nil } // JSValue returns the underlying PeerConnection func (pc *PeerConnection) JSValue() js.Value { return pc.underlying } // OnSignalingStateChange sets an event handler which is invoked when the // peer connection's signaling state changes func (pc *PeerConnection) OnSignalingStateChange(f func(SignalingState)) { if pc.onSignalingStateChangeHandler != nil { oldHandler := pc.onSignalingStateChangeHandler defer oldHandler.Release() } onSignalingStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { state := newSignalingState(args[0].String()) go f(state) return js.Undefined() }) pc.onSignalingStateChangeHandler = &onSignalingStateChangeHandler pc.underlying.Set("onsignalingstatechange", onSignalingStateChangeHandler) } // OnDataChannel sets an event handler which is invoked when a data // channel message arrives from a remote peer. func (pc *PeerConnection) OnDataChannel(f func(*DataChannel)) { if pc.onDataChannelHandler != nil { oldHandler := pc.onDataChannelHandler defer oldHandler.Release() } onDataChannelHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { // pion/webrtc/projects/15 // This reference to the underlying DataChannel doesn't know // about any other references to the same DataChannel. This might result in // memory leaks where we don't clean up handler functions. Could possibly fix // by keeping a mutex-protected list of all DataChannel references as a // property of this PeerConnection, but at the cost of additional overhead. dataChannel := &DataChannel{ underlying: args[0].Get("channel"), api: pc.api, } go f(dataChannel) return js.Undefined() }) pc.onDataChannelHandler = &onDataChannelHandler pc.underlying.Set("ondatachannel", onDataChannelHandler) } // OnNegotiationNeeded sets an event handler which is invoked when // a change has occurred which requires session negotiation func (pc *PeerConnection) OnNegotiationNeeded(f func()) { if pc.onNegotiationNeededHandler != nil { oldHandler := pc.onNegotiationNeededHandler defer oldHandler.Release() } onNegotiationNeededHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { go f() return js.Undefined() }) pc.onNegotiationNeededHandler = &onNegotiationNeededHandler pc.underlying.Set("onnegotiationneeded", onNegotiationNeededHandler) } // OnICEConnectionStateChange sets an event handler which is called // when an ICE connection state is changed. func (pc *PeerConnection) OnICEConnectionStateChange(f func(ICEConnectionState)) { if pc.onICEConnectionStateChangeHandler != nil { oldHandler := pc.onICEConnectionStateChangeHandler defer oldHandler.Release() } onICEConnectionStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { connectionState := NewICEConnectionState(pc.underlying.Get("iceConnectionState").String()) go f(connectionState) return js.Undefined() }) pc.onICEConnectionStateChangeHandler = &onICEConnectionStateChangeHandler pc.underlying.Set("oniceconnectionstatechange", onICEConnectionStateChangeHandler) } // OnConnectionStateChange sets an event handler which is called // when an PeerConnectionState is changed. func (pc *PeerConnection) OnConnectionStateChange(f func(PeerConnectionState)) { if pc.onConnectionStateChangeHandler != nil { oldHandler := pc.onConnectionStateChangeHandler defer oldHandler.Release() } onConnectionStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { connectionState := newPeerConnectionState(pc.underlying.Get("connectionState").String()) go f(connectionState) return js.Undefined() }) pc.onConnectionStateChangeHandler = &onConnectionStateChangeHandler pc.underlying.Set("onconnectionstatechange", onConnectionStateChangeHandler) } func (pc *PeerConnection) checkConfiguration(configuration Configuration) error { // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2) if pc.ConnectionState() == PeerConnectionStateClosed { return &rtcerr.InvalidStateError{Err: ErrConnectionClosed} } existingConfig := pc.GetConfiguration() // https://www.w3.org/TR/webrtc/#set-the-configuration (step #3) if configuration.PeerIdentity != "" { if configuration.PeerIdentity != existingConfig.PeerIdentity { return &rtcerr.InvalidModificationError{Err: ErrModifyingPeerIdentity} } } // https://github.com/pion/webrtc/issues/513 // https://www.w3.org/TR/webrtc/#set-the-configuration (step #4) // if len(configuration.Certificates) > 0 { // if len(configuration.Certificates) != len(existingConfiguration.Certificates) { // return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates} // } // for i, certificate := range configuration.Certificates { // if !pc.configuration.Certificates[i].Equals(certificate) { // return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates} // } // } // pc.configuration.Certificates = configuration.Certificates // } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #5) if configuration.BundlePolicy != BundlePolicy(Unknown) { if configuration.BundlePolicy != existingConfig.BundlePolicy { return &rtcerr.InvalidModificationError{Err: ErrModifyingBundlePolicy} } } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #6) if configuration.RTCPMuxPolicy != RTCPMuxPolicy(Unknown) { if configuration.RTCPMuxPolicy != existingConfig.RTCPMuxPolicy { return &rtcerr.InvalidModificationError{Err: ErrModifyingRTCPMuxPolicy} } } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #7) if configuration.ICECandidatePoolSize != 0 { if configuration.ICECandidatePoolSize != existingConfig.ICECandidatePoolSize && pc.LocalDescription() != nil { return &rtcerr.InvalidModificationError{Err: ErrModifyingICECandidatePoolSize} } } // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11) if len(configuration.ICEServers) > 0 { // https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3) for _, server := range configuration.ICEServers { if _, err := server.validate(); err != nil { return err } } } return nil } // SetConfiguration updates the configuration of this PeerConnection object. func (pc *PeerConnection) SetConfiguration(configuration Configuration) (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() if err := pc.checkConfiguration(configuration); err != nil { return err } configMap := configurationToValue(configuration) pc.underlying.Call("setConfiguration", configMap) return nil } // GetConfiguration returns a Configuration object representing the current // configuration of this PeerConnection object. The returned object is a // copy and direct mutation on it will not take affect until SetConfiguration // has been called with Configuration passed as its only argument. // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-getconfiguration func (pc *PeerConnection) GetConfiguration() Configuration { return valueToConfiguration(pc.underlying.Call("getConfiguration")) } // CreateOffer starts the PeerConnection and generates the localDescription func (pc *PeerConnection) CreateOffer(options *OfferOptions) (_ SessionDescription, err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() promise := pc.underlying.Call("createOffer", offerOptionsToValue(options)) desc, err := awaitPromise(promise) if err != nil { return SessionDescription{}, err } return *valueToSessionDescription(desc), nil } // CreateAnswer starts the PeerConnection and generates the localDescription func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (_ SessionDescription, err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() promise := pc.underlying.Call("createAnswer", answerOptionsToValue(options)) desc, err := awaitPromise(promise) if err != nil { return SessionDescription{}, err } return *valueToSessionDescription(desc), nil } // SetLocalDescription sets the SessionDescription of the local peer func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() promise := pc.underlying.Call("setLocalDescription", sessionDescriptionToValue(&desc)) _, err = awaitPromise(promise) return err } // LocalDescription returns PendingLocalDescription if it is not null and // otherwise it returns CurrentLocalDescription. This property is used to // determine if setLocalDescription has already been called. // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-localdescription func (pc *PeerConnection) LocalDescription() *SessionDescription { return valueToSessionDescription(pc.underlying.Get("localDescription")) } // SetRemoteDescription sets the SessionDescription of the remote peer func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() promise := pc.underlying.Call("setRemoteDescription", sessionDescriptionToValue(&desc)) _, err = awaitPromise(promise) return err } // RemoteDescription returns PendingRemoteDescription if it is not null and // otherwise it returns CurrentRemoteDescription. This property is used to // determine if setRemoteDescription has already been called. // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-remotedescription func (pc *PeerConnection) RemoteDescription() *SessionDescription { return valueToSessionDescription(pc.underlying.Get("remoteDescription")) } // AddICECandidate accepts an ICE candidate string and adds it // to the existing set of candidates func (pc *PeerConnection) AddICECandidate(candidate ICECandidateInit) (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() promise := pc.underlying.Call("addIceCandidate", iceCandidateInitToValue(candidate)) _, err = awaitPromise(promise) return err } // ICEConnectionState returns the ICE connection state of the // PeerConnection instance. func (pc *PeerConnection) ICEConnectionState() ICEConnectionState { return NewICEConnectionState(pc.underlying.Get("iceConnectionState").String()) } // OnICECandidate sets an event handler which is invoked when a new ICE // candidate is found. func (pc *PeerConnection) OnICECandidate(f func(candidate *ICECandidate)) { if pc.onICECandidateHandler != nil { oldHandler := pc.onICECandidateHandler defer oldHandler.Release() } onICECandidateHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { candidate := valueToICECandidate(args[0].Get("candidate")) if candidate == nil && pc.onGatherCompleteHandler != nil { go pc.onGatherCompleteHandler() } go f(candidate) return js.Undefined() }) pc.onICECandidateHandler = &onICECandidateHandler pc.underlying.Set("onicecandidate", onICECandidateHandler) } // OnICEGatheringStateChange sets an event handler which is invoked when the // ICE candidate gathering state has changed. func (pc *PeerConnection) OnICEGatheringStateChange(f func()) { if pc.onICEGatheringStateChangeHandler != nil { oldHandler := pc.onICEGatheringStateChangeHandler defer oldHandler.Release() } onICEGatheringStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { go f() return js.Undefined() }) pc.onICEGatheringStateChangeHandler = &onICEGatheringStateChangeHandler pc.underlying.Set("onicegatheringstatechange", onICEGatheringStateChangeHandler) } // CreateDataChannel creates a new DataChannel object with the given label // and optional DataChannelInit used to configure properties of the // underlying channel such as data reliability. func (pc *PeerConnection) CreateDataChannel(label string, options *DataChannelInit) (_ *DataChannel, err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() channel := pc.underlying.Call("createDataChannel", label, dataChannelInitToValue(options)) return &DataChannel{ underlying: channel, api: pc.api, }, nil } // SetIdentityProvider is used to configure an identity provider to generate identity assertions func (pc *PeerConnection) SetIdentityProvider(provider string) (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() pc.underlying.Call("setIdentityProvider", provider) return nil } // Close ends the PeerConnection func (pc *PeerConnection) Close() (err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() pc.underlying.Call("close") // Release any handlers as required by the syscall/js API. if pc.onSignalingStateChangeHandler != nil { pc.onSignalingStateChangeHandler.Release() } if pc.onDataChannelHandler != nil { pc.onDataChannelHandler.Release() } if pc.onNegotiationNeededHandler != nil { pc.onNegotiationNeededHandler.Release() } if pc.onConnectionStateChangeHandler != nil { pc.onConnectionStateChangeHandler.Release() } if pc.onICEConnectionStateChangeHandler != nil { pc.onICEConnectionStateChangeHandler.Release() } if pc.onICECandidateHandler != nil { pc.onICECandidateHandler.Release() } if pc.onICEGatheringStateChangeHandler != nil { pc.onICEGatheringStateChangeHandler.Release() } return nil } // CurrentLocalDescription represents the local description that was // successfully negotiated the last time the PeerConnection transitioned // into the stable state plus any local candidates that have been generated // by the ICEAgent since the offer or answer was created. func (pc *PeerConnection) CurrentLocalDescription() *SessionDescription { desc := pc.underlying.Get("currentLocalDescription") return valueToSessionDescription(desc) } // PendingLocalDescription represents a local description that is in the // process of being negotiated plus any local candidates that have been // generated by the ICEAgent since the offer or answer was created. If the // PeerConnection is in the stable state, the value is null. func (pc *PeerConnection) PendingLocalDescription() *SessionDescription { desc := pc.underlying.Get("pendingLocalDescription") return valueToSessionDescription(desc) } // CurrentRemoteDescription represents the last remote description that was // successfully negotiated the last time the PeerConnection transitioned // into the stable state plus any remote candidates that have been supplied // via AddICECandidate() since the offer or answer was created. func (pc *PeerConnection) CurrentRemoteDescription() *SessionDescription { desc := pc.underlying.Get("currentRemoteDescription") return valueToSessionDescription(desc) } // PendingRemoteDescription represents a remote description that is in the // process of being negotiated, complete with any remote candidates that // have been supplied via AddICECandidate() since the offer or answer was // created. If the PeerConnection is in the stable state, the value is // null. func (pc *PeerConnection) PendingRemoteDescription() *SessionDescription { desc := pc.underlying.Get("pendingRemoteDescription") return valueToSessionDescription(desc) } // SignalingState returns the signaling state of the PeerConnection instance. func (pc *PeerConnection) SignalingState() SignalingState { rawState := pc.underlying.Get("signalingState").String() return newSignalingState(rawState) } // ICEGatheringState attribute the ICE gathering state of the PeerConnection // instance. func (pc *PeerConnection) ICEGatheringState() ICEGatheringState { rawState := pc.underlying.Get("iceGatheringState").String() return NewICEGatheringState(rawState) } // ConnectionState attribute the connection state of the PeerConnection // instance. func (pc *PeerConnection) ConnectionState() PeerConnectionState { rawState := pc.underlying.Get("connectionState").String() return newPeerConnectionState(rawState) } func (pc *PeerConnection) setGatherCompleteHandler(handler func()) { pc.onGatherCompleteHandler = handler // If no onIceCandidate handler has been set provide an empty one // otherwise our onGatherCompleteHandler will not be executed if pc.onICECandidateHandler == nil { pc.OnICECandidate(func(i *ICECandidate) {}) } } // AddTransceiverFromKind Create a new RtpTransceiver and adds it to the set of transceivers. func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RTPTransceiverInit) (transceiver *RTPTransceiver, err error) { defer func() { if e := recover(); e != nil { err = recoveryToError(e) } }() if len(init) == 1 { return &RTPTransceiver{ underlying: pc.underlying.Call("addTransceiver", kind.String(), rtpTransceiverInitInitToValue(init[0])), }, err } return &RTPTransceiver{ underlying: pc.underlying.Call("addTransceiver", kind.String()), }, err } // GetTransceivers returns the RtpTransceiver that are currently attached to this PeerConnection func (pc *PeerConnection) GetTransceivers() (transceivers []*RTPTransceiver) { rawTransceivers := pc.underlying.Call("getTransceivers") transceivers = make([]*RTPTransceiver, rawTransceivers.Length()) for i := 0; i < rawTransceivers.Length(); i++ { transceivers[i] = &RTPTransceiver{ underlying: rawTransceivers.Index(i), } } return } // SCTP returns the SCTPTransport for this PeerConnection // // The SCTP transport over which SCTP data is sent and received. If SCTP has not been negotiated, the value is nil. // https://www.w3.org/TR/webrtc/#attributes-15 func (pc *PeerConnection) SCTP() *SCTPTransport { underlying := pc.underlying.Get("sctp") if underlying.IsNull() || underlying.IsUndefined() { return nil } return &SCTPTransport{ underlying: underlying, } } // Converts a Configuration to js.Value so it can be passed // through to the JavaScript WebRTC API. Any zero values are converted to // js.Undefined(), which will result in the default value being used. func configurationToValue(configuration Configuration) js.Value { return js.ValueOf(map[string]interface{}{ "iceServers": iceServersToValue(configuration.ICEServers), "iceTransportPolicy": stringEnumToValueOrUndefined(configuration.ICETransportPolicy.String()), "bundlePolicy": stringEnumToValueOrUndefined(configuration.BundlePolicy.String()), "rtcpMuxPolicy": stringEnumToValueOrUndefined(configuration.RTCPMuxPolicy.String()), "peerIdentity": stringToValueOrUndefined(configuration.PeerIdentity), "iceCandidatePoolSize": uint8ToValueOrUndefined(configuration.ICECandidatePoolSize), // Note: Certificates are not currently supported. // "certificates": configuration.Certificates, }) } func iceServersToValue(iceServers []ICEServer) js.Value { if len(iceServers) == 0 { return js.Undefined() } maps := make([]interface{}, len(iceServers)) for i, server := range iceServers { maps[i] = iceServerToValue(server) } return js.ValueOf(maps) } func oauthCredentialToValue(o OAuthCredential) js.Value { out := map[string]interface{}{ "MACKey": o.MACKey, "AccessToken": o.AccessToken, } return js.ValueOf(out) } func iceServerToValue(server ICEServer) js.Value { out := map[string]interface{}{ "urls": stringsToValue(server.URLs), // required } if server.Username != "" { out["username"] = stringToValueOrUndefined(server.Username) } if server.Credential != nil { switch t := server.Credential.(type) { case string: out["credential"] = stringToValueOrUndefined(t) case OAuthCredential: out["credential"] = oauthCredentialToValue(t) } } out["credentialType"] = stringEnumToValueOrUndefined(server.CredentialType.String()) return js.ValueOf(out) } func valueToConfiguration(configValue js.Value) Configuration { if configValue.IsNull() || configValue.IsUndefined() { return Configuration{} } return Configuration{ ICEServers: valueToICEServers(configValue.Get("iceServers")), ICETransportPolicy: NewICETransportPolicy(valueToStringOrZero(configValue.Get("iceTransportPolicy"))), BundlePolicy: newBundlePolicy(valueToStringOrZero(configValue.Get("bundlePolicy"))), RTCPMuxPolicy: newRTCPMuxPolicy(valueToStringOrZero(configValue.Get("rtcpMuxPolicy"))), PeerIdentity: valueToStringOrZero(configValue.Get("peerIdentity")), ICECandidatePoolSize: valueToUint8OrZero(configValue.Get("iceCandidatePoolSize")), // Note: Certificates are not supported. // Certificates []Certificate } } func valueToICEServers(iceServersValue js.Value) []ICEServer { if iceServersValue.IsNull() || iceServersValue.IsUndefined() { return nil } iceServers := make([]ICEServer, iceServersValue.Length()) for i := 0; i < iceServersValue.Length(); i++ { iceServers[i] = valueToICEServer(iceServersValue.Index(i)) } return iceServers } func valueToICECredential(iceCredentialValue js.Value) interface{} { if iceCredentialValue.IsNull() || iceCredentialValue.IsUndefined() { return nil } if iceCredentialValue.Type() == js.TypeString { return iceCredentialValue.String() } if iceCredentialValue.Type() == js.TypeObject { return OAuthCredential{ MACKey: iceCredentialValue.Get("MACKey").String(), AccessToken: iceCredentialValue.Get("AccessToken").String(), } } return nil } func valueToICEServer(iceServerValue js.Value) ICEServer { tpe, err := newICECredentialType(valueToStringOrZero(iceServerValue.Get("credentialType"))) if err != nil { tpe = ICECredentialTypePassword } s := ICEServer{ URLs: valueToStrings(iceServerValue.Get("urls")), // required Username: valueToStringOrZero(iceServerValue.Get("username")), // Note: Credential and CredentialType are not currently supported. Credential: valueToICECredential(iceServerValue.Get("credential")), CredentialType: tpe, } return s } func valueToICECandidate(val js.Value) *ICECandidate { if val.IsNull() || val.IsUndefined() { return nil } if val.Get("protocol").IsUndefined() && !val.Get("candidate").IsUndefined() { // Missing some fields, assume it's Firefox and parse SDP candidate. c, err := ice.UnmarshalCandidate(val.Get("candidate").String()) if err != nil { return nil } iceCandidate, err := newICECandidateFromICE(c) if err != nil { return nil } return &iceCandidate } protocol, _ := NewICEProtocol(val.Get("protocol").String()) candidateType, _ := NewICECandidateType(val.Get("type").String()) return &ICECandidate{ Foundation: val.Get("foundation").String(), Priority: valueToUint32OrZero(val.Get("priority")), Address: val.Get("address").String(), Protocol: protocol, Port: valueToUint16OrZero(val.Get("port")), Typ: candidateType, Component: stringToComponentIDOrZero(val.Get("component").String()), RelatedAddress: val.Get("relatedAddress").String(), RelatedPort: valueToUint16OrZero(val.Get("relatedPort")), } } func stringToComponentIDOrZero(val string) uint16 { // See: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceComponent switch val { case "rtp": return 1 case "rtcp": return 2 } return 0 } func sessionDescriptionToValue(desc *SessionDescription) js.Value { if desc == nil { return js.Undefined() } return js.ValueOf(map[string]interface{}{ "type": desc.Type.String(), "sdp": desc.SDP, }) } func valueToSessionDescription(descValue js.Value) *SessionDescription { if descValue.IsNull() || descValue.IsUndefined() { return nil } return &SessionDescription{ Type: NewSDPType(descValue.Get("type").String()), SDP: descValue.Get("sdp").String(), } } func offerOptionsToValue(offerOptions *OfferOptions) js.Value { if offerOptions == nil { return js.Undefined() } return js.ValueOf(map[string]interface{}{ "iceRestart": offerOptions.ICERestart, "voiceActivityDetection": offerOptions.VoiceActivityDetection, }) } func answerOptionsToValue(answerOptions *AnswerOptions) js.Value { if answerOptions == nil { return js.Undefined() } return js.ValueOf(map[string]interface{}{ "voiceActivityDetection": answerOptions.VoiceActivityDetection, }) } func iceCandidateInitToValue(candidate ICECandidateInit) js.Value { return js.ValueOf(map[string]interface{}{ "candidate": candidate.Candidate, "sdpMid": stringPointerToValue(candidate.SDPMid), "sdpMLineIndex": uint16PointerToValue(candidate.SDPMLineIndex), "usernameFragment": stringPointerToValue(candidate.UsernameFragment), }) } func dataChannelInitToValue(options *DataChannelInit) js.Value { if options == nil { return js.Undefined() } maxPacketLifeTime := uint16PointerToValue(options.MaxPacketLifeTime) return js.ValueOf(map[string]interface{}{ "ordered": boolPointerToValue(options.Ordered), "maxPacketLifeTime": maxPacketLifeTime, // See https://bugs.chromium.org/p/chromium/issues/detail?id=696681 // Chrome calls this "maxRetransmitTime" "maxRetransmitTime": maxPacketLifeTime, "maxRetransmits": uint16PointerToValue(options.MaxRetransmits), "protocol": stringPointerToValue(options.Protocol), "negotiated": boolPointerToValue(options.Negotiated), "id": uint16PointerToValue(options.ID), }) } func rtpTransceiverInitInitToValue(init RTPTransceiverInit) js.Value { return js.ValueOf(map[string]interface{}{ "direction": init.Direction.String(), }) } webrtc-3.1.56/peerconnection_js_test.go000066400000000000000000000060751437620512100202020ustar00rootroot00000000000000package webrtc import ( "encoding/json" "syscall/js" "testing" "github.com/stretchr/testify/assert" ) func TestValueToICECandidate(t *testing.T) { testCases := []struct { jsonCandidate string expect ICECandidate }{ { // Firefox-style ICECandidateInit: `{"candidate":"1966762133 1 udp 2122260222 192.168.20.128 47298 typ srflx raddr 203.0.113.1 rport 5000"}`, ICECandidate{ Foundation: "1966762133", Priority: 2122260222, Address: "192.168.20.128", Protocol: ICEProtocolUDP, Port: 47298, Typ: ICECandidateTypeSrflx, Component: 1, RelatedAddress: "203.0.113.1", RelatedPort: 5000, }, }, { // Chrome/Webkit-style ICECandidate: `{"foundation":"1966762134", "component":"rtp", "protocol":"udp", "priority":2122260223, "address":"192.168.20.129", "port":47299, "type":"host", "relatedAddress":null}`, ICECandidate{ Foundation: "1966762134", Priority: 2122260223, Address: "192.168.20.129", Protocol: ICEProtocolUDP, Port: 47299, Typ: ICECandidateTypeHost, Component: 1, RelatedAddress: "", RelatedPort: 0, }, }, { // Both are present, Chrome/Webkit-style takes precedent: `{"candidate":"1966762133 1 udp 2122260222 192.168.20.128 47298 typ srflx raddr 203.0.113.1 rport 5000", "foundation":"1966762134", "component":"rtp", "protocol":"udp", "priority":2122260223, "address":"192.168.20.129", "port":47299, "type":"host", "relatedAddress":null}`, ICECandidate{ Foundation: "1966762134", Priority: 2122260223, Address: "192.168.20.129", Protocol: ICEProtocolUDP, Port: 47299, Typ: ICECandidateTypeHost, Component: 1, RelatedAddress: "", RelatedPort: 0, }, }, } for i, testCase := range testCases { v := map[string]interface{}{} err := json.Unmarshal([]byte(testCase.jsonCandidate), &v) if err != nil { t.Errorf("Case %d: bad test, got error: %v", i, err) } val := *valueToICECandidate(js.ValueOf(v)) val.statsID = "" assert.Equal(t, testCase.expect, val) } } func TestValueToICEServer(t *testing.T) { testCases := []ICEServer{ { URLs: []string{"turn:192.158.29.39?transport=udp"}, Username: "unittest", Credential: "placeholder", CredentialType: ICECredentialTypePassword, }, { URLs: []string{"turn:[2001:db8:1234:5678::1]?transport=udp"}, Username: "unittest", Credential: "placeholder", CredentialType: ICECredentialTypePassword, }, { URLs: []string{"turn:192.158.29.39?transport=udp"}, Username: "unittest", Credential: OAuthCredential{ MACKey: "WmtzanB3ZW9peFhtdm42NzUzNG0=", AccessToken: "AAwg3kPHWPfvk9bDFL936wYvkoctMADzQ5VhNDgeMR3+ZlZ35byg972fW8QjpEl7bx91YLBPFsIhsxloWcXPhA==", }, CredentialType: ICECredentialTypeOauth, }, } for _, testCase := range testCases { v := iceServerToValue(testCase) s := valueToICEServer(v) assert.Equal(t, testCase, s) } } webrtc-3.1.56/peerconnection_media_test.go000066400000000000000000001062721437620512100206450ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "bufio" "bytes" "context" "errors" "fmt" "io" "strings" "sync" "testing" "time" "github.com/pion/logging" "github.com/pion/randutil" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/pion/sdp/v3" "github.com/pion/transport/v2/test" "github.com/pion/webrtc/v3/pkg/media" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( errIncomingTrackIDInvalid = errors.New("incoming Track ID is invalid") errIncomingTrackLabelInvalid = errors.New("incoming Track Label is invalid") errNoTransceiverwithMid = errors.New("no transceiver with mid") ) func registerSimulcastHeaderExtensions(m *MediaEngine, codecType RTPCodecType) { for _, extension := range []string{ sdp.SDESMidURI, sdp.SDESRTPStreamIDURI, sdesRepairRTPStreamIDURI, } { if err := m.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: extension}, codecType); err != nil { panic(err) } } } /* Integration test for bi-directional peers This asserts we can send RTP and RTCP both ways, and blocks until each side gets something (and asserts payload contents) */ // nolint: gocyclo func TestPeerConnection_Media_Sample(t *testing.T) { const ( expectedTrackID = "video" expectedStreamID = "pion" ) lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } awaitRTPRecv := make(chan bool) awaitRTPRecvClosed := make(chan bool) awaitRTPSend := make(chan bool) awaitRTCPSenderRecv := make(chan bool) awaitRTCPSenderSend := make(chan error) awaitRTCPReceiverRecv := make(chan error) awaitRTCPReceiverSend := make(chan error) trackMetadataValid := make(chan error) pcAnswer.OnTrack(func(track *TrackRemote, receiver *RTPReceiver) { if track.ID() != expectedTrackID { trackMetadataValid <- fmt.Errorf("%w: expected(%s) actual(%s)", errIncomingTrackIDInvalid, expectedTrackID, track.ID()) return } if track.StreamID() != expectedStreamID { trackMetadataValid <- fmt.Errorf("%w: expected(%s) actual(%s)", errIncomingTrackLabelInvalid, expectedStreamID, track.StreamID()) return } close(trackMetadataValid) go func() { for { time.Sleep(time.Millisecond * 100) if routineErr := pcAnswer.WriteRTCP([]rtcp.Packet{&rtcp.RapidResynchronizationRequest{SenderSSRC: uint32(track.SSRC()), MediaSSRC: uint32(track.SSRC())}}); routineErr != nil { awaitRTCPReceiverSend <- routineErr return } select { case <-awaitRTCPSenderRecv: close(awaitRTCPReceiverSend) return default: } } }() go func() { _, _, routineErr := receiver.Read(make([]byte, 1400)) if routineErr != nil { awaitRTCPReceiverRecv <- routineErr } else { close(awaitRTCPReceiverRecv) } }() haveClosedAwaitRTPRecv := false for { p, _, routineErr := track.ReadRTP() if routineErr != nil { close(awaitRTPRecvClosed) return } else if bytes.Equal(p.Payload, []byte{0x10, 0x00}) && !haveClosedAwaitRTPRecv { haveClosedAwaitRTPRecv = true close(awaitRTPRecv) } } }) vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, expectedTrackID, expectedStreamID) if err != nil { t.Fatal(err) } sender, err := pcOffer.AddTrack(vp8Track) if err != nil { t.Fatal(err) } go func() { for { time.Sleep(time.Millisecond * 100) if pcOffer.ICEConnectionState() != ICEConnectionStateConnected { continue } if routineErr := vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}); routineErr != nil { fmt.Println(routineErr) } select { case <-awaitRTPRecv: close(awaitRTPSend) return default: } } }() go func() { for { time.Sleep(time.Millisecond * 100) if routineErr := pcOffer.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{SenderSSRC: uint32(sender.trackEncodings[0].ssrc), MediaSSRC: uint32(sender.trackEncodings[0].ssrc)}}); routineErr != nil { awaitRTCPSenderSend <- routineErr } select { case <-awaitRTCPReceiverRecv: close(awaitRTCPSenderSend) return default: } } }() go func() { if _, _, routineErr := sender.Read(make([]byte, 1400)); routineErr == nil { close(awaitRTCPSenderRecv) } }() assert.NoError(t, signalPair(pcOffer, pcAnswer)) err, ok := <-trackMetadataValid if ok { t.Fatal(err) } <-awaitRTPRecv <-awaitRTPSend <-awaitRTCPSenderRecv if err, ok = <-awaitRTCPSenderSend; ok { t.Fatal(err) } <-awaitRTCPReceiverRecv if err, ok = <-awaitRTCPReceiverSend; ok { t.Fatal(err) } closePairNow(t, pcOffer, pcAnswer) <-awaitRTPRecvClosed } /* PeerConnection should be able to be torn down at anytime This test adds an input track and asserts * OnTrack doesn't fire since no video packets will arrive * No goroutine leaks * No deadlocks on shutdown */ func TestPeerConnection_Media_Shutdown(t *testing.T) { iceCompleteAnswer := make(chan struct{}) iceCompleteOffer := make(chan struct{}) lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } _, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) if err != nil { t.Fatal(err) } _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) if err != nil { t.Fatal(err) } opusTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "audio", "pion1") if err != nil { t.Fatal(err) } vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2") if err != nil { t.Fatal(err) } if _, err = pcOffer.AddTrack(opusTrack); err != nil { t.Fatal(err) } else if _, err = pcAnswer.AddTrack(vp8Track); err != nil { t.Fatal(err) } var onTrackFiredLock sync.Mutex onTrackFired := false pcAnswer.OnTrack(func(track *TrackRemote, receiver *RTPReceiver) { onTrackFiredLock.Lock() defer onTrackFiredLock.Unlock() onTrackFired = true }) pcAnswer.OnICEConnectionStateChange(func(iceState ICEConnectionState) { if iceState == ICEConnectionStateConnected { close(iceCompleteAnswer) } }) pcOffer.OnICEConnectionStateChange(func(iceState ICEConnectionState) { if iceState == ICEConnectionStateConnected { close(iceCompleteOffer) } }) err = signalPair(pcOffer, pcAnswer) if err != nil { t.Fatal(err) } <-iceCompleteAnswer <-iceCompleteOffer // Each PeerConnection should have one sender, one receiver and one transceiver for _, pc := range []*PeerConnection{pcOffer, pcAnswer} { senders := pc.GetSenders() if len(senders) != 1 { t.Errorf("Each PeerConnection should have one RTPSender, we have %d", len(senders)) } receivers := pc.GetReceivers() if len(receivers) != 2 { t.Errorf("Each PeerConnection should have two RTPReceivers, we have %d", len(receivers)) } transceivers := pc.GetTransceivers() if len(transceivers) != 2 { t.Errorf("Each PeerConnection should have two RTPTransceivers, we have %d", len(transceivers)) } } closePairNow(t, pcOffer, pcAnswer) onTrackFiredLock.Lock() if onTrackFired { t.Fatalf("PeerConnection OnTrack fired even though we got no packets") } onTrackFiredLock.Unlock() } /* Integration test for behavior around media and disconnected peers * Sending RTP and RTCP to a disconnected Peer shouldn't return an error */ func TestPeerConnection_Media_Disconnected(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() s := SettingEngine{} s.SetICETimeouts(time.Second/2, time.Second/2, time.Second/8) m := &MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) pcOffer, pcAnswer, err := NewAPI(WithSettingEngine(s), WithMediaEngine(m)).newPair(Configuration{}) if err != nil { t.Fatal(err) } vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2") if err != nil { t.Fatal(err) } vp8Sender, err := pcOffer.AddTrack(vp8Track) if err != nil { t.Fatal(err) } haveDisconnected := make(chan error) pcOffer.OnICEConnectionStateChange(func(iceState ICEConnectionState) { if iceState == ICEConnectionStateDisconnected { close(haveDisconnected) } else if iceState == ICEConnectionStateConnected { // Assert that DTLS is done by pull remote certificate, don't tear down the PC early for { if len(vp8Sender.Transport().GetRemoteCertificate()) != 0 { if pcAnswer.sctpTransport.association() != nil { break } } time.Sleep(time.Second) } if pcCloseErr := pcAnswer.Close(); pcCloseErr != nil { haveDisconnected <- pcCloseErr } } }) err = signalPair(pcOffer, pcAnswer) if err != nil { t.Fatal(err) } err, ok := <-haveDisconnected if ok { t.Fatal(err) } for i := 0; i <= 5; i++ { if rtpErr := vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}); rtpErr != nil { t.Fatal(rtpErr) } else if rtcpErr := pcOffer.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: 0}}); rtcpErr != nil { t.Fatal(rtcpErr) } } assert.NoError(t, pcOffer.Close()) } type undeclaredSsrcLogger struct{ unhandledSimulcastError chan struct{} } func (u *undeclaredSsrcLogger) Trace(msg string) {} func (u *undeclaredSsrcLogger) Tracef(format string, args ...interface{}) {} func (u *undeclaredSsrcLogger) Debug(msg string) {} func (u *undeclaredSsrcLogger) Debugf(format string, args ...interface{}) {} func (u *undeclaredSsrcLogger) Info(msg string) {} func (u *undeclaredSsrcLogger) Infof(format string, args ...interface{}) {} func (u *undeclaredSsrcLogger) Warn(msg string) {} func (u *undeclaredSsrcLogger) Warnf(format string, args ...interface{}) {} func (u *undeclaredSsrcLogger) Error(msg string) {} func (u *undeclaredSsrcLogger) Errorf(format string, args ...interface{}) { if format == incomingUnhandledRTPSsrc { close(u.unhandledSimulcastError) } } type undeclaredSsrcLoggerFactory struct{ unhandledSimulcastError chan struct{} } func (u *undeclaredSsrcLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { return &undeclaredSsrcLogger{u.unhandledSimulcastError} } // Filter SSRC lines func filterSsrc(offer string) (filteredSDP string) { scanner := bufio.NewScanner(strings.NewReader(offer)) for scanner.Scan() { l := scanner.Text() if strings.HasPrefix(l, "a=ssrc") { continue } filteredSDP += l + "\n" } return } // If a SessionDescription has a single media section and no SSRC // assume that it is meant to handle all RTP packets func TestUndeclaredSSRC(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() t.Run("No SSRC", func(t *testing.T) { pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) vp8Writer, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2") assert.NoError(t, err) _, err = pcOffer.AddTrack(vp8Writer) assert.NoError(t, err) onTrackFired := make(chan struct{}) pcAnswer.OnTrack(func(trackRemote *TrackRemote, r *RTPReceiver) { assert.Equal(t, trackRemote.StreamID(), vp8Writer.StreamID()) assert.Equal(t, trackRemote.ID(), vp8Writer.ID()) close(onTrackFired) }) offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) <-offerGatheringComplete offer.SDP = filterSsrc(pcOffer.LocalDescription().SDP) assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) <-answerGatheringComplete assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) sendVideoUntilDone(onTrackFired, t, []*TrackLocalStaticSample{vp8Writer}) closePairNow(t, pcOffer, pcAnswer) }) t.Run("Has RID", func(t *testing.T) { unhandledSimulcastError := make(chan struct{}) m := &MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) pcOffer, pcAnswer, err := NewAPI(WithSettingEngine(SettingEngine{ LoggerFactory: &undeclaredSsrcLoggerFactory{unhandledSimulcastError}, }), WithMediaEngine(m)).newPair(Configuration{}) assert.NoError(t, err) vp8Writer, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2") assert.NoError(t, err) _, err = pcOffer.AddTrack(vp8Writer) assert.NoError(t, err) offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) <-offerGatheringComplete // Append RID to end of SessionDescription. Will not be considered unhandled anymore offer.SDP = filterSsrc(pcOffer.LocalDescription().SDP) + "a=" + sdpAttributeRid + "\r\n" assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) <-answerGatheringComplete assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) sendVideoUntilDone(unhandledSimulcastError, t, []*TrackLocalStaticSample{vp8Writer}) closePairNow(t, pcOffer, pcAnswer) }) } func TestAddTransceiverFromTrackSendOnly(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pc, err := NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } track, err := NewTrackLocalStaticSample( RTPCodecCapability{MimeType: "audio/Opus"}, "track-id", "stream-id", ) if err != nil { t.Error(err.Error()) } transceiver, err := pc.AddTransceiverFromTrack(track, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendonly, }) if err != nil { t.Error(err.Error()) } if transceiver.Receiver() != nil { t.Errorf("Transceiver shouldn't have a receiver") } if transceiver.Sender() == nil { t.Errorf("Transceiver should have a sender") } if len(pc.GetTransceivers()) != 1 { t.Errorf("PeerConnection should have one transceiver but has %d", len(pc.GetTransceivers())) } if len(pc.GetSenders()) != 1 { t.Errorf("PeerConnection should have one sender but has %d", len(pc.GetSenders())) } offer, err := pc.CreateOffer(nil) if err != nil { t.Error(err.Error()) } if !offerMediaHasDirection(offer, RTPCodecTypeAudio, RTPTransceiverDirectionSendonly) { t.Errorf("Direction on SDP is not %s", RTPTransceiverDirectionSendonly) } assert.NoError(t, pc.Close()) } func TestAddTransceiverFromTrackSendRecv(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pc, err := NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } track, err := NewTrackLocalStaticSample( RTPCodecCapability{MimeType: "audio/Opus"}, "track-id", "stream-id", ) if err != nil { t.Error(err.Error()) } transceiver, err := pc.AddTransceiverFromTrack(track, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendrecv, }) if err != nil { t.Error(err.Error()) } if transceiver.Receiver() == nil { t.Errorf("Transceiver should have a receiver") } if transceiver.Sender() == nil { t.Errorf("Transceiver should have a sender") } if len(pc.GetTransceivers()) != 1 { t.Errorf("PeerConnection should have one transceiver but has %d", len(pc.GetTransceivers())) } offer, err := pc.CreateOffer(nil) if err != nil { t.Error(err.Error()) } if !offerMediaHasDirection(offer, RTPCodecTypeAudio, RTPTransceiverDirectionSendrecv) { t.Errorf("Direction on SDP is not %s", RTPTransceiverDirectionSendrecv) } assert.NoError(t, pc.Close()) } func TestAddTransceiverAddTrack_Reuse(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) tr, err := pc.AddTransceiverFromKind( RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}, ) assert.NoError(t, err) assert.Equal(t, []*RTPTransceiver{tr}, pc.GetTransceivers()) addTrack := func() (TrackLocal, *RTPSender) { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) sender, err := pc.AddTrack(track) assert.NoError(t, err) return track, sender } track1, sender1 := addTrack() assert.Equal(t, 1, len(pc.GetTransceivers())) assert.Equal(t, sender1, tr.Sender()) assert.Equal(t, track1, tr.Sender().Track()) require.NoError(t, pc.RemoveTrack(sender1)) track2, _ := addTrack() assert.Equal(t, 1, len(pc.GetTransceivers())) assert.Equal(t, track2, tr.Sender().Track()) addTrack() assert.Equal(t, 2, len(pc.GetTransceivers())) assert.NoError(t, pc.Close()) } func TestAddTransceiverAddTrack_NewRTPSender_Error(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = pc.AddTransceiverFromKind( RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}, ) assert.NoError(t, err) dtlsTransport := pc.dtlsTransport pc.dtlsTransport = nil track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) _, err = pc.AddTrack(track) assert.Error(t, err, "DTLSTransport must not be nil") assert.Equal(t, 1, len(pc.GetTransceivers())) pc.dtlsTransport = dtlsTransport assert.NoError(t, pc.Close()) } func TestRtpSenderReceiver_ReadClose_Error(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) tr, err := pc.AddTransceiverFromKind( RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionSendrecv}, ) assert.NoError(t, err) sender, receiver := tr.Sender(), tr.Receiver() assert.NoError(t, sender.Stop()) _, _, err = sender.Read(make([]byte, 0, 1400)) assert.ErrorIs(t, err, io.ErrClosedPipe) assert.NoError(t, receiver.Stop()) _, _, err = receiver.Read(make([]byte, 0, 1400)) assert.ErrorIs(t, err, io.ErrClosedPipe) assert.NoError(t, pc.Close()) } // nolint: dupl func TestAddTransceiverFromKind(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pc, err := NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } transceiver, err := pc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ Direction: RTPTransceiverDirectionRecvonly, }) if err != nil { t.Error(err.Error()) } if transceiver.Receiver() == nil { t.Errorf("Transceiver should have a receiver") } if transceiver.Sender() != nil { t.Errorf("Transceiver shouldn't have a sender") } offer, err := pc.CreateOffer(nil) if err != nil { t.Error(err.Error()) } if !offerMediaHasDirection(offer, RTPCodecTypeVideo, RTPTransceiverDirectionRecvonly) { t.Errorf("Direction on SDP is not %s", RTPTransceiverDirectionRecvonly) } assert.NoError(t, pc.Close()) } func TestAddTransceiverFromTrackFailsRecvOnly(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pc, err := NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } track, err := NewTrackLocalStaticSample( RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "track-id", "track-label", ) if err != nil { t.Error(err.Error()) } transceiver, err := pc.AddTransceiverFromTrack(track, RTPTransceiverInit{ Direction: RTPTransceiverDirectionRecvonly, }) if transceiver != nil { t.Error("AddTransceiverFromTrack shouldn't succeed with Direction RTPTransceiverDirectionRecvonly") } assert.NotNil(t, err) assert.NoError(t, pc.Close()) } func TestPlanBMediaExchange(t *testing.T) { runTest := func(trackCount int, t *testing.T) { addSingleTrack := func(p *PeerConnection) *TrackLocalStaticSample { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32())) assert.NoError(t, err) _, err = p.AddTrack(track) assert.NoError(t, err) return track } pcOffer, err := NewPeerConnection(Configuration{SDPSemantics: SDPSemanticsPlanB}) assert.NoError(t, err) pcAnswer, err := NewPeerConnection(Configuration{SDPSemantics: SDPSemanticsPlanB}) assert.NoError(t, err) var onTrackWaitGroup sync.WaitGroup onTrackWaitGroup.Add(trackCount) pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { onTrackWaitGroup.Done() }) done := make(chan struct{}) go func() { onTrackWaitGroup.Wait() close(done) }() _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) outboundTracks := []*TrackLocalStaticSample{} for i := 0; i < trackCount; i++ { outboundTracks = append(outboundTracks, addSingleTrack(pcOffer)) } assert.NoError(t, signalPair(pcOffer, pcAnswer)) func() { for { select { case <-time.After(20 * time.Millisecond): for _, track := range outboundTracks { assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second})) } case <-done: return } } }() closePairNow(t, pcOffer, pcAnswer) } lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() t.Run("Single Track", func(t *testing.T) { runTest(1, t) }) t.Run("Multi Track", func(t *testing.T) { runTest(2, t) }) } // TestPeerConnection_Start_Only_Negotiated_Senders tests that only // the current negotiated transceivers senders provided in an // offer/answer are started func TestPeerConnection_Start_Only_Negotiated_Senders(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) defer func() { assert.NoError(t, pcOffer.Close()) }() pcAnswer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) defer func() { assert.NoError(t, pcAnswer.Close()) }() track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1") require.NoError(t, err) sender1, err := pcOffer.AddTrack(track1) require.NoError(t, err) offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) <-offerGatheringComplete assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription())) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) <-answerGatheringComplete // Add a new track between providing the offer and applying the answer track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2") require.NoError(t, err) sender2, err := pcOffer.AddTrack(track2) require.NoError(t, err) // apply answer so we'll test generateMatchedSDP assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) // Wait for senders to be started by startTransports spawned goroutine pcOffer.ops.Done() // sender1 should be started but sender2 should not be started assert.True(t, sender1.hasSent(), "sender1 is not started but should be started") assert.False(t, sender2.hasSent(), "sender2 is started but should not be started") } // TestPeerConnection_Start_Right_Receiver tests that the right // receiver (the receiver which transceiver has the same media section as the track) // is started for the specified track func TestPeerConnection_Start_Right_Receiver(t *testing.T) { isTransceiverReceiverStarted := func(pc *PeerConnection, mid string) (bool, error) { for _, transceiver := range pc.GetTransceivers() { if transceiver.Mid() != mid { continue } return transceiver.Receiver() != nil && transceiver.Receiver().haveReceived(), nil } return false, fmt.Errorf("%w: %q", errNoTransceiverwithMid, mid) } lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() require.NoError(t, err) _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) assert.NoError(t, err) track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1") require.NoError(t, err) sender1, err := pcOffer.AddTrack(track1) require.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) pcOffer.ops.Done() pcAnswer.ops.Done() // transceiver with mid 0 should be started started, err := isTransceiverReceiverStarted(pcAnswer, "0") assert.NoError(t, err) assert.True(t, started, "transceiver with mid 0 should be started") // Remove track assert.NoError(t, pcOffer.RemoveTrack(sender1)) assert.NoError(t, signalPair(pcOffer, pcAnswer)) pcOffer.ops.Done() pcAnswer.ops.Done() // transceiver with mid 0 should not be started started, err = isTransceiverReceiverStarted(pcAnswer, "0") assert.NoError(t, err) assert.False(t, started, "transceiver with mid 0 should not be started") // Add a new transceiver (we're not using AddTrack since it'll reuse the transceiver with mid 0) _, err = pcOffer.AddTransceiverFromTrack(track1) assert.NoError(t, err) _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) pcOffer.ops.Done() pcAnswer.ops.Done() // transceiver with mid 0 should not be started started, err = isTransceiverReceiverStarted(pcAnswer, "0") assert.NoError(t, err) assert.False(t, started, "transceiver with mid 0 should not be started") // transceiver with mid 2 should be started started, err = isTransceiverReceiverStarted(pcAnswer, "2") assert.NoError(t, err) assert.True(t, started, "transceiver with mid 2 should be started") closePairNow(t, pcOffer, pcAnswer) } func TestPeerConnection_Simulcast_Probe(t *testing.T) { lim := test.TimeOut(time.Second * 30) //nolint defer lim.Stop() report := test.CheckRoutines(t) defer report() // Assert that failed Simulcast probing doesn't cause // the handleUndeclaredSSRC to be leaked t.Run("Leak", func(t *testing.T) { track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) offerer, answerer, err := newPair() assert.NoError(t, err) _, err = offerer.AddTrack(track) assert.NoError(t, err) ticker := time.NewTicker(time.Millisecond * 20) testFinished := make(chan struct{}) seenFiveStreams, seenFiveStreamsCancel := context.WithCancel(context.Background()) go func() { for { select { case <-testFinished: return case <-ticker.C: answerer.dtlsTransport.lock.Lock() if len(answerer.dtlsTransport.simulcastStreams) >= 5 { seenFiveStreamsCancel() } answerer.dtlsTransport.lock.Unlock() track.mu.Lock() if len(track.bindings) == 1 { _, err = track.bindings[0].writeStream.WriteRTP(&rtp.Header{ Version: 2, SSRC: randutil.NewMathRandomGenerator().Uint32(), }, []byte{0, 1, 2, 3, 4, 5}) assert.NoError(t, err) } track.mu.Unlock() } } }() assert.NoError(t, signalPair(offerer, answerer)) peerConnectionConnected := untilConnectionState(PeerConnectionStateConnected, offerer, answerer) peerConnectionConnected.Wait() <-seenFiveStreams.Done() closePairNow(t, offerer, answerer) close(testFinished) }) // Assert that NonSimulcast Traffic isn't incorrectly broken by the probe t.Run("Break NonSimulcast", func(t *testing.T) { unhandledSimulcastError := make(chan struct{}) m := &MediaEngine{} if err := m.RegisterDefaultCodecs(); err != nil { panic(err) } registerSimulcastHeaderExtensions(m, RTPCodecTypeVideo) pcOffer, pcAnswer, err := NewAPI(WithSettingEngine(SettingEngine{ LoggerFactory: &undeclaredSsrcLoggerFactory{unhandledSimulcastError}, }), WithMediaEngine(m)).newPair(Configuration{}) assert.NoError(t, err) firstTrack, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "firstTrack", "firstTrack") assert.NoError(t, err) _, err = pcOffer.AddTrack(firstTrack) assert.NoError(t, err) secondTrack, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "secondTrack", "secondTrack") assert.NoError(t, err) _, err = pcOffer.AddTrack(secondTrack) assert.NoError(t, err) assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) (filtered string) { shouldDiscard := false scanner := bufio.NewScanner(strings.NewReader(sessionDescription)) for scanner.Scan() { if strings.HasPrefix(scanner.Text(), "m=video") { shouldDiscard = !shouldDiscard } if !shouldDiscard { filtered += scanner.Text() + "\r\n" } } return })) sequenceNumber := uint16(0) sendRTPPacket := func() { sequenceNumber++ assert.NoError(t, firstTrack.WriteRTP(&rtp.Packet{ Header: rtp.Header{ Version: 2, SequenceNumber: sequenceNumber, }, Payload: []byte{0x00}, })) time.Sleep(20 * time.Millisecond) } for ; sequenceNumber <= 5; sequenceNumber++ { sendRTPPacket() } assert.NoError(t, signalPair(pcOffer, pcAnswer)) trackRemoteChan := make(chan *TrackRemote, 1) pcAnswer.OnTrack(func(trackRemote *TrackRemote, _ *RTPReceiver) { trackRemoteChan <- trackRemote }) trackRemote := func() *TrackRemote { for { select { case t := <-trackRemoteChan: return t default: sendRTPPacket() } } }() func() { for { select { case <-unhandledSimulcastError: return default: sendRTPPacket() } } }() _, _, err = trackRemote.Read(make([]byte, 1500)) assert.NoError(t, err) closePairNow(t, pcOffer, pcAnswer) }) } // Assert that CreateOffer returns an error for a RTPSender with no codecs // pion/webrtc#1702 func TestPeerConnection_CreateOffer_NoCodecs(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() m := &MediaEngine{} pc, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{}) assert.NoError(t, err) track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = pc.AddTrack(track) assert.NoError(t, err) _, err = pc.CreateOffer(nil) assert.Equal(t, err, ErrSenderWithNoCodecs) assert.NoError(t, pc.Close()) } // Assert that AddTrack is thread-safe func TestPeerConnection_RaceReplaceTrack(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) addTrack := func() *TrackLocalStaticSample { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) _, err = pc.AddTrack(track) assert.NoError(t, err) return track } for i := 0; i < 10; i++ { addTrack() } for _, tr := range pc.GetTransceivers() { assert.NoError(t, pc.RemoveTrack(tr.Sender())) } var wg sync.WaitGroup tracks := make([]*TrackLocalStaticSample, 10) wg.Add(10) for i := 0; i < 10; i++ { go func(j int) { tracks[j] = addTrack() wg.Done() }(i) } wg.Wait() for _, track := range tracks { have := false for _, t := range pc.GetTransceivers() { if t.Sender() != nil && t.Sender().Track() == track { have = true break } } if !have { t.Errorf("track was added but not found on senders") } } assert.NoError(t, pc.Close()) } func TestPeerConnection_Simulcast(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() rids := []string{"a", "b", "c"} var ridMapLock sync.RWMutex ridMap := map[string]int{} // Enable Extension Headers needed for Simulcast m := &MediaEngine{} if err := m.RegisterDefaultCodecs(); err != nil { panic(err) } registerSimulcastHeaderExtensions(m, RTPCodecTypeVideo) assertRidCorrect := func(t *testing.T) { ridMapLock.Lock() defer ridMapLock.Unlock() for _, rid := range rids { assert.Equal(t, ridMap[rid], 1) } assert.Equal(t, len(ridMap), 3) } ridsFullfilled := func() bool { ridMapLock.Lock() defer ridMapLock.Unlock() ridCount := len(ridMap) return ridCount == 3 } onTrackHandler := func(trackRemote *TrackRemote, _ *RTPReceiver) { ridMapLock.Lock() defer ridMapLock.Unlock() ridMap[trackRemote.RID()] = ridMap[trackRemote.RID()] + 1 } t.Run("RTP Extension Based", func(t *testing.T) { pcOffer, pcAnswer, err := NewAPI(WithMediaEngine(m)).newPair(Configuration{}) assert.NoError(t, err) vp8WriterA, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2", WithRTPStreamID("a")) assert.NoError(t, err) sender, err := pcOffer.AddTrack(vp8WriterA) assert.NoError(t, err) assert.NotNil(t, sender) vp8WriterB, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2", WithRTPStreamID("b")) assert.NoError(t, err) err = sender.AddEncoding(vp8WriterB) assert.NoError(t, err) vp8WriterC, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2", WithRTPStreamID("c")) assert.NoError(t, err) err = sender.AddEncoding(vp8WriterC) assert.NoError(t, err) ridMap = map[string]int{} pcAnswer.OnTrack(onTrackHandler) parameters := sender.GetParameters() assert.Equal(t, "a", parameters.Encodings[0].RID) assert.Equal(t, "b", parameters.Encodings[1].RID) assert.Equal(t, "c", parameters.Encodings[2].RID) var midID, ridID, rsidID uint8 for _, extension := range parameters.HeaderExtensions { switch extension.URI { case sdp.SDESMidURI: midID = uint8(extension.ID) case sdp.SDESRTPStreamIDURI: ridID = uint8(extension.ID) case sdesRepairRTPStreamIDURI: rsidID = uint8(extension.ID) } } assert.NotZero(t, midID) assert.NotZero(t, ridID) assert.NotZero(t, rsidID) assert.NoError(t, signalPair(pcOffer, pcAnswer)) for sequenceNumber := uint16(0); !ridsFullfilled(); sequenceNumber++ { time.Sleep(20 * time.Millisecond) for ssrc, rid := range rids { header := &rtp.Header{ Version: 2, SSRC: uint32(ssrc), SequenceNumber: sequenceNumber, PayloadType: 96, } assert.NoError(t, header.SetExtension(midID, []byte("0"))) // Send RSID for first 10 packets if sequenceNumber >= 10 { assert.NoError(t, header.SetExtension(ridID, []byte(rid))) } else { assert.NoError(t, header.SetExtension(rsidID, []byte(rid))) header.SSRC += 10 } var writer *TrackLocalStaticRTP switch rid { case "a": writer = vp8WriterA case "b": writer = vp8WriterB case "c": writer = vp8WriterC } _, err = writer.bindings[0].writeStream.WriteRTP(header, []byte{0x00}) assert.NoError(t, err) } } assertRidCorrect(t) closePairNow(t, pcOffer, pcAnswer) }) } webrtc-3.1.56/peerconnection_renegotiation_test.go000066400000000000000000001125601437620512100224320ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "bufio" "context" "errors" "io" "strconv" "strings" "sync" "sync/atomic" "testing" "time" "github.com/pion/rtp" "github.com/pion/transport/v2/test" "github.com/pion/webrtc/v3/internal/util" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/rtcerr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func sendVideoUntilDone(done <-chan struct{}, t *testing.T, tracks []*TrackLocalStaticSample) { for { select { case <-time.After(20 * time.Millisecond): for _, track := range tracks { assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second})) } case <-done: return } } } func sdpMidHasSsrc(offer SessionDescription, mid string, ssrc SSRC) bool { for _, media := range offer.parsed.MediaDescriptions { cmid, ok := media.Attribute("mid") if !ok { continue } if cmid != mid { continue } cssrc, ok := media.Attribute("ssrc") if !ok { continue } parts := strings.Split(cssrc, " ") ssrcInt64, err := strconv.ParseUint(parts[0], 10, 32) if err != nil { continue } if uint32(ssrcInt64) == uint32(ssrc) { return true } } return false } func TestPeerConnection_Renegotiation_AddRecvonlyTransceiver(t *testing.T) { type testCase struct { name string answererSends bool } testCases := []testCase{ // Assert the following behaviors: // - Offerer can add a recvonly transceiver // - During negotiation, answerer peer adds an inactive (or sendonly) transceiver // - Offerer can add a track // - Answerer can receive the RTP packets. {"add recvonly, then receive from answerer", false}, // Assert the following behaviors: // - Offerer can add a recvonly transceiver // - During negotiation, answerer peer adds an inactive (or sendonly) transceiver // - Answerer can add a track to the existing sendonly transceiver // - Offerer can receive the RTP packets. {"add recvonly, then send to answerer", true}, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } _, err = pcOffer.AddTransceiverFromKind( RTPCodecTypeVideo, RTPTransceiverInit{ Direction: RTPTransceiverDirectionRecvonly, }, ) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) localTrack, err := NewTrackLocalStaticSample( RTPCodecCapability{MimeType: "video/VP8"}, "track-one", "stream-one", ) require.NoError(t, err) if tc.answererSends { _, err = pcAnswer.AddTrack(localTrack) } else { _, err = pcOffer.AddTrack(localTrack) } require.NoError(t, err) onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background()) if tc.answererSends { pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { onTrackFiredFunc() }) assert.NoError(t, signalPair(pcAnswer, pcOffer)) } else { pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { onTrackFiredFunc() }) assert.NoError(t, signalPair(pcOffer, pcAnswer)) } sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{localTrack}) closePairNow(t, pcOffer, pcAnswer) }) } } /* * Assert the following behaviors * - We are able to call AddTrack after signaling * - OnTrack is NOT called on the other side until after SetRemoteDescription * - We are able to re-negotiate and AddTrack is properly called */ func TestPeerConnection_Renegotiation_AddTrack(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } haveRenegotiated := &atomicBool{} onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background()) pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { if !haveRenegotiated.get() { t.Fatal("OnTrack was called before renegotiation") } onTrackFiredFunc() }) assert.NoError(t, signalPair(pcOffer, pcAnswer)) _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) assert.NoError(t, err) vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) sender, err := pcOffer.AddTrack(vp8Track) assert.NoError(t, err) // Send 10 packets, OnTrack MUST not be fired for i := 0; i <= 10; i++ { assert.NoError(t, vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second})) time.Sleep(20 * time.Millisecond) } haveRenegotiated.set(true) assert.False(t, sender.isNegotiated()) offer, err := pcOffer.CreateOffer(nil) assert.True(t, sender.isNegotiated()) assert.NoError(t, err) assert.NoError(t, pcOffer.SetLocalDescription(offer)) assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) pcOffer.ops.Done() assert.Equal(t, 0, len(vp8Track.rtpTrack.bindings)) assert.NoError(t, pcOffer.SetRemoteDescription(answer)) pcOffer.ops.Done() assert.Equal(t, 1, len(vp8Track.rtpTrack.bindings)) sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track}) closePairNow(t, pcOffer, pcAnswer) } // Assert that adding tracks across multiple renegotiations performs as expected func TestPeerConnection_Renegotiation_AddTrack_Multiple(t *testing.T) { addTrackWithLabel := func(trackID string, pcOffer, pcAnswer *PeerConnection) *TrackLocalStaticSample { _, err := pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) assert.NoError(t, err) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, trackID, trackID) assert.NoError(t, err) _, err = pcOffer.AddTrack(track) assert.NoError(t, err) return track } trackIDs := []string{util.MathRandAlpha(16), util.MathRandAlpha(16), util.MathRandAlpha(16)} outboundTracks := []*TrackLocalStaticSample{} onTrackCount := map[string]int{} onTrackChan := make(chan struct{}, 1) lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { onTrackCount[track.ID()]++ onTrackChan <- struct{}{} }) assert.NoError(t, signalPair(pcOffer, pcAnswer)) for i := range trackIDs { outboundTracks = append(outboundTracks, addTrackWithLabel(trackIDs[i], pcOffer, pcAnswer)) assert.NoError(t, signalPair(pcOffer, pcAnswer)) sendVideoUntilDone(onTrackChan, t, outboundTracks) } closePairNow(t, pcOffer, pcAnswer) assert.Equal(t, onTrackCount[trackIDs[0]], 1) assert.Equal(t, onTrackCount[trackIDs[1]], 1) assert.Equal(t, onTrackCount[trackIDs[2]], 1) } // Assert that renegotiation triggers OnTrack() with correct ID and label from // remote side, even when a transceiver was added before the actual track data // was received. This happens when we add a transceiver on the server, create // an offer on the server and the browser's answer contains the same SSRC, but // a track hasn't been added on the browser side yet. The browser can add a // track later and renegotiate, and track ID and label will be set by the time // first packets are received. func TestPeerConnection_Renegotiation_AddTrack_Rename(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } haveRenegotiated := &atomicBool{} onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background()) var atomicRemoteTrack atomic.Value pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { if !haveRenegotiated.get() { t.Fatal("OnTrack was called before renegotiation") } onTrackFiredFunc() atomicRemoteTrack.Store(track) }) _, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) assert.NoError(t, err) vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo1", "bar1") assert.NoError(t, err) _, err = pcAnswer.AddTrack(vp8Track) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) vp8Track.rtpTrack.id = "foo2" vp8Track.rtpTrack.streamID = "bar2" haveRenegotiated.set(true) assert.NoError(t, signalPair(pcOffer, pcAnswer)) sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track}) closePairNow(t, pcOffer, pcAnswer) remoteTrack, ok := atomicRemoteTrack.Load().(*TrackRemote) require.True(t, ok) require.NotNil(t, remoteTrack) assert.Equal(t, "foo2", remoteTrack.ID()) assert.Equal(t, "bar2", remoteTrack.StreamID()) } // TestPeerConnection_Transceiver_Mid tests that we'll provide the same // transceiver for a media id on successive offer/answer func TestPeerConnection_Transceiver_Mid(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) pcAnswer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1") require.NoError(t, err) sender1, err := pcOffer.AddTrack(track1) require.NoError(t, err) track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2") require.NoError(t, err) sender2, err := pcOffer.AddTrack(track2) require.NoError(t, err) // this will create the initial offer using generateUnmatchedSDP offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) <-offerGatheringComplete assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription())) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) <-answerGatheringComplete // apply answer so we'll test generateMatchedSDP assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) pcOffer.ops.Done() pcAnswer.ops.Done() // Must have 3 media descriptions (2 video channels) assert.Equal(t, len(offer.parsed.MediaDescriptions), 2) assert.True(t, sdpMidHasSsrc(offer, "0", sender1.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "0", sender1.trackEncodings[0].ssrc, offer.SDP) // Remove first track, must keep same number of media // descriptions and same track ssrc for mid 1 as previous assert.NoError(t, pcOffer.RemoveTrack(sender1)) offer, err = pcOffer.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, pcOffer.SetLocalDescription(offer)) assert.Equal(t, len(offer.parsed.MediaDescriptions), 2) assert.True(t, sdpMidHasSsrc(offer, "1", sender2.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "1", sender2.trackEncodings[0].ssrc, offer.SDP) _, err = pcAnswer.CreateAnswer(nil) assert.Equal(t, err, &rtcerr.InvalidStateError{Err: ErrIncorrectSignalingState}) pcOffer.ops.Done() pcAnswer.ops.Done() assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) answer, err = pcAnswer.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, pcOffer.SetRemoteDescription(answer)) track3, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion3") require.NoError(t, err) sender3, err := pcOffer.AddTrack(track3) require.NoError(t, err) offer, err = pcOffer.CreateOffer(nil) assert.NoError(t, err) // We reuse the existing non-sending transceiver assert.Equal(t, len(offer.parsed.MediaDescriptions), 2) assert.True(t, sdpMidHasSsrc(offer, "0", sender3.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "0", sender3.trackEncodings[0].ssrc, offer.SDP) assert.True(t, sdpMidHasSsrc(offer, "1", sender2.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "1", sender2.trackEncodings[0].ssrc, offer.SDP) closePairNow(t, pcOffer, pcAnswer) } func TestPeerConnection_Renegotiation_CodecChange(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) pcAnswer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video1", "pion1") require.NoError(t, err) track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video2", "pion2") require.NoError(t, err) sender1, err := pcOffer.AddTrack(track1) require.NoError(t, err) _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) require.NoError(t, err) tracksCh := make(chan *TrackRemote) tracksClosed := make(chan struct{}) pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { tracksCh <- track for { if _, _, readErr := track.ReadRTP(); errors.Is(readErr, io.EOF) { tracksClosed <- struct{}{} return } } }) err = signalPair(pcOffer, pcAnswer) require.NoError(t, err) transceivers := pcOffer.GetTransceivers() require.Equal(t, 1, len(transceivers)) require.Equal(t, "0", transceivers[0].Mid()) transceivers = pcAnswer.GetTransceivers() require.Equal(t, 1, len(transceivers)) require.Equal(t, "0", transceivers[0].Mid()) ctx, cancel := context.WithCancel(context.Background()) go sendVideoUntilDone(ctx.Done(), t, []*TrackLocalStaticSample{track1}) remoteTrack1 := <-tracksCh cancel() assert.Equal(t, "video1", remoteTrack1.ID()) assert.Equal(t, "pion1", remoteTrack1.StreamID()) require.NoError(t, pcOffer.RemoveTrack(sender1)) require.NoError(t, signalPair(pcOffer, pcAnswer)) <-tracksClosed sender2, err := pcOffer.AddTrack(track2) require.NoError(t, err) require.NoError(t, signalPair(pcOffer, pcAnswer)) transceivers = pcOffer.GetTransceivers() require.Equal(t, 1, len(transceivers)) require.Equal(t, "0", transceivers[0].Mid()) transceivers = pcAnswer.GetTransceivers() require.Equal(t, 1, len(transceivers)) require.Equal(t, "0", transceivers[0].Mid()) ctx, cancel = context.WithCancel(context.Background()) go sendVideoUntilDone(ctx.Done(), t, []*TrackLocalStaticSample{track2}) remoteTrack2 := <-tracksCh cancel() require.NoError(t, pcOffer.RemoveTrack(sender2)) err = signalPair(pcOffer, pcAnswer) require.NoError(t, err) <-tracksClosed assert.Equal(t, "video2", remoteTrack2.ID()) assert.Equal(t, "pion2", remoteTrack2.StreamID()) closePairNow(t, pcOffer, pcAnswer) } func TestPeerConnection_Renegotiation_RemoveTrack(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) assert.NoError(t, err) vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) sender, err := pcOffer.AddTrack(vp8Track) assert.NoError(t, err) onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background()) trackClosed, trackClosedFunc := context.WithCancel(context.Background()) pcAnswer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { onTrackFiredFunc() for { if _, _, err := track.ReadRTP(); errors.Is(err, io.EOF) { trackClosedFunc() return } } }) assert.NoError(t, signalPair(pcOffer, pcAnswer)) sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track}) assert.NoError(t, pcOffer.RemoveTrack(sender)) assert.NoError(t, signalPair(pcOffer, pcAnswer)) <-trackClosed.Done() closePairNow(t, pcOffer, pcAnswer) } func TestPeerConnection_RoleSwitch(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcFirstOfferer, pcSecondOfferer, err := newPair() if err != nil { t.Fatal(err) } onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background()) pcFirstOfferer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { onTrackFiredFunc() }) assert.NoError(t, signalPair(pcFirstOfferer, pcSecondOfferer)) // Add a new Track to the second offerer // This asserts that it will match the ordering of the last RemoteDescription, but then also add new Transceivers to the end _, err = pcFirstOfferer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) assert.NoError(t, err) vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) _, err = pcSecondOfferer.AddTrack(vp8Track) assert.NoError(t, err) assert.NoError(t, signalPair(pcSecondOfferer, pcFirstOfferer)) sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{vp8Track}) closePairNow(t, pcFirstOfferer, pcSecondOfferer) } // Assert that renegotiation doesn't attempt to gather ICE twice // Before we would attempt to gather multiple times and would put // the PeerConnection into a broken state func TestPeerConnection_Renegotiation_Trickle(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() settingEngine := SettingEngine{} api := NewAPI(WithSettingEngine(settingEngine)) assert.NoError(t, api.mediaEngine.RegisterDefaultCodecs()) // Invalid STUN server on purpose, will stop ICE Gathering from completing in time pcOffer, pcAnswer, err := api.newPair(Configuration{ ICEServers: []ICEServer{ { URLs: []string{"stun:127.0.0.1:5000"}, }, }, }) if err != nil { t.Fatal(err) } _, err = pcOffer.CreateDataChannel("test-channel", nil) assert.NoError(t, err) var wg sync.WaitGroup wg.Add(2) pcOffer.OnICECandidate(func(c *ICECandidate) { if c != nil { assert.NoError(t, pcAnswer.AddICECandidate(c.ToJSON())) } else { wg.Done() } }) pcAnswer.OnICECandidate(func(c *ICECandidate) { if c != nil { assert.NoError(t, pcOffer.AddICECandidate(c.ToJSON())) } else { wg.Done() } }) negotiate := func() { offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) assert.NoError(t, pcOffer.SetLocalDescription(offer)) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, pcOffer.SetRemoteDescription(answer)) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) } negotiate() negotiate() pcOffer.ops.Done() pcAnswer.ops.Done() wg.Wait() closePairNow(t, pcOffer, pcAnswer) } func TestPeerConnection_Renegotiation_SetLocalDescription(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background()) pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { onTrackFiredFunc() }) assert.NoError(t, signalPair(pcOffer, pcAnswer)) pcOffer.ops.Done() pcAnswer.ops.Done() _, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly}) assert.NoError(t, err) localTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) sender, err := pcAnswer.AddTrack(localTrack) assert.NoError(t, err) offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, pcOffer.SetLocalDescription(offer)) assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) assert.False(t, sender.isNegotiated()) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) assert.True(t, sender.isNegotiated()) pcAnswer.ops.Done() assert.Equal(t, 0, len(localTrack.rtpTrack.bindings)) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) pcAnswer.ops.Done() assert.Equal(t, 1, len(localTrack.rtpTrack.bindings)) assert.NoError(t, pcOffer.SetRemoteDescription(answer)) sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{localTrack}) closePairNow(t, pcOffer, pcAnswer) } // Issue #346, don't start the SCTP Subsystem if the RemoteDescription doesn't contain one // Before we would always start it, and re-negotations would fail because SCTP was in flight func TestPeerConnection_Renegotiation_NoApplication(t *testing.T) { signalPairExcludeDataChannel := func(pcOffer, pcAnswer *PeerConnection) { offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) <-offerGatheringComplete assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription())) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) <-answerGatheringComplete assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) } lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } pcOfferConnected, pcOfferConnectedCancel := context.WithCancel(context.Background()) pcOffer.OnICEConnectionStateChange(func(i ICEConnectionState) { if i == ICEConnectionStateConnected { pcOfferConnectedCancel() } }) pcAnswerConnected, pcAnswerConnectedCancel := context.WithCancel(context.Background()) pcAnswer.OnICEConnectionStateChange(func(i ICEConnectionState) { if i == ICEConnectionStateConnected { pcAnswerConnectedCancel() } }) _, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionSendrecv}) assert.NoError(t, err) _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionSendrecv}) assert.NoError(t, err) signalPairExcludeDataChannel(pcOffer, pcAnswer) pcOffer.ops.Done() pcAnswer.ops.Done() signalPairExcludeDataChannel(pcOffer, pcAnswer) pcOffer.ops.Done() pcAnswer.ops.Done() <-pcAnswerConnected.Done() <-pcOfferConnected.Done() assert.Equal(t, pcOffer.SCTP().State(), SCTPTransportStateConnecting) assert.Equal(t, pcAnswer.SCTP().State(), SCTPTransportStateConnecting) closePairNow(t, pcOffer, pcAnswer) } func TestAddDataChannelDuringRenegotation(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) pcAnswer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = pcOffer.AddTrack(track) assert.NoError(t, err) offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) <-offerGatheringComplete assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription())) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) <-answerGatheringComplete assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) _, err = pcOffer.CreateDataChannel("data-channel", nil) assert.NoError(t, err) // Assert that DataChannel is in offer now offer, err = pcOffer.CreateOffer(nil) assert.NoError(t, err) applicationMediaSectionCount := 0 for _, d := range offer.parsed.MediaDescriptions { if d.MediaName.Media == mediaSectionApplication { applicationMediaSectionCount++ } } assert.Equal(t, applicationMediaSectionCount, 1) onDataChannelFired, onDataChannelFiredFunc := context.WithCancel(context.Background()) pcAnswer.OnDataChannel(func(*DataChannel) { onDataChannelFiredFunc() }) assert.NoError(t, signalPair(pcOffer, pcAnswer)) <-onDataChannelFired.Done() closePairNow(t, pcOffer, pcAnswer) } // Assert that CreateDataChannel fires OnNegotiationNeeded func TestNegotiationCreateDataChannel(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) var wg sync.WaitGroup wg.Add(1) pc.OnNegotiationNeeded(func() { defer func() { wg.Done() }() }) // Create DataChannel, wait until OnNegotiationNeeded is fired if _, err = pc.CreateDataChannel("testChannel", nil); err != nil { t.Error(err.Error()) } // Wait until OnNegotiationNeeded is fired wg.Wait() assert.NoError(t, pc.Close()) } func TestNegotiationNeededRemoveTrack(t *testing.T) { var wg sync.WaitGroup wg.Add(1) report := test.CheckRoutines(t) defer report() pcOffer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) pcAnswer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) pcOffer.OnNegotiationNeeded(func() { wg.Add(1) offer, createOfferErr := pcOffer.CreateOffer(nil) assert.NoError(t, createOfferErr) offerGatheringComplete := GatheringCompletePromise(pcOffer) assert.NoError(t, pcOffer.SetLocalDescription(offer)) <-offerGatheringComplete assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription())) answer, createAnswerErr := pcAnswer.CreateAnswer(nil) assert.NoError(t, createAnswerErr) answerGatheringComplete := GatheringCompletePromise(pcAnswer) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) <-answerGatheringComplete assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())) wg.Done() wg.Done() }) sender, err := pcOffer.AddTrack(track) assert.NoError(t, err) assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second})) wg.Wait() wg.Add(1) assert.NoError(t, pcOffer.RemoveTrack(sender)) wg.Wait() closePairNow(t, pcOffer, pcAnswer) } func TestNegotiationNeededStressOneSided(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcA, pcB, err := newPair() assert.NoError(t, err) const expectedTrackCount = 500 ctx, done := context.WithCancel(context.Background()) pcA.OnNegotiationNeeded(func() { count := len(pcA.GetTransceivers()) assert.NoError(t, signalPair(pcA, pcB)) if count == expectedTrackCount { done() } }) for i := 0; i < expectedTrackCount; i++ { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = pcA.AddTrack(track) assert.NoError(t, err) } <-ctx.Done() assert.Equal(t, expectedTrackCount, len(pcB.GetTransceivers())) closePairNow(t, pcA, pcB) } // TestPeerConnection_Renegotiation_DisableTrack asserts that if a remote track is set inactive // that locally it goes inactive as well func TestPeerConnection_Renegotiation_DisableTrack(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) // Create two transceivers _, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) transceiver, err := pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) // Assert we have three active transceivers offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) assert.Equal(t, strings.Count(offer.SDP, "a=sendrecv"), 3) // Assert we have two active transceivers, one inactive assert.NoError(t, transceiver.Stop()) offer, err = pcOffer.CreateOffer(nil) assert.NoError(t, err) assert.Equal(t, strings.Count(offer.SDP, "a=sendrecv"), 2) assert.Equal(t, strings.Count(offer.SDP, "a=inactive"), 1) // Assert that the offer disabled one of our transceivers assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) assert.Equal(t, strings.Count(answer.SDP, "a=sendrecv"), 1) // DataChannel assert.Equal(t, strings.Count(answer.SDP, "a=recvonly"), 1) assert.Equal(t, strings.Count(answer.SDP, "a=inactive"), 1) closePairNow(t, pcOffer, pcAnswer) } func TestPeerConnection_Renegotiation_Simulcast(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() m := &MediaEngine{} if err := m.RegisterDefaultCodecs(); err != nil { panic(err) } registerSimulcastHeaderExtensions(m, RTPCodecTypeVideo) originalRids := []string{"a", "b", "c"} signalWithRids := func(sessionDescription string, rids []string) string { sessionDescription = strings.SplitAfter(sessionDescription, "a=end-of-candidates\r\n")[0] sessionDescription = filterSsrc(sessionDescription) for _, rid := range rids { sessionDescription += "a=" + sdpAttributeRid + ":" + rid + " send\r\n" } return sessionDescription + "a=simulcast:send " + strings.Join(rids, ";") + "\r\n" } var trackMapLock sync.RWMutex trackMap := map[string]*TrackRemote{} onTrackHandler := func(track *TrackRemote, _ *RTPReceiver) { trackMapLock.Lock() defer trackMapLock.Unlock() trackMap[track.RID()] = track } sendUntilAllTracksFired := func(vp8Writer *TrackLocalStaticRTP, rids []string) { allTracksFired := func() bool { trackMapLock.Lock() defer trackMapLock.Unlock() return len(trackMap) == len(rids) } for sequenceNumber := uint16(0); !allTracksFired(); sequenceNumber++ { time.Sleep(20 * time.Millisecond) for ssrc, rid := range rids { header := &rtp.Header{ Version: 2, SSRC: uint32(ssrc), SequenceNumber: sequenceNumber, PayloadType: 96, } assert.NoError(t, header.SetExtension(1, []byte("0"))) assert.NoError(t, header.SetExtension(2, []byte(rid))) _, err := vp8Writer.bindings[0].writeStream.WriteRTP(header, []byte{0x00}) assert.NoError(t, err) } } } assertTracksClosed := func(t *testing.T) { trackMapLock.Lock() defer trackMapLock.Unlock() for _, track := range trackMap { _, _, err := track.ReadRTP() // Ignore first Read, this is our peeked data assert.Nil(t, err) _, _, err = track.ReadRTP() assert.Equal(t, err, io.EOF) } } t.Run("Disable Transceiver", func(t *testing.T) { trackMap = map[string]*TrackRemote{} pcOffer, pcAnswer, err := NewAPI(WithMediaEngine(m)).newPair(Configuration{}) assert.NoError(t, err) vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2") assert.NoError(t, err) rtpTransceiver, err := pcOffer.AddTransceiverFromTrack( vp8Writer, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendonly, }, ) assert.NoError(t, err) assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string { return signalWithRids(sessionDescription, originalRids) })) pcAnswer.OnTrack(onTrackHandler) sendUntilAllTracksFired(vp8Writer, originalRids) assert.NoError(t, pcOffer.RemoveTrack(rtpTransceiver.Sender())) assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string { sessionDescription = strings.SplitAfter(sessionDescription, "a=end-of-candidates\r\n")[0] return sessionDescription })) assertTracksClosed(t) closePairNow(t, pcOffer, pcAnswer) }) t.Run("Change RID", func(t *testing.T) { trackMap = map[string]*TrackRemote{} pcOffer, pcAnswer, err := NewAPI(WithMediaEngine(m)).newPair(Configuration{}) assert.NoError(t, err) vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2") assert.NoError(t, err) _, err = pcOffer.AddTransceiverFromTrack( vp8Writer, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendonly, }, ) assert.NoError(t, err) assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string { return signalWithRids(sessionDescription, originalRids) })) pcAnswer.OnTrack(onTrackHandler) sendUntilAllTracksFired(vp8Writer, originalRids) newRids := []string{"d", "e", "f"} assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string { scanner := bufio.NewScanner(strings.NewReader(sessionDescription)) sessionDescription = "" for scanner.Scan() { l := scanner.Text() if strings.HasPrefix(l, "a=rid") || strings.HasPrefix(l, "a=simulcast") { continue } sessionDescription += l + "\n" } return signalWithRids(sessionDescription, newRids) })) assertTracksClosed(t) closePairNow(t, pcOffer, pcAnswer) }) } func TestPeerConnection_Regegotiation_ReuseTransceiver(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) sender, err := pcOffer.AddTrack(vp8Track) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) assert.Equal(t, len(pcOffer.GetTransceivers()), 1) assert.Equal(t, pcOffer.GetTransceivers()[0].getCurrentDirection(), RTPTransceiverDirectionSendonly) assert.NoError(t, pcOffer.RemoveTrack(sender)) assert.Equal(t, pcOffer.GetTransceivers()[0].getCurrentDirection(), RTPTransceiverDirectionSendonly) // should not reuse tranceiver vp8Track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) sender2, err := pcOffer.AddTrack(vp8Track2) assert.NoError(t, err) assert.Equal(t, len(pcOffer.GetTransceivers()), 2) assert.NoError(t, signalPair(pcOffer, pcAnswer)) assert.True(t, sender2.rtpTransceiver == pcOffer.GetTransceivers()[1]) // should reuse first transceiver sender, err = pcOffer.AddTrack(vp8Track) assert.NoError(t, err) assert.Equal(t, len(pcOffer.GetTransceivers()), 2) assert.True(t, sender.rtpTransceiver == pcOffer.GetTransceivers()[0]) closePairNow(t, pcOffer, pcAnswer) } func TestPeerConnection_Renegotiation_MidConflict(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() offerPC, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) answerPC, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = offerPC.CreateDataChannel("test", nil) assert.NoError(t, err) _, err = offerPC.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionSendonly}) assert.NoError(t, err) _, err = offerPC.AddTransceiverFromKind(RTPCodecTypeAudio, RtpTransceiverInit{Direction: RTPTransceiverDirectionSendonly}) assert.NoError(t, err) offer, err := offerPC.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, offerPC.SetLocalDescription(offer)) assert.NoError(t, answerPC.SetRemoteDescription(offer), offer.SDP) answer, err := answerPC.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, answerPC.SetLocalDescription(answer)) assert.NoError(t, offerPC.SetRemoteDescription(answer)) assert.Equal(t, SignalingStateStable, offerPC.SignalingState()) tr, err := offerPC.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionSendonly}) assert.NoError(t, err) assert.NoError(t, tr.SetMid("3")) _, err = offerPC.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{Direction: RTPTransceiverDirectionSendrecv}) assert.NoError(t, err) _, err = offerPC.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, offerPC.Close()) assert.NoError(t, answerPC.Close()) } func TestPeerConnection_Regegotiation_AnswerAddsTrack(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() if err != nil { t.Fatal(err) } tracksCh := make(chan *TrackRemote) pcOffer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { tracksCh <- track for { if _, _, readErr := track.ReadRTP(); errors.Is(readErr, io.EOF) { return } } }) vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar") assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) _, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{ Direction: RTPTransceiverDirectionRecvonly, }) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RtpTransceiverInit{ Direction: RTPTransceiverDirectionSendonly, }) assert.NoError(t, err) assert.NoError(t, err) _, err = pcAnswer.AddTrack(vp8Track) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) ctx, cancel := context.WithCancel(context.Background()) go sendVideoUntilDone(ctx.Done(), t, []*TrackLocalStaticSample{vp8Track}) <-tracksCh cancel() closePairNow(t, pcOffer, pcAnswer) } webrtc-3.1.56/peerconnection_test.go000066400000000000000000000504261437620512100175050ustar00rootroot00000000000000package webrtc import ( "reflect" "sync" "testing" "time" "github.com/pion/sdp/v3" "github.com/pion/transport/v2/test" "github.com/pion/webrtc/v3/pkg/rtcerr" "github.com/stretchr/testify/assert" ) // newPair creates two new peer connections (an offerer and an answerer) // *without* using an api (i.e. using the default settings). func newPair() (pcOffer *PeerConnection, pcAnswer *PeerConnection, err error) { pca, err := NewPeerConnection(Configuration{}) if err != nil { return nil, nil, err } pcb, err := NewPeerConnection(Configuration{}) if err != nil { return nil, nil, err } return pca, pcb, nil } func signalPairWithModification(pcOffer *PeerConnection, pcAnswer *PeerConnection, modificationFunc func(string) string) error { // Note(albrow): We need to create a data channel in order to trigger ICE // candidate gathering in the background for the JavaScript/Wasm bindings. If // we don't do this, the complete offer including ICE candidates will never be // generated. if _, err := pcOffer.CreateDataChannel("initial_data_channel", nil); err != nil { return err } offer, err := pcOffer.CreateOffer(nil) if err != nil { return err } offerGatheringComplete := GatheringCompletePromise(pcOffer) if err = pcOffer.SetLocalDescription(offer); err != nil { return err } <-offerGatheringComplete offer.SDP = modificationFunc(pcOffer.LocalDescription().SDP) if err = pcAnswer.SetRemoteDescription(offer); err != nil { return err } answer, err := pcAnswer.CreateAnswer(nil) if err != nil { return err } answerGatheringComplete := GatheringCompletePromise(pcAnswer) if err = pcAnswer.SetLocalDescription(answer); err != nil { return err } <-answerGatheringComplete return pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()) } func signalPair(pcOffer *PeerConnection, pcAnswer *PeerConnection) error { return signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string { return sessionDescription }) } func offerMediaHasDirection(offer SessionDescription, kind RTPCodecType, direction RTPTransceiverDirection) bool { parsed := &sdp.SessionDescription{} if err := parsed.Unmarshal([]byte(offer.SDP)); err != nil { return false } for _, media := range parsed.MediaDescriptions { if media.MediaName.Media == kind.String() { _, exists := media.Attribute(direction.String()) return exists } } return false } func untilConnectionState(state PeerConnectionState, peers ...*PeerConnection) *sync.WaitGroup { var triggered sync.WaitGroup triggered.Add(len(peers)) for _, p := range peers { done := false hdlr := func(p PeerConnectionState) { if !done && p == state { done = true triggered.Done() } } p.OnConnectionStateChange(hdlr) } return &triggered } func TestNew(t *testing.T) { pc, err := NewPeerConnection(Configuration{ ICEServers: []ICEServer{ { URLs: []string{ "stun:stun.l.google.com:19302", }, Username: "unittest", }, }, ICETransportPolicy: ICETransportPolicyRelay, BundlePolicy: BundlePolicyMaxCompat, RTCPMuxPolicy: RTCPMuxPolicyNegotiate, PeerIdentity: "unittest", ICECandidatePoolSize: 5, }) assert.NoError(t, err) assert.NotNil(t, pc) assert.NoError(t, pc.Close()) } func TestPeerConnection_SetConfiguration(t *testing.T) { // Note: These tests don't include ICEServer.Credential, // ICEServer.CredentialType, or Certificates because those are not supported // in the WASM bindings. for _, test := range []struct { name string init func() (*PeerConnection, error) config Configuration wantErr error }{ { name: "valid", init: func() (*PeerConnection, error) { pc, err := NewPeerConnection(Configuration{ ICECandidatePoolSize: 5, }) if err != nil { return pc, err } err = pc.SetConfiguration(Configuration{ ICEServers: []ICEServer{ { URLs: []string{ "stun:stun.l.google.com:19302", }, Username: "unittest", }, }, ICETransportPolicy: ICETransportPolicyAll, BundlePolicy: BundlePolicyBalanced, RTCPMuxPolicy: RTCPMuxPolicyRequire, ICECandidatePoolSize: 5, }) if err != nil { return pc, err } return pc, nil }, config: Configuration{}, wantErr: nil, }, { name: "closed connection", init: func() (*PeerConnection, error) { pc, err := NewPeerConnection(Configuration{}) assert.Nil(t, err) err = pc.Close() assert.Nil(t, err) return pc, err }, config: Configuration{}, wantErr: &rtcerr.InvalidStateError{Err: ErrConnectionClosed}, }, { name: "update PeerIdentity", init: func() (*PeerConnection, error) { return NewPeerConnection(Configuration{}) }, config: Configuration{ PeerIdentity: "unittest", }, wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingPeerIdentity}, }, { name: "update BundlePolicy", init: func() (*PeerConnection, error) { return NewPeerConnection(Configuration{}) }, config: Configuration{ BundlePolicy: BundlePolicyMaxCompat, }, wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingBundlePolicy}, }, { name: "update RTCPMuxPolicy", init: func() (*PeerConnection, error) { return NewPeerConnection(Configuration{}) }, config: Configuration{ RTCPMuxPolicy: RTCPMuxPolicyNegotiate, }, wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingRTCPMuxPolicy}, }, { name: "update ICECandidatePoolSize", init: func() (*PeerConnection, error) { pc, err := NewPeerConnection(Configuration{ ICECandidatePoolSize: 0, }) if err != nil { return pc, err } offer, err := pc.CreateOffer(nil) if err != nil { return pc, err } err = pc.SetLocalDescription(offer) if err != nil { return pc, err } return pc, nil }, config: Configuration{ ICECandidatePoolSize: 1, }, wantErr: &rtcerr.InvalidModificationError{Err: ErrModifyingICECandidatePoolSize}, }, } { pc, err := test.init() if err != nil { t.Errorf("SetConfiguration %q: init failed: %v", test.name, err) } err = pc.SetConfiguration(test.config) if got, want := err, test.wantErr; !reflect.DeepEqual(got, want) { t.Errorf("SetConfiguration %q: err = %v, want %v", test.name, got, want) } assert.NoError(t, pc.Close()) } } func TestPeerConnection_GetConfiguration(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) expected := Configuration{ ICEServers: []ICEServer{}, ICETransportPolicy: ICETransportPolicyAll, BundlePolicy: BundlePolicyBalanced, RTCPMuxPolicy: RTCPMuxPolicyRequire, ICECandidatePoolSize: 0, } actual := pc.GetConfiguration() assert.True(t, &expected != &actual) assert.Equal(t, expected.ICEServers, actual.ICEServers) assert.Equal(t, expected.ICETransportPolicy, actual.ICETransportPolicy) assert.Equal(t, expected.BundlePolicy, actual.BundlePolicy) assert.Equal(t, expected.RTCPMuxPolicy, actual.RTCPMuxPolicy) // nolint:godox // TODO(albrow): Uncomment this after #513 is fixed. // See: https://github.com/pion/webrtc/issues/513. // assert.Equal(t, len(expected.Certificates), len(actual.Certificates)) assert.Equal(t, expected.ICECandidatePoolSize, actual.ICECandidatePoolSize) assert.NoError(t, pc.Close()) } const minimalOffer = `v=0 o=- 4596489990601351948 2 IN IP4 127.0.0.1 s=- t=0 0 a=msid-semantic: WMS m=application 47299 DTLS/SCTP 5000 c=IN IP4 192.168.20.129 a=candidate:1966762134 1 udp 2122260223 192.168.20.129 47299 typ host generation 0 a=candidate:1966762134 1 udp 2122262783 2001:db8::1 47199 typ host generation 0 a=candidate:211962667 1 udp 2122194687 10.0.3.1 40864 typ host generation 0 a=candidate:1002017894 1 tcp 1518280447 192.168.20.129 0 typ host tcptype active generation 0 a=candidate:1109506011 1 tcp 1518214911 10.0.3.1 0 typ host tcptype active generation 0 a=ice-ufrag:1/MvHwjAyVf27aLu a=ice-pwd:3dBU7cFOBl120v33cynDvN1E a=ice-options:google-ice a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24 a=setup:actpass a=mid:data a=sctpmap:5000 webrtc-datachannel 1024 ` func TestSetRemoteDescription(t *testing.T) { testCases := []struct { desc SessionDescription expectError bool }{ {SessionDescription{Type: SDPTypeOffer, SDP: minimalOffer}, false}, {SessionDescription{Type: 0, SDP: ""}, true}, } for i, testCase := range testCases { peerConn, err := NewPeerConnection(Configuration{}) if err != nil { t.Errorf("Case %d: got error: %v", i, err) } if testCase.expectError { assert.Error(t, peerConn.SetRemoteDescription(testCase.desc)) } else { assert.NoError(t, peerConn.SetRemoteDescription(testCase.desc)) } assert.NoError(t, peerConn.Close()) } } func TestCreateOfferAnswer(t *testing.T) { offerPeerConn, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) answerPeerConn, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = offerPeerConn.CreateDataChannel("test-channel", nil) assert.NoError(t, err) offer, err := offerPeerConn.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, offerPeerConn.SetLocalDescription(offer)) assert.NoError(t, answerPeerConn.SetRemoteDescription(offer)) answer, err := answerPeerConn.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, answerPeerConn.SetLocalDescription(answer)) assert.NoError(t, offerPeerConn.SetRemoteDescription(answer)) // after setLocalDescription(answer), signaling state should be stable. // so CreateAnswer should return an InvalidStateError assert.Equal(t, answerPeerConn.SignalingState(), SignalingStateStable) _, err = answerPeerConn.CreateAnswer(nil) assert.Error(t, err) closePairNow(t, offerPeerConn, answerPeerConn) } func TestPeerConnection_EventHandlers(t *testing.T) { pcOffer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) pcAnswer, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) // wasCalled is a list of event handlers that were called. wasCalled := []string{} wasCalledMut := &sync.Mutex{} // wg is used to wait for all event handlers to be called. wg := &sync.WaitGroup{} wg.Add(6) // Each sync.Once is used to ensure that we call wg.Done once for each event // handler and don't add multiple entries to wasCalled. The event handlers can // be called more than once in some cases. onceOffererOnICEConnectionStateChange := &sync.Once{} onceOffererOnConnectionStateChange := &sync.Once{} onceOffererOnSignalingStateChange := &sync.Once{} onceAnswererOnICEConnectionStateChange := &sync.Once{} onceAnswererOnConnectionStateChange := &sync.Once{} onceAnswererOnSignalingStateChange := &sync.Once{} // Register all the event handlers. pcOffer.OnICEConnectionStateChange(func(ICEConnectionState) { onceOffererOnICEConnectionStateChange.Do(func() { wasCalledMut.Lock() defer wasCalledMut.Unlock() wasCalled = append(wasCalled, "offerer OnICEConnectionStateChange") wg.Done() }) }) pcOffer.OnConnectionStateChange(func(callbackState PeerConnectionState) { if storedState := pcOffer.ConnectionState(); callbackState != storedState { t.Errorf("State in callback argument is different from ConnectionState(): callbackState=%s, storedState=%s", callbackState, storedState) } onceOffererOnConnectionStateChange.Do(func() { wasCalledMut.Lock() defer wasCalledMut.Unlock() wasCalled = append(wasCalled, "offerer OnConnectionStateChange") wg.Done() }) }) pcOffer.OnSignalingStateChange(func(SignalingState) { onceOffererOnSignalingStateChange.Do(func() { wasCalledMut.Lock() defer wasCalledMut.Unlock() wasCalled = append(wasCalled, "offerer OnSignalingStateChange") wg.Done() }) }) pcAnswer.OnICEConnectionStateChange(func(ICEConnectionState) { onceAnswererOnICEConnectionStateChange.Do(func() { wasCalledMut.Lock() defer wasCalledMut.Unlock() wasCalled = append(wasCalled, "answerer OnICEConnectionStateChange") wg.Done() }) }) pcAnswer.OnConnectionStateChange(func(PeerConnectionState) { onceAnswererOnConnectionStateChange.Do(func() { wasCalledMut.Lock() defer wasCalledMut.Unlock() wasCalled = append(wasCalled, "answerer OnConnectionStateChange") wg.Done() }) }) pcAnswer.OnSignalingStateChange(func(SignalingState) { onceAnswererOnSignalingStateChange.Do(func() { wasCalledMut.Lock() defer wasCalledMut.Unlock() wasCalled = append(wasCalled, "answerer OnSignalingStateChange") wg.Done() }) }) // Use signalPair to establish a connection between pcOffer and pcAnswer. This // process should trigger the above event handlers. assert.NoError(t, signalPair(pcOffer, pcAnswer)) // Wait for all of the event handlers to be triggered. done := make(chan struct{}) go func() { wg.Wait() done <- struct{}{} }() timeout := time.After(5 * time.Second) select { case <-done: break case <-timeout: t.Fatalf("timed out waiting for one or more events handlers to be called (these *were* called: %+v)", wasCalled) } closePairNow(t, pcOffer, pcAnswer) } func TestMultipleOfferAnswer(t *testing.T) { firstPeerConn, err := NewPeerConnection(Configuration{}) if err != nil { t.Errorf("New PeerConnection: got error: %v", err) } if _, err = firstPeerConn.CreateOffer(nil); err != nil { t.Errorf("First Offer: got error: %v", err) } if _, err = firstPeerConn.CreateOffer(nil); err != nil { t.Errorf("Second Offer: got error: %v", err) } secondPeerConn, err := NewPeerConnection(Configuration{}) if err != nil { t.Errorf("New PeerConnection: got error: %v", err) } secondPeerConn.OnICECandidate(func(i *ICECandidate) { }) if _, err = secondPeerConn.CreateOffer(nil); err != nil { t.Errorf("First Offer: got error: %v", err) } if _, err = secondPeerConn.CreateOffer(nil); err != nil { t.Errorf("Second Offer: got error: %v", err) } closePairNow(t, firstPeerConn, secondPeerConn) } func TestNoFingerprintInFirstMediaIfSetRemoteDescription(t *testing.T) { const sdpNoFingerprintInFirstMedia = `v=0 o=- 143087887 1561022767 IN IP4 192.168.84.254 s=VideoRoom 404986692241682 t=0 0 a=group:BUNDLE audio a=msid-semantic: WMS 2867270241552712 m=video 0 UDP/TLS/RTP/SAVPF 0 a=mid:video c=IN IP4 192.168.84.254 a=inactive m=audio 9 UDP/TLS/RTP/SAVPF 111 c=IN IP4 192.168.84.254 a=recvonly a=mid:audio a=rtcp-mux a=ice-ufrag:AS/w a=ice-pwd:9NOgoAOMALYu/LOpA1iqg/ a=ice-options:trickle a=fingerprint:sha-256 D2:B9:31:8F:DF:24:D8:0E:ED:D2:EF:25:9E:AF:6F:B8:34:AE:53:9C:E6:F3:8F:F2:64:15:FA:E8:7F:53:2D:38 a=setup:active a=rtpmap:111 opus/48000/2 a=candidate:1 1 udp 2013266431 192.168.84.254 46492 typ host a=end-of-candidates ` report := test.CheckRoutines(t) defer report() pc, err := NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } desc := SessionDescription{ Type: SDPTypeOffer, SDP: sdpNoFingerprintInFirstMedia, } if err = pc.SetRemoteDescription(desc); err != nil { t.Error(err.Error()) } assert.NoError(t, pc.Close()) } func TestNegotiationNeeded(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pc, err := NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } var wg sync.WaitGroup wg.Add(1) pc.OnNegotiationNeeded(wg.Done) _, err = pc.CreateDataChannel("initial_data_channel", nil) assert.NoError(t, err) wg.Wait() assert.NoError(t, pc.Close()) } func TestMultipleCreateChannel(t *testing.T) { var wg sync.WaitGroup report := test.CheckRoutines(t) defer report() // Two OnDataChannel // One OnNegotiationNeeded wg.Add(3) pcOffer, _ := NewPeerConnection(Configuration{}) pcAnswer, _ := NewPeerConnection(Configuration{}) pcAnswer.OnDataChannel(func(d *DataChannel) { wg.Done() }) pcOffer.OnNegotiationNeeded(func() { offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) offerGatheringComplete := GatheringCompletePromise(pcOffer) if err = pcOffer.SetLocalDescription(offer); err != nil { t.Error(err) } <-offerGatheringComplete if err = pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()); err != nil { t.Error(err) } answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) answerGatheringComplete := GatheringCompletePromise(pcAnswer) if err = pcAnswer.SetLocalDescription(answer); err != nil { t.Error(err) } <-answerGatheringComplete if err = pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()); err != nil { t.Error(err) } wg.Done() }) if _, err := pcOffer.CreateDataChannel("initial_data_channel_0", nil); err != nil { t.Error(err) } if _, err := pcOffer.CreateDataChannel("initial_data_channel_1", nil); err != nil { t.Error(err) } wg.Wait() closePairNow(t, pcOffer, pcAnswer) } // Assert that candidates are gathered by calling SetLocalDescription, not SetRemoteDescription func TestGatherOnSetLocalDescription(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOfferGathered := make(chan SessionDescription) pcAnswerGathered := make(chan SessionDescription) s := SettingEngine{} api := NewAPI(WithSettingEngine(s)) pcOffer, err := api.NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } // We need to create a data channel in order to trigger ICE if _, err = pcOffer.CreateDataChannel("initial_data_channel", nil); err != nil { t.Error(err.Error()) } pcOffer.OnICECandidate(func(i *ICECandidate) { if i == nil { close(pcOfferGathered) } }) offer, err := pcOffer.CreateOffer(nil) if err != nil { t.Error(err.Error()) } else if err = pcOffer.SetLocalDescription(offer); err != nil { t.Error(err.Error()) } <-pcOfferGathered pcAnswer, err := api.NewPeerConnection(Configuration{}) if err != nil { t.Error(err.Error()) } pcAnswer.OnICECandidate(func(i *ICECandidate) { if i == nil { close(pcAnswerGathered) } }) if err = pcAnswer.SetRemoteDescription(offer); err != nil { t.Error(err.Error()) } select { case <-pcAnswerGathered: t.Fatal("pcAnswer started gathering with no SetLocalDescription") // Gathering is async, not sure of a better way to catch this currently case <-time.After(3 * time.Second): } answer, err := pcAnswer.CreateAnswer(nil) if err != nil { t.Error(err.Error()) } else if err = pcAnswer.SetLocalDescription(answer); err != nil { t.Error(err.Error()) } <-pcAnswerGathered closePairNow(t, pcOffer, pcAnswer) } // Assert that SetRemoteDescription handles invalid states func TestSetRemoteDescriptionInvalid(t *testing.T) { t.Run("local-offer+SetRemoteDescription(Offer)", func(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) offer, err := pc.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, pc.SetLocalDescription(offer)) assert.Error(t, pc.SetRemoteDescription(offer)) assert.NoError(t, pc.Close()) }) } func TestAddTransceiver(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() for _, testCase := range []struct { expectSender, expectReceiver bool direction RTPTransceiverDirection }{ {true, true, RTPTransceiverDirectionSendrecv}, // Go and WASM diverge // {true, false, RTPTransceiverDirectionSendonly}, // {false, true, RTPTransceiverDirectionRecvonly}, } { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) transceiver, err := pc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ Direction: testCase.direction, }) assert.NoError(t, err) if testCase.expectReceiver { assert.NotNil(t, transceiver.Receiver()) } else { assert.Nil(t, transceiver.Receiver()) } if testCase.expectSender { assert.NotNil(t, transceiver.Sender()) } else { assert.Nil(t, transceiver.Sender()) } offer, err := pc.CreateOffer(nil) assert.NoError(t, err) assert.True(t, offerMediaHasDirection(offer, RTPCodecTypeVideo, testCase.direction)) assert.NoError(t, pc.Close()) } } // Assert that SCTPTransport -> DTLSTransport -> ICETransport works after connected func TestTransportChain(t *testing.T) { offer, answer, err := newPair() assert.NoError(t, err) peerConnectionsConnected := untilConnectionState(PeerConnectionStateConnected, offer, answer) assert.NoError(t, signalPair(offer, answer)) peerConnectionsConnected.Wait() assert.NotNil(t, offer.SCTP().Transport().ICETransport()) closePairNow(t, offer, answer) } webrtc-3.1.56/peerconnectionstate.go000066400000000000000000000063051437620512100175040ustar00rootroot00000000000000package webrtc // PeerConnectionState indicates the state of the PeerConnection. type PeerConnectionState int const ( // PeerConnectionStateNew indicates that any of the ICETransports or // DTLSTransports are in the "new" state and none of the transports are // in the "connecting", "checking", "failed" or "disconnected" state, or // all transports are in the "closed" state, or there are no transports. PeerConnectionStateNew PeerConnectionState = iota + 1 // PeerConnectionStateConnecting indicates that any of the // ICETransports or DTLSTransports are in the "connecting" or // "checking" state and none of them is in the "failed" state. PeerConnectionStateConnecting // PeerConnectionStateConnected indicates that all ICETransports and // DTLSTransports are in the "connected", "completed" or "closed" state // and at least one of them is in the "connected" or "completed" state. PeerConnectionStateConnected // PeerConnectionStateDisconnected indicates that any of the // ICETransports or DTLSTransports are in the "disconnected" state // and none of them are in the "failed" or "connecting" or "checking" state. PeerConnectionStateDisconnected // PeerConnectionStateFailed indicates that any of the ICETransports // or DTLSTransports are in a "failed" state. PeerConnectionStateFailed // PeerConnectionStateClosed indicates the peer connection is closed // and the isClosed member variable of PeerConnection is true. PeerConnectionStateClosed ) // This is done this way because of a linter. const ( peerConnectionStateNewStr = "new" peerConnectionStateConnectingStr = "connecting" peerConnectionStateConnectedStr = "connected" peerConnectionStateDisconnectedStr = "disconnected" peerConnectionStateFailedStr = "failed" peerConnectionStateClosedStr = "closed" ) func newPeerConnectionState(raw string) PeerConnectionState { switch raw { case peerConnectionStateNewStr: return PeerConnectionStateNew case peerConnectionStateConnectingStr: return PeerConnectionStateConnecting case peerConnectionStateConnectedStr: return PeerConnectionStateConnected case peerConnectionStateDisconnectedStr: return PeerConnectionStateDisconnected case peerConnectionStateFailedStr: return PeerConnectionStateFailed case peerConnectionStateClosedStr: return PeerConnectionStateClosed default: return PeerConnectionState(Unknown) } } func (t PeerConnectionState) String() string { switch t { case PeerConnectionStateNew: return peerConnectionStateNewStr case PeerConnectionStateConnecting: return peerConnectionStateConnectingStr case PeerConnectionStateConnected: return peerConnectionStateConnectedStr case PeerConnectionStateDisconnected: return peerConnectionStateDisconnectedStr case PeerConnectionStateFailed: return peerConnectionStateFailedStr case PeerConnectionStateClosed: return peerConnectionStateClosedStr default: return ErrUnknownType.Error() } } type negotiationNeededState int const ( // NegotiationNeededStateEmpty not running and queue is empty negotiationNeededStateEmpty = iota // NegotiationNeededStateEmpty running and queue is empty negotiationNeededStateRun // NegotiationNeededStateEmpty running and queue negotiationNeededStateQueue ) webrtc-3.1.56/peerconnectionstate_test.go000066400000000000000000000024511437620512100205410ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewPeerConnectionState(t *testing.T) { testCases := []struct { stateString string expectedState PeerConnectionState }{ {unknownStr, PeerConnectionState(Unknown)}, {"new", PeerConnectionStateNew}, {"connecting", PeerConnectionStateConnecting}, {"connected", PeerConnectionStateConnected}, {"disconnected", PeerConnectionStateDisconnected}, {"failed", PeerConnectionStateFailed}, {"closed", PeerConnectionStateClosed}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedState, newPeerConnectionState(testCase.stateString), "testCase: %d %v", i, testCase, ) } } func TestPeerConnectionState_String(t *testing.T) { testCases := []struct { state PeerConnectionState expectedString string }{ {PeerConnectionState(Unknown), unknownStr}, {PeerConnectionStateNew, "new"}, {PeerConnectionStateConnecting, "connecting"}, {PeerConnectionStateConnected, "connected"}, {PeerConnectionStateDisconnected, "disconnected"}, {PeerConnectionStateFailed, "failed"}, {PeerConnectionStateClosed, "closed"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.state.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/pkg/000077500000000000000000000000001437620512100136565ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/000077500000000000000000000000001437620512100147355ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/h264reader/000077500000000000000000000000001437620512100166035ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/h264reader/h264reader.go000066400000000000000000000106501437620512100210020ustar00rootroot00000000000000// Package h264reader implements a H264 Annex-B Reader package h264reader import ( "bytes" "errors" "io" ) // H264Reader reads data from stream and constructs h264 nal units type H264Reader struct { stream io.Reader nalBuffer []byte countOfConsecutiveZeroBytes int nalPrefixParsed bool readBuffer []byte tmpReadBuf []byte } var ( errNilReader = errors.New("stream is nil") errDataIsNotH264Stream = errors.New("data is not a H264 bitstream") ) // NewReader creates new H264Reader func NewReader(in io.Reader) (*H264Reader, error) { if in == nil { return nil, errNilReader } reader := &H264Reader{ stream: in, nalBuffer: make([]byte, 0), nalPrefixParsed: false, readBuffer: make([]byte, 0), tmpReadBuf: make([]byte, 4096), } return reader, nil } // NAL H.264 Network Abstraction Layer type NAL struct { PictureOrderCount uint32 // NAL header ForbiddenZeroBit bool RefIdc uint8 UnitType NalUnitType Data []byte // header byte + rbsp } func (reader *H264Reader) read(numToRead int) (data []byte, e error) { for len(reader.readBuffer) < numToRead { n, err := reader.stream.Read(reader.tmpReadBuf) if err != nil { return nil, err } if n == 0 { break } reader.readBuffer = append(reader.readBuffer, reader.tmpReadBuf[0:n]...) } var numShouldRead int if numToRead <= len(reader.readBuffer) { numShouldRead = numToRead } else { numShouldRead = len(reader.readBuffer) } data = reader.readBuffer[0:numShouldRead] reader.readBuffer = reader.readBuffer[numShouldRead:] return data, nil } func (reader *H264Reader) bitStreamStartsWithH264Prefix() (prefixLength int, e error) { nalPrefix3Bytes := []byte{0, 0, 1} nalPrefix4Bytes := []byte{0, 0, 0, 1} prefixBuffer, e := reader.read(4) if e != nil { return } n := len(prefixBuffer) if n == 0 { return 0, io.EOF } if n < 3 { return 0, errDataIsNotH264Stream } nalPrefix3BytesFound := bytes.Equal(nalPrefix3Bytes, prefixBuffer[:3]) if n == 3 { if nalPrefix3BytesFound { return 0, io.EOF } return 0, errDataIsNotH264Stream } // n == 4 if nalPrefix3BytesFound { reader.nalBuffer = append(reader.nalBuffer, prefixBuffer[3]) return 3, nil } nalPrefix4BytesFound := bytes.Equal(nalPrefix4Bytes, prefixBuffer) if nalPrefix4BytesFound { return 4, nil } return 0, errDataIsNotH264Stream } // NextNAL reads from stream and returns then next NAL, // and an error if there is incomplete frame data. // Returns all nil values when no more NALs are available. func (reader *H264Reader) NextNAL() (*NAL, error) { if !reader.nalPrefixParsed { _, err := reader.bitStreamStartsWithH264Prefix() if err != nil { return nil, err } reader.nalPrefixParsed = true } for { buffer, err := reader.read(1) if err != nil { break } n := len(buffer) if n != 1 { break } readByte := buffer[0] nalFound := reader.processByte(readByte) if nalFound { nal := newNal(reader.nalBuffer) nal.parseHeader() if nal.UnitType == NalUnitTypeSEI { reader.nalBuffer = nil continue } break } reader.nalBuffer = append(reader.nalBuffer, readByte) } if len(reader.nalBuffer) == 0 { return nil, io.EOF } nal := newNal(reader.nalBuffer) reader.nalBuffer = nil nal.parseHeader() return nal, nil } func (reader *H264Reader) processByte(readByte byte) (nalFound bool) { nalFound = false switch readByte { case 0: reader.countOfConsecutiveZeroBytes++ case 1: if reader.countOfConsecutiveZeroBytes >= 2 { countOfConsecutiveZeroBytesInPrefix := 2 if reader.countOfConsecutiveZeroBytes > 2 { countOfConsecutiveZeroBytesInPrefix = 3 } if nalUnitLength := len(reader.nalBuffer) - countOfConsecutiveZeroBytesInPrefix; nalUnitLength > 0 { reader.nalBuffer = reader.nalBuffer[0:nalUnitLength] nalFound = true } } reader.countOfConsecutiveZeroBytes = 0 default: reader.countOfConsecutiveZeroBytes = 0 } return nalFound } func newNal(data []byte) *NAL { return &NAL{PictureOrderCount: 0, ForbiddenZeroBit: false, RefIdc: 0, UnitType: NalUnitTypeUnspecified, Data: data} } func (h *NAL) parseHeader() { firstByte := h.Data[0] h.ForbiddenZeroBit = (((firstByte & 0x80) >> 7) == 1) // 0x80 = 0b10000000 h.RefIdc = (firstByte & 0x60) >> 5 // 0x60 = 0b01100000 h.UnitType = NalUnitType((firstByte & 0x1F) >> 0) // 0x1F = 0b00011111 } webrtc-3.1.56/pkg/media/h264reader/h264reader_test.go000066400000000000000000000053571437620512100220510ustar00rootroot00000000000000package h264reader import ( "bytes" "io" "testing" "github.com/stretchr/testify/require" ) func CreateReader(h264 []byte, require *require.Assertions) *H264Reader { reader, err := NewReader(bytes.NewReader(h264)) require.Nil(err) require.NotNil(reader) return reader } func TestDataDoesNotStartWithH264Header(t *testing.T) { require := require.New(t) testFunction := func(input []byte, expectedErr error) { reader := CreateReader(input, require) nal, err := reader.NextNAL() require.ErrorIs(err, expectedErr) require.Nil(nal) } h264Bytes1 := []byte{2} testFunction(h264Bytes1, io.EOF) h264Bytes2 := []byte{0, 2} testFunction(h264Bytes2, io.EOF) h264Bytes3 := []byte{0, 0, 2} testFunction(h264Bytes3, io.EOF) h264Bytes4 := []byte{0, 0, 2, 0} testFunction(h264Bytes4, errDataIsNotH264Stream) h264Bytes5 := []byte{0, 0, 0, 2} testFunction(h264Bytes5, errDataIsNotH264Stream) } func TestParseHeader(t *testing.T) { require := require.New(t) h264Bytes := []byte{0x0, 0x0, 0x1, 0xAB} reader := CreateReader(h264Bytes, require) nal, err := reader.NextNAL() require.Nil(err) require.Equal(1, len(nal.Data)) require.True(nal.ForbiddenZeroBit) require.Equal(uint32(0), nal.PictureOrderCount) require.Equal(uint8(1), nal.RefIdc) require.Equal(NalUnitTypeEndOfStream, nal.UnitType) } func TestEOF(t *testing.T) { require := require.New(t) testFunction := func(input []byte) { reader := CreateReader(input, require) nal, err := reader.NextNAL() require.Equal(io.EOF, err) require.Nil(nal) } h264Bytes1 := []byte{0, 0, 0, 1} testFunction(h264Bytes1) h264Bytes2 := []byte{0, 0, 1} testFunction(h264Bytes2) h264Bytes3 := []byte{} testFunction(h264Bytes3) } func TestSkipSEI(t *testing.T) { require := require.New(t) h264Bytes := []byte{ 0x0, 0x0, 0x0, 0x1, 0xAA, 0x0, 0x0, 0x0, 0x1, 0x6, // SEI 0x0, 0x0, 0x0, 0x1, 0xAB, } reader := CreateReader(h264Bytes, require) nal, err := reader.NextNAL() require.Nil(err) require.Equal(byte(0xAA), nal.Data[0]) nal, err = reader.NextNAL() require.Nil(err) require.Equal(byte(0xAB), nal.Data[0]) } func TestIssue1734_NextNal(t *testing.T) { tt := [...][]byte{ []byte("\x00\x00\x010\x00\x00\x01\x00\x00\x01"), []byte("\x00\x00\x00\x01\x00\x00\x01"), } for _, cur := range tt { r, err := NewReader(bytes.NewReader(cur)) require.NoError(t, err) // Just make sure it doesn't crash for { nal, err := r.NextNAL() if err != nil || nal == nil { break } } } } func TestTrailing01AfterStartCode(t *testing.T) { r, err := NewReader(bytes.NewReader([]byte{ 0x0, 0x0, 0x0, 0x1, 0x01, 0x0, 0x0, 0x0, 0x1, 0x01, })) require.NoError(t, err) for i := 0; i <= 1; i++ { nal, err := r.NextNAL() require.NoError(t, err) require.NotNil(t, nal) } } webrtc-3.1.56/pkg/media/h264reader/nalunittype.go000066400000000000000000000050241437620512100215070ustar00rootroot00000000000000package h264reader import "strconv" // NalUnitType is the type of a NAL type NalUnitType uint8 // Enums for NalUnitTypes const ( NalUnitTypeUnspecified NalUnitType = 0 // Unspecified NalUnitTypeCodedSliceNonIdr NalUnitType = 1 // Coded slice of a non-IDR picture NalUnitTypeCodedSliceDataPartitionA NalUnitType = 2 // Coded slice data partition A NalUnitTypeCodedSliceDataPartitionB NalUnitType = 3 // Coded slice data partition B NalUnitTypeCodedSliceDataPartitionC NalUnitType = 4 // Coded slice data partition C NalUnitTypeCodedSliceIdr NalUnitType = 5 // Coded slice of an IDR picture NalUnitTypeSEI NalUnitType = 6 // Supplemental enhancement information (SEI) NalUnitTypeSPS NalUnitType = 7 // Sequence parameter set NalUnitTypePPS NalUnitType = 8 // Picture parameter set NalUnitTypeAUD NalUnitType = 9 // Access unit delimiter NalUnitTypeEndOfSequence NalUnitType = 10 // End of sequence NalUnitTypeEndOfStream NalUnitType = 11 // End of stream NalUnitTypeFiller NalUnitType = 12 // Filler data NalUnitTypeSpsExt NalUnitType = 13 // Sequence parameter set extension NalUnitTypeCodedSliceAux NalUnitType = 19 // Coded slice of an auxiliary coded picture without partitioning // 14..18 // Reserved // 20..23 // Reserved // 24..31 // Unspecified ) func (n *NalUnitType) String() string { var str string switch *n { case NalUnitTypeUnspecified: str = "Unspecified" case NalUnitTypeCodedSliceNonIdr: str = "CodedSliceNonIdr" case NalUnitTypeCodedSliceDataPartitionA: str = "CodedSliceDataPartitionA" case NalUnitTypeCodedSliceDataPartitionB: str = "CodedSliceDataPartitionB" case NalUnitTypeCodedSliceDataPartitionC: str = "CodedSliceDataPartitionC" case NalUnitTypeCodedSliceIdr: str = "CodedSliceIdr" case NalUnitTypeSEI: str = "SEI" case NalUnitTypeSPS: str = "SPS" case NalUnitTypePPS: str = "PPS" case NalUnitTypeAUD: str = "AUD" case NalUnitTypeEndOfSequence: str = "EndOfSequence" case NalUnitTypeEndOfStream: str = "EndOfStream" case NalUnitTypeFiller: str = "Filler" case NalUnitTypeSpsExt: str = "SpsExt" case NalUnitTypeCodedSliceAux: str = "NalUnitTypeCodedSliceAux" default: str = "Unknown" } str = str + "(" + strconv.FormatInt(int64(*n), 10) + ")" return str } webrtc-3.1.56/pkg/media/h264writer/000077500000000000000000000000001437620512100166555ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/h264writer/h264writer.go000066400000000000000000000041061437620512100211250ustar00rootroot00000000000000// Package h264writer implements H264 media container writer package h264writer import ( "bytes" "encoding/binary" "io" "os" "github.com/pion/rtp" "github.com/pion/rtp/codecs" ) type ( // H264Writer is used to take RTP packets, parse them and // write the data to an io.Writer. // Currently it only supports non-interleaved mode // Therefore, only 1-23, 24 (STAP-A), 28 (FU-A) NAL types are allowed. // https://tools.ietf.org/html/rfc6184#section-5.2 H264Writer struct { writer io.Writer hasKeyFrame bool cachedPacket *codecs.H264Packet } ) // New builds a new H264 writer func New(filename string) (*H264Writer, error) { f, err := os.Create(filename) //nolint:gosec if err != nil { return nil, err } return NewWith(f), nil } // NewWith initializes a new H264 writer with an io.Writer output func NewWith(w io.Writer) *H264Writer { return &H264Writer{ writer: w, } } // WriteRTP adds a new packet and writes the appropriate headers for it func (h *H264Writer) WriteRTP(packet *rtp.Packet) error { if len(packet.Payload) == 0 { return nil } if !h.hasKeyFrame { if h.hasKeyFrame = isKeyFrame(packet.Payload); !h.hasKeyFrame { // key frame not defined yet. discarding packet return nil } } if h.cachedPacket == nil { h.cachedPacket = &codecs.H264Packet{} } data, err := h.cachedPacket.Unmarshal(packet.Payload) if err != nil { return err } _, err = h.writer.Write(data) return err } // Close closes the underlying writer func (h *H264Writer) Close() error { h.cachedPacket = nil if h.writer != nil { if closer, ok := h.writer.(io.Closer); ok { return closer.Close() } } return nil } func isKeyFrame(data []byte) bool { const ( typeSTAPA = 24 typeSPS = 7 naluTypeBitmask = 0x1F ) var word uint32 payload := bytes.NewReader(data) if err := binary.Read(payload, binary.BigEndian, &word); err != nil { return false } naluType := (word >> 24) & naluTypeBitmask if naluType == typeSTAPA && word&naluTypeBitmask == typeSPS { return true } else if naluType == typeSPS { return true } return false } webrtc-3.1.56/pkg/media/h264writer/h264writer_test.go000066400000000000000000000061501437620512100221650ustar00rootroot00000000000000package h264writer import ( "bytes" "errors" "testing" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) type writerCloser struct { bytes.Buffer } var errClose = errors.New("close error") func (w *writerCloser) Close() error { return errClose } func TestNewWith(t *testing.T) { writer := &writerCloser{} h264Writer := NewWith(writer) assert.NotNil(t, h264Writer.Close()) } func TestIsKeyFrame(t *testing.T) { tests := []struct { name string payload []byte want bool }{ { "When given a non-keyframe; it should return false", []byte{0x27, 0x90, 0x90}, false, }, { "When given a SPS packetized with STAP-A; it should return true", []byte{0x38, 0x00, 0x03, 0x27, 0x90, 0x90, 0x00, 0x05, 0x28, 0x90, 0x90, 0x90, 0x90}, true, }, { "When given a SPS with no packetization; it should return true", []byte{0x27, 0x90, 0x90, 0x00}, true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { got := isKeyFrame(tt.payload) assert.Equal(t, tt.want, got) }) } } func TestWriteRTP(t *testing.T) { tests := []struct { name string payload []byte hasKeyFrame bool wantBytes []byte wantErr error reuseWriter bool }{ { "When given an empty payload; it should return nil", []byte{}, false, []byte{}, nil, false, }, { "When no keyframe is defined; it should discard the packet", []byte{0x25, 0x90, 0x90}, false, []byte{}, nil, false, }, { "When a valid Single NAL Unit packet is given; it should unpack it without error", []byte{0x27, 0x90, 0x90}, true, []byte{0x00, 0x00, 0x00, 0x01, 0x27, 0x90, 0x90}, nil, false, }, { "When a valid STAP-A packet is given; it should unpack it without error", []byte{0x38, 0x00, 0x03, 0x27, 0x90, 0x90, 0x00, 0x05, 0x28, 0x90, 0x90, 0x90, 0x90}, true, []byte{0x00, 0x00, 0x00, 0x01, 0x27, 0x90, 0x90, 0x00, 0x00, 0x00, 0x01, 0x28, 0x90, 0x90, 0x90, 0x90}, nil, false, }, { "When a valid FU-A start packet is given; it should unpack it without error", []byte{0x3C, 0x85, 0x90, 0x90, 0x90}, true, []byte{}, nil, true, }, { "When a valid FU-A end packet is given; it should unpack it without error", []byte{0x3C, 0x45, 0x90, 0x90, 0x90}, true, []byte{0x00, 0x00, 0x00, 0x01, 0x25, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90}, nil, false, }, } var reuseWriter *bytes.Buffer var reuseH264Writer *H264Writer for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { writer := &bytes.Buffer{} h264Writer := &H264Writer{ hasKeyFrame: tt.hasKeyFrame, writer: writer, } if reuseWriter != nil { writer = reuseWriter } if reuseH264Writer != nil { h264Writer = reuseH264Writer } assert.Equal(t, tt.wantErr, h264Writer.WriteRTP(&rtp.Packet{ Payload: tt.payload, })) assert.True(t, bytes.Equal(tt.wantBytes, writer.Bytes())) if !tt.reuseWriter { assert.Nil(t, h264Writer.Close()) reuseWriter = nil reuseH264Writer = nil } else { reuseWriter = writer reuseH264Writer = h264Writer } }) } } webrtc-3.1.56/pkg/media/ivfreader/000077500000000000000000000000001437620512100167045ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/ivfreader/ivfreader.go000066400000000000000000000105511437620512100212040ustar00rootroot00000000000000// Package ivfreader implements IVF media container reader package ivfreader import ( "encoding/binary" "errors" "fmt" "io" ) const ( ivfFileHeaderSignature = "DKIF" ivfFileHeaderSize = 32 ivfFrameHeaderSize = 12 ) var ( errNilStream = errors.New("stream is nil") errIncompleteFrameHeader = errors.New("incomplete frame header") errIncompleteFrameData = errors.New("incomplete frame data") errIncompleteFileHeader = errors.New("incomplete file header") errSignatureMismatch = errors.New("IVF signature mismatch") errUnknownIVFVersion = errors.New("IVF version unknown, parser may not parse correctly") ) // IVFFileHeader 32-byte header for IVF files // https://wiki.multimedia.cx/index.php/IVF type IVFFileHeader struct { signature string // 0-3 version uint16 // 4-5 headerSize uint16 // 6-7 FourCC string // 8-11 Width uint16 // 12-13 Height uint16 // 14-15 TimebaseDenominator uint32 // 16-19 TimebaseNumerator uint32 // 20-23 NumFrames uint32 // 24-27 unused uint32 // 28-31 } // IVFFrameHeader 12-byte header for IVF frames // https://wiki.multimedia.cx/index.php/IVF type IVFFrameHeader struct { FrameSize uint32 // 0-3 Timestamp uint64 // 4-11 } // IVFReader is used to read IVF files and return frame payloads type IVFReader struct { stream io.Reader bytesReadSuccesfully int64 } // NewWith returns a new IVF reader and IVF file header // with an io.Reader input func NewWith(in io.Reader) (*IVFReader, *IVFFileHeader, error) { if in == nil { return nil, nil, errNilStream } reader := &IVFReader{ stream: in, } header, err := reader.parseFileHeader() if err != nil { return nil, nil, err } return reader, header, nil } // ResetReader resets the internal stream of IVFReader. This is useful // for live streams, where the end of the file might be read without the // data being finished. func (i *IVFReader) ResetReader(reset func(bytesRead int64) io.Reader) { i.stream = reset(i.bytesReadSuccesfully) } // ParseNextFrame reads from stream and returns IVF frame payload, header, // and an error if there is incomplete frame data. // Returns all nil values when no more frames are available. func (i *IVFReader) ParseNextFrame() ([]byte, *IVFFrameHeader, error) { buffer := make([]byte, ivfFrameHeaderSize) var header *IVFFrameHeader bytesRead, err := io.ReadFull(i.stream, buffer) headerBytesRead := bytesRead if errors.Is(err, io.ErrUnexpectedEOF) { return nil, nil, errIncompleteFrameHeader } else if err != nil { return nil, nil, err } header = &IVFFrameHeader{ FrameSize: binary.LittleEndian.Uint32(buffer[:4]), Timestamp: binary.LittleEndian.Uint64(buffer[4:12]), } payload := make([]byte, header.FrameSize) bytesRead, err = io.ReadFull(i.stream, payload) if errors.Is(err, io.ErrUnexpectedEOF) { return nil, nil, errIncompleteFrameData } else if err != nil { return nil, nil, err } i.bytesReadSuccesfully += int64(headerBytesRead) + int64(bytesRead) return payload, header, nil } // parseFileHeader reads 32 bytes from stream and returns // IVF file header. This is always called before ParseNextFrame() func (i *IVFReader) parseFileHeader() (*IVFFileHeader, error) { buffer := make([]byte, ivfFileHeaderSize) bytesRead, err := io.ReadFull(i.stream, buffer) if errors.Is(err, io.ErrUnexpectedEOF) { return nil, errIncompleteFileHeader } else if err != nil { return nil, err } header := &IVFFileHeader{ signature: string(buffer[:4]), version: binary.LittleEndian.Uint16(buffer[4:6]), headerSize: binary.LittleEndian.Uint16(buffer[6:8]), FourCC: string(buffer[8:12]), Width: binary.LittleEndian.Uint16(buffer[12:14]), Height: binary.LittleEndian.Uint16(buffer[14:16]), TimebaseDenominator: binary.LittleEndian.Uint32(buffer[16:20]), TimebaseNumerator: binary.LittleEndian.Uint32(buffer[20:24]), NumFrames: binary.LittleEndian.Uint32(buffer[24:28]), unused: binary.LittleEndian.Uint32(buffer[28:32]), } if header.signature != ivfFileHeaderSignature { return nil, errSignatureMismatch } else if header.version != uint16(0) { return nil, fmt.Errorf("%w: expected(0) got(%d)", errUnknownIVFVersion, header.version) } i.bytesReadSuccesfully += int64(bytesRead) return header, nil } webrtc-3.1.56/pkg/media/ivfreader/ivfreader_test.go000066400000000000000000000122441437620512100222440ustar00rootroot00000000000000package ivfreader import ( "bytes" "io" "testing" "github.com/stretchr/testify/assert" ) // buildIVFContainer takes frames and prepends valid IVF file header func buildIVFContainer(frames ...*[]byte) *bytes.Buffer { // Valid IVF file header taken from: https://github.com/webmproject/... // vp8-test-vectors/blob/master/vp80-00-comprehensive-001.ivf // Video Image Width - 176 // Video Image Height - 144 // Frame Rate Rate - 30000 // Frame Rate Scale - 1000 // Video Length in Frames - 29 // BitRate: 64.01 kb/s ivf := []byte{ 0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x38, 0x30, 0xb0, 0x00, 0x90, 0x00, 0x30, 0x75, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } for f := range frames { ivf = append(ivf, *frames[f]...) } return bytes.NewBuffer(ivf) } func TestIVFReader_ParseValidFileHeader(t *testing.T) { assert := assert.New(t) ivf := buildIVFContainer(&[]byte{}) reader, header, err := NewWith(ivf) assert.Nil(err, "IVFReader should be created") assert.NotNil(reader, "Reader shouldn't be nil") assert.NotNil(header, "Header shouldn't be nil") assert.Equal("DKIF", header.signature, "signature is 'DKIF'") assert.Equal(uint16(0), header.version, "version should be 0") assert.Equal("VP80", header.FourCC, "FourCC should be 'VP80'") assert.Equal(uint16(176), header.Width, "width should be 176") assert.Equal(uint16(144), header.Height, "height should be 144") assert.Equal(uint32(30000), header.TimebaseDenominator, "timebase denominator should be 30000") assert.Equal(uint32(1000), header.TimebaseNumerator, "timebase numerator should be 1000") assert.Equal(uint32(29), header.NumFrames, "number of frames should be 29") assert.Equal(uint32(0), header.unused, "bytes should be unused") } func TestIVFReader_ParseValidFrames(t *testing.T) { assert := assert.New(t) // Frame Length - 4 // Timestamp - None // Frame Payload - 0xDEADBEEF validFrame1 := []byte{ 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, } // Frame Length - 12 // Timestamp - None // Frame Payload - 0xDEADBEEFDEADBEEF validFrame2 := []byte{ 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, } ivf := buildIVFContainer(&validFrame1, &validFrame2) reader, _, err := NewWith(ivf) assert.Nil(err, "IVFReader should be created") assert.NotNil(reader, "Reader shouldn't be nil") // Parse Frame #1 payload, header, err := reader.ParseNextFrame() assert.Nil(err, "Should have parsed frame #1 without error") assert.Equal(uint32(4), header.FrameSize, "Frame header frameSize should be 4") assert.Equal(4, len(payload), "Payload should be length 4") assert.Equal( payload, []byte{ 0xDE, 0xAD, 0xBE, 0xEF, }, "Payload value should be 0xDEADBEEF") assert.Equal(int64(ivfFrameHeaderSize+ivfFileHeaderSize+header.FrameSize), reader.bytesReadSuccesfully) previousBytesRead := reader.bytesReadSuccesfully // Parse Frame #2 payload, header, err = reader.ParseNextFrame() assert.Nil(err, "Should have parsed frame #2 without error") assert.Equal(uint32(12), header.FrameSize, "Frame header frameSize should be 4") assert.Equal(12, len(payload), "Payload should be length 12") assert.Equal( payload, []byte{ 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, }, "Payload value should be 0xDEADBEEFDEADBEEF") assert.Equal(int64(ivfFrameHeaderSize+header.FrameSize)+previousBytesRead, reader.bytesReadSuccesfully) } func TestIVFReader_ParseIncompleteFrameHeader(t *testing.T) { assert := assert.New(t) // frame with 11-byte header (missing 1 byte) incompleteFrame := []byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } ivf := buildIVFContainer(&incompleteFrame) reader, _, err := NewWith(ivf) assert.Nil(err, "IVFReader should be created") assert.NotNil(reader, "Reader shouldn't be nil") // Parse Frame #1 payload, header, err := reader.ParseNextFrame() assert.Nil(payload, "Payload should be nil") assert.Nil(header, "Incomplete header should be nil") assert.Equal(errIncompleteFrameHeader, err) } func TestIVFReader_ParseIncompleteFramePayload(t *testing.T) { assert := assert.New(t) // frame with header defining frameSize of 4 // but only 2 bytes available (missing 2 bytes) incompleteFrame := []byte{ 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, } ivf := buildIVFContainer(&incompleteFrame) reader, _, err := NewWith(ivf) assert.Nil(err, "IVFReader should be created") assert.NotNil(reader, "Reader shouldn't be nil") // Parse Frame #1 payload, header, err := reader.ParseNextFrame() assert.Nil(payload, "Incomplete payload should be nil") assert.Nil(header, "Header should be nil") assert.Equal(errIncompleteFrameData, err) } func TestIVFReader_EOFWhenNoFramesLeft(t *testing.T) { assert := assert.New(t) ivf := buildIVFContainer(&[]byte{}) reader, _, err := NewWith(ivf) assert.Nil(err, "IVFReader should be created") assert.NotNil(reader, "Reader shouldn't be nil") _, _, err = reader.ParseNextFrame() assert.Equal(io.EOF, err) } webrtc-3.1.56/pkg/media/ivfwriter/000077500000000000000000000000001437620512100167565ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/ivfwriter/ivfwriter.go000066400000000000000000000116601437620512100213320ustar00rootroot00000000000000// Package ivfwriter implements IVF media container writer package ivfwriter import ( "encoding/binary" "errors" "io" "os" "github.com/pion/rtp" "github.com/pion/rtp/codecs" "github.com/pion/rtp/pkg/frame" ) var ( errFileNotOpened = errors.New("file not opened") errInvalidNilPacket = errors.New("invalid nil packet") errCodecAlreadySet = errors.New("codec is already set") errNoSuchCodec = errors.New("no codec for this MimeType") ) const ( mimeTypeVP8 = "video/VP8" mimeTypeAV1 = "video/AV1" ivfFileHeaderSignature = "DKIF" ) // IVFWriter is used to take RTP packets and write them to an IVF on disk type IVFWriter struct { ioWriter io.Writer count uint64 seenKeyFrame bool isVP8, isAV1 bool // VP8 currentFrame []byte // AV1 av1Frame frame.AV1 } // New builds a new IVF writer func New(fileName string, opts ...Option) (*IVFWriter, error) { f, err := os.Create(fileName) //nolint:gosec if err != nil { return nil, err } writer, err := NewWith(f, opts...) if err != nil { return nil, err } writer.ioWriter = f return writer, nil } // NewWith initialize a new IVF writer with an io.Writer output func NewWith(out io.Writer, opts ...Option) (*IVFWriter, error) { if out == nil { return nil, errFileNotOpened } writer := &IVFWriter{ ioWriter: out, seenKeyFrame: false, } for _, o := range opts { if err := o(writer); err != nil { return nil, err } } if !writer.isAV1 && !writer.isVP8 { writer.isVP8 = true } if err := writer.writeHeader(); err != nil { return nil, err } return writer, nil } func (i *IVFWriter) writeHeader() error { header := make([]byte, 32) copy(header[0:], ivfFileHeaderSignature) // DKIF binary.LittleEndian.PutUint16(header[4:], 0) // Version binary.LittleEndian.PutUint16(header[6:], 32) // Header size // FOURCC if i.isVP8 { copy(header[8:], "VP80") } else if i.isAV1 { copy(header[8:], "AV01") } binary.LittleEndian.PutUint16(header[12:], 640) // Width in pixels binary.LittleEndian.PutUint16(header[14:], 480) // Height in pixels binary.LittleEndian.PutUint32(header[16:], 30) // Framerate denominator binary.LittleEndian.PutUint32(header[20:], 1) // Framerate numerator binary.LittleEndian.PutUint32(header[24:], 900) // Frame count, will be updated on first Close() call binary.LittleEndian.PutUint32(header[28:], 0) // Unused _, err := i.ioWriter.Write(header) return err } func (i *IVFWriter) writeFrame(frame []byte) error { frameHeader := make([]byte, 12) binary.LittleEndian.PutUint32(frameHeader[0:], uint32(len(frame))) // Frame length binary.LittleEndian.PutUint64(frameHeader[4:], i.count) // PTS i.count++ if _, err := i.ioWriter.Write(frameHeader); err != nil { return err } _, err := i.ioWriter.Write(frame) return err } // WriteRTP adds a new packet and writes the appropriate headers for it func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error { if i.ioWriter == nil { return errFileNotOpened } else if len(packet.Payload) == 0 { return nil } if i.isVP8 { vp8Packet := codecs.VP8Packet{} if _, err := vp8Packet.Unmarshal(packet.Payload); err != nil { return err } isKeyFrame := vp8Packet.Payload[0] & 0x01 switch { case !i.seenKeyFrame && isKeyFrame == 1: return nil case i.currentFrame == nil && vp8Packet.S != 1: return nil } i.seenKeyFrame = true i.currentFrame = append(i.currentFrame, vp8Packet.Payload[0:]...) if !packet.Marker { return nil } else if len(i.currentFrame) == 0 { return nil } if err := i.writeFrame(i.currentFrame); err != nil { return err } i.currentFrame = nil } else if i.isAV1 { av1Packet := &codecs.AV1Packet{} if _, err := av1Packet.Unmarshal(packet.Payload); err != nil { return err } obus, err := i.av1Frame.ReadFrames(av1Packet) if err != nil { return err } for j := range obus { if err := i.writeFrame(obus[j]); err != nil { return err } } } return nil } // Close stops the recording func (i *IVFWriter) Close() error { if i.ioWriter == nil { // Returns no error as it may be convenient to call // Close() multiple times return nil } defer func() { i.ioWriter = nil }() if ws, ok := i.ioWriter.(io.WriteSeeker); ok { // Update the framecount if _, err := ws.Seek(24, 0); err != nil { return err } buff := make([]byte, 4) binary.LittleEndian.PutUint32(buff, uint32(i.count)) if _, err := ws.Write(buff); err != nil { return err } } if closer, ok := i.ioWriter.(io.Closer); ok { return closer.Close() } return nil } // An Option configures a SampleBuilder. type Option func(i *IVFWriter) error // WithCodec configures if IVFWriter is writing AV1 or VP8 packets to disk func WithCodec(mimeType string) Option { return func(i *IVFWriter) error { if i.isVP8 || i.isAV1 { return errCodecAlreadySet } switch mimeType { case mimeTypeVP8: i.isVP8 = true case mimeTypeAV1: i.isAV1 = true default: return errNoSuchCodec } return nil } } webrtc-3.1.56/pkg/media/ivfwriter/ivfwriter_test.go000066400000000000000000000246001437620512100223670ustar00rootroot00000000000000package ivfwriter import ( "bytes" "io" "testing" "github.com/pion/rtp" "github.com/pion/rtp/codecs" "github.com/stretchr/testify/assert" ) type ivfWriterPacketTest struct { buffer io.Writer message string messageClose string packet *rtp.Packet writer *IVFWriter err error closeErr error } func TestIVFWriter_Basic(t *testing.T) { assert := assert.New(t) addPacketTestCase := []ivfWriterPacketTest{ { buffer: &bytes.Buffer{}, message: "IVFWriter shouldn't be able to write something to a closed file", messageClose: "IVFWriter should be able to close an already closed file", packet: nil, err: errFileNotOpened, closeErr: nil, }, { buffer: &bytes.Buffer{}, message: "IVFWriter shouldn't be able to write something an empty packet", messageClose: "IVFWriter should be able to close the file", packet: &rtp.Packet{}, err: errInvalidNilPacket, closeErr: nil, }, { buffer: nil, message: "IVFWriter shouldn't be able to write something to a closed file", messageClose: "IVFWriter should be able to close an already closed file", packet: nil, err: errFileNotOpened, closeErr: nil, }, } // First test case has a 'nil' file descriptor writer, err := NewWith(addPacketTestCase[0].buffer) assert.Nil(err, "IVFWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false") assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0") err = writer.Close() assert.Nil(err, "IVFWriter should be able to close the stream") writer.ioWriter = nil addPacketTestCase[0].writer = writer // Second test tries to write an empty packet writer, err = NewWith(addPacketTestCase[1].buffer) assert.Nil(err, "IVFWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false") assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0") addPacketTestCase[1].writer = writer // Fourth test tries to write to a nil stream writer, err = NewWith(addPacketTestCase[2].buffer) assert.NotNil(err, "IVFWriter shouldn't be created") assert.Nil(writer, "Writer should be nil") addPacketTestCase[2].writer = writer } func TestIVFWriter_VP8(t *testing.T) { // Construct valid packet rawValidPkt := []byte{ 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x89, 0x9e, } validPacket := &rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: true, ExtensionProfile: 1, Version: 2, PayloadType: 96, SequenceNumber: 27023, Timestamp: 3653407706, SSRC: 476325762, CSRC: []uint32{}, }, Payload: rawValidPkt[20:], } assert.NoError(t, validPacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF})) // Construct mid partition packet rawMidPartPkt := []byte{ 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x88, 0x36, 0xbe, 0x89, 0x9e, } midPartPacket := &rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: true, ExtensionProfile: 1, Version: 2, PayloadType: 96, SequenceNumber: 27023, Timestamp: 3653407706, SSRC: 476325762, CSRC: []uint32{}, }, Payload: rawMidPartPkt[20:], } assert.NoError(t, midPartPacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF})) // Construct keyframe packet rawKeyframePkt := []byte{ 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, } keyframePacket := &rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: true, ExtensionProfile: 1, Version: 2, PayloadType: 96, SequenceNumber: 27023, Timestamp: 3653407706, SSRC: 476325762, CSRC: []uint32{}, }, Payload: rawKeyframePkt[20:], } assert.NoError(t, keyframePacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF})) assert := assert.New(t) // Check valid packet parameters vp8Packet := codecs.VP8Packet{} _, err := vp8Packet.Unmarshal(validPacket.Payload) assert.Nil(err, "Packet did not process") assert.Equal(uint8(1), vp8Packet.S, "Start packet S value should be 1") assert.Equal(uint8(1), vp8Packet.Payload[0]&0x01, "Non Keyframe packet P value should be 1") // Check mid partition packet parameters vp8Packet = codecs.VP8Packet{} _, err = vp8Packet.Unmarshal(midPartPacket.Payload) assert.Nil(err, "Packet did not process") assert.Equal(uint8(0), vp8Packet.S, "Mid Partition packet S value should be 0") assert.Equal(uint8(1), vp8Packet.Payload[0]&0x01, "Non Keyframe packet P value should be 1") // Check keyframe packet parameters vp8Packet = codecs.VP8Packet{} _, err = vp8Packet.Unmarshal(keyframePacket.Payload) assert.Nil(err, "Packet did not process") assert.Equal(uint8(1), vp8Packet.S, "Start packet S value should be 1") assert.Equal(uint8(0), vp8Packet.Payload[0]&0x01, "Keyframe packet P value should be 0") // The linter misbehave and thinks this code is the same as the tests in oggwriter_test // nolint:dupl addPacketTestCase := []ivfWriterPacketTest{ { buffer: &bytes.Buffer{}, message: "IVFWriter should be able to write an IVF packet", messageClose: "IVFWriter should be able to close the file", packet: validPacket, err: nil, closeErr: nil, }, { buffer: &bytes.Buffer{}, message: "IVFWriter should be able to write a Keframe IVF packet", messageClose: "IVFWriter should be able to close the file", packet: keyframePacket, err: nil, closeErr: nil, }, } // first test tries to write a valid VP8 packet writer, err := NewWith(addPacketTestCase[0].buffer, WithCodec(mimeTypeVP8)) assert.Nil(err, "IVFWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false") assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0") addPacketTestCase[0].writer = writer // second test tries to write a keyframe packet writer, err = NewWith(addPacketTestCase[1].buffer) assert.Nil(err, "IVFWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false") assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0") addPacketTestCase[1].writer = writer for _, t := range addPacketTestCase { if t.writer != nil { res := t.writer.WriteRTP(t.packet) assert.Equal(res, t.err, t.message) } } // Third test tries to write a valid VP8 packet - No Keyframe assert.False(addPacketTestCase[0].writer.seenKeyFrame, "Writer's seenKeyFrame should remain false") assert.Equal(uint64(0), addPacketTestCase[0].writer.count, "Writer's packet count should remain 0") assert.Equal(nil, addPacketTestCase[0].writer.WriteRTP(midPartPacket), "Write packet failed") // add a mid partition packet assert.Equal(uint64(0), addPacketTestCase[0].writer.count, "Writer's packet count should remain 0") // Fifth test tries to write a keyframe packet assert.True(addPacketTestCase[1].writer.seenKeyFrame, "Writer's seenKeyFrame should now be true") assert.Equal(uint64(1), addPacketTestCase[1].writer.count, "Writer's packet count should now be 1") assert.Equal(nil, addPacketTestCase[1].writer.WriteRTP(midPartPacket), "Write packet failed") // add a mid partition packet assert.Equal(uint64(1), addPacketTestCase[1].writer.count, "Writer's packet count should remain 1") assert.Equal(nil, addPacketTestCase[1].writer.WriteRTP(validPacket), "Write packet failed") // add a valid packet assert.Equal(uint64(2), addPacketTestCase[1].writer.count, "Writer's packet count should now be 2") for _, t := range addPacketTestCase { if t.writer != nil { res := t.writer.Close() assert.Equal(res, t.closeErr, t.messageClose) } } } func TestIVFWriter_EmptyPayload(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWith(buffer) assert.NoError(t, err) assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{}})) } func TestIVFWriter_Errors(t *testing.T) { // Creating a Writer with AV1 and VP8 _, err := NewWith(&bytes.Buffer{}, WithCodec(mimeTypeAV1), WithCodec(mimeTypeAV1)) assert.ErrorIs(t, err, errCodecAlreadySet) // Creating a Writer with Invalid Codec _, err = NewWith(&bytes.Buffer{}, WithCodec("")) assert.ErrorIs(t, err, errNoSuchCodec) } func TestIVFWriter_AV1(t *testing.T) { t.Run("Unfragmented", func(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWith(buffer, WithCodec(mimeTypeAV1)) assert.NoError(t, err) assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{0x00, 0x01, 0xFF}})) assert.NoError(t, writer.Close()) assert.Equal(t, buffer.Bytes(), []byte{ 0x44, 0x4b, 0x49, 0x46, 0x0, 0x0, 0x20, 0x0, 0x41, 0x56, 0x30, 0x31, 0x80, 0x2, 0xe0, 0x1, 0x1e, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x84, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, }) }) t.Run("Fragmented", func(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWith(buffer, WithCodec(mimeTypeAV1)) assert.NoError(t, err) for _, p := range [][]byte{{0x40, 0x02, 0x00, 0x01}, {0xc0, 0x02, 0x02, 0x03}, {0xc0, 0x02, 0x04, 0x04}} { assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: p})) assert.Equal(t, buffer.Bytes(), []byte{ 0x44, 0x4b, 0x49, 0x46, 0x0, 0x0, 0x20, 0x0, 0x41, 0x56, 0x30, 0x31, 0x80, 0x2, 0xe0, 0x1, 0x1e, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x84, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, }) } assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{0x80, 0x01, 0x05}})) assert.Equal(t, buffer.Bytes(), []byte{ 0x44, 0x4b, 0x49, 0x46, 0x0, 0x0, 0x20, 0x0, 0x41, 0x56, 0x30, 0x31, 0x80, 0x2, 0xe0, 0x1, 0x1e, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x84, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x3, 0x4, 0x4, 0x5, }) assert.NoError(t, writer.Close()) }) } webrtc-3.1.56/pkg/media/media.go000066400000000000000000000011361437620512100163440ustar00rootroot00000000000000// Package media provides media writer and filters package media import ( "time" "github.com/pion/rtp" ) // A Sample contains encoded media and timing information type Sample struct { Data []byte Timestamp time.Time Duration time.Duration PacketTimestamp uint32 PrevDroppedPackets uint16 } // Writer defines an interface to handle // the creation of media files type Writer interface { // Add the content of an RTP packet to the media WriteRTP(packet *rtp.Packet) error // Close the media // Note: Close implementation must be idempotent Close() error } webrtc-3.1.56/pkg/media/media_test.go000066400000000000000000000000231437620512100173750ustar00rootroot00000000000000package media_test webrtc-3.1.56/pkg/media/oggreader/000077500000000000000000000000001437620512100166745ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/oggreader/oggreader.go000066400000000000000000000123571437620512100211720ustar00rootroot00000000000000// Package oggreader implements the Ogg media container reader package oggreader import ( "encoding/binary" "errors" "io" ) const ( pageHeaderTypeBeginningOfStream = 0x02 pageHeaderSignature = "OggS" idPageSignature = "OpusHead" pageHeaderLen = 27 idPagePayloadLength = 19 ) var ( errNilStream = errors.New("stream is nil") errBadIDPageSignature = errors.New("bad header signature") errBadIDPageType = errors.New("wrong header, expected beginning of stream") errBadIDPageLength = errors.New("payload for id page must be 19 bytes") errBadIDPagePayloadSignature = errors.New("bad payload signature") errShortPageHeader = errors.New("not enough data for payload header") errChecksumMismatch = errors.New("expected and actual checksum do not match") ) // OggReader is used to read Ogg files and return page payloads type OggReader struct { stream io.Reader bytesReadSuccesfully int64 checksumTable *[256]uint32 doChecksum bool } // OggHeader is the metadata from the first two pages // in the file (ID and Comment) // // https://tools.ietf.org/html/rfc7845.html#section-3 type OggHeader struct { ChannelMap uint8 Channels uint8 OutputGain uint16 PreSkip uint16 SampleRate uint32 Version uint8 } // OggPageHeader is the metadata for a Page // Pages are the fundamental unit of multiplexing in an Ogg stream // // https://tools.ietf.org/html/rfc7845.html#section-1 type OggPageHeader struct { GranulePosition uint64 sig [4]byte version uint8 headerType uint8 serial uint32 index uint32 segmentsCount uint8 } // NewWith returns a new Ogg reader and Ogg header // with an io.Reader input func NewWith(in io.Reader) (*OggReader, *OggHeader, error) { return newWith(in /* doChecksum */, true) } func newWith(in io.Reader, doChecksum bool) (*OggReader, *OggHeader, error) { if in == nil { return nil, nil, errNilStream } reader := &OggReader{ stream: in, checksumTable: generateChecksumTable(), doChecksum: doChecksum, } header, err := reader.readHeaders() if err != nil { return nil, nil, err } return reader, header, nil } func (o *OggReader) readHeaders() (*OggHeader, error) { payload, pageHeader, err := o.ParseNextPage() if err != nil { return nil, err } header := &OggHeader{} if string(pageHeader.sig[:]) != pageHeaderSignature { return nil, errBadIDPageSignature } if pageHeader.headerType != pageHeaderTypeBeginningOfStream { return nil, errBadIDPageType } if len(payload) != idPagePayloadLength { return nil, errBadIDPageLength } if s := string(payload[:8]); s != idPageSignature { return nil, errBadIDPagePayloadSignature } header.Version = payload[8] header.Channels = payload[9] header.PreSkip = binary.LittleEndian.Uint16(payload[10:12]) header.SampleRate = binary.LittleEndian.Uint32(payload[12:16]) header.OutputGain = binary.LittleEndian.Uint16(payload[16:18]) header.ChannelMap = payload[18] return header, nil } // ParseNextPage reads from stream and returns Ogg page payload, header, // and an error if there is incomplete page data. func (o *OggReader) ParseNextPage() ([]byte, *OggPageHeader, error) { h := make([]byte, pageHeaderLen) n, err := io.ReadFull(o.stream, h) if err != nil { return nil, nil, err } else if n < len(h) { return nil, nil, errShortPageHeader } pageHeader := &OggPageHeader{ sig: [4]byte{h[0], h[1], h[2], h[3]}, } pageHeader.version = h[4] pageHeader.headerType = h[5] pageHeader.GranulePosition = binary.LittleEndian.Uint64(h[6 : 6+8]) pageHeader.serial = binary.LittleEndian.Uint32(h[14 : 14+4]) pageHeader.index = binary.LittleEndian.Uint32(h[18 : 18+4]) pageHeader.segmentsCount = h[26] sizeBuffer := make([]byte, pageHeader.segmentsCount) if _, err = io.ReadFull(o.stream, sizeBuffer); err != nil { return nil, nil, err } payloadSize := 0 for _, s := range sizeBuffer { payloadSize += int(s) } payload := make([]byte, payloadSize) if _, err = io.ReadFull(o.stream, payload); err != nil { return nil, nil, err } if o.doChecksum { var checksum uint32 updateChecksum := func(v byte) { checksum = (checksum << 8) ^ o.checksumTable[byte(checksum>>24)^v] } for index := range h { // Don't include expected checksum in our generation if index > 21 && index < 26 { updateChecksum(0) continue } updateChecksum(h[index]) } for _, s := range sizeBuffer { updateChecksum(s) } for index := range payload { updateChecksum(payload[index]) } if binary.LittleEndian.Uint32(h[22:22+4]) != checksum { return nil, nil, errChecksumMismatch } } return payload, pageHeader, nil } // ResetReader resets the internal stream of OggReader. This is useful // for live streams, where the end of the file might be read without the // data being finished. func (o *OggReader) ResetReader(reset func(bytesRead int64) io.Reader) { o.stream = reset(o.bytesReadSuccesfully) } func generateChecksumTable() *[256]uint32 { var table [256]uint32 const poly = 0x04c11db7 for i := range table { r := uint32(i) << 24 for j := 0; j < 8; j++ { if (r & 0x80000000) != 0 { r = (r << 1) ^ poly } else { r <<= 1 } table[i] = (r & 0xffffffff) } } return &table } webrtc-3.1.56/pkg/media/oggreader/oggreader_test.go000066400000000000000000000053241437620512100222250ustar00rootroot00000000000000package oggreader import ( "bytes" "io" "testing" "github.com/stretchr/testify/assert" ) // buildOggFile generates a valid oggfile that can // be used for tests func buildOggContainer() []byte { return []byte{ 0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0x9b, 0x20, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x61, 0xee, 0x61, 0x17, 0x01, 0x13, 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x02, 0x00, 0x0f, 0x80, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4f, 0x67, 0x67, 0x53, 0x00, 0x00, 0xda, 0x93, 0xc2, 0xd9, 0x00, 0x00, 0x00, 0x00, 0x8e, 0x9b, 0x20, 0xaa, 0x02, 0x00, 0x00, 0x00, 0x49, 0x97, 0x03, 0x37, 0x01, 0x05, 0x98, 0x36, 0xbe, 0x88, 0x9e, } } func TestOggReader_ParseValidHeader(t *testing.T) { reader, header, err := NewWith(bytes.NewReader(buildOggContainer())) assert.NoError(t, err) assert.NotNil(t, reader) assert.NotNil(t, header) assert.EqualValues(t, header.ChannelMap, 0) assert.EqualValues(t, header.Channels, 2) assert.EqualValues(t, header.OutputGain, 0) assert.EqualValues(t, header.PreSkip, 0xf00) assert.EqualValues(t, header.SampleRate, 48000) assert.EqualValues(t, header.Version, 1) } func TestOggReader_ParseNextPage(t *testing.T) { ogg := bytes.NewReader(buildOggContainer()) reader, _, err := NewWith(ogg) assert.NoError(t, err) assert.NotNil(t, reader) payload, _, err := reader.ParseNextPage() assert.Equal(t, []byte{0x98, 0x36, 0xbe, 0x88, 0x9e}, payload) assert.NoError(t, err) _, _, err = reader.ParseNextPage() assert.Equal(t, err, io.EOF) } func TestOggReader_ParseErrors(t *testing.T) { t.Run("Assert that Reader isn't nil", func(t *testing.T) { _, _, err := NewWith(nil) assert.Equal(t, err, errNilStream) }) t.Run("Invalid ID Page Header Signature", func(t *testing.T) { ogg := buildOggContainer() ogg[0] = 0 _, _, err := newWith(bytes.NewReader(ogg), false) assert.Equal(t, err, errBadIDPageSignature) }) t.Run("Invalid ID Page Header Type", func(t *testing.T) { ogg := buildOggContainer() ogg[5] = 0 _, _, err := newWith(bytes.NewReader(ogg), false) assert.Equal(t, err, errBadIDPageType) }) t.Run("Invalid ID Page Payload Length", func(t *testing.T) { ogg := buildOggContainer() ogg[27] = 0 _, _, err := newWith(bytes.NewReader(ogg), false) assert.Equal(t, err, errBadIDPageLength) }) t.Run("Invalid ID Page Payload Length", func(t *testing.T) { ogg := buildOggContainer() ogg[35] = 0 _, _, err := newWith(bytes.NewReader(ogg), false) assert.Equal(t, err, errBadIDPagePayloadSignature) }) t.Run("Invalid Page Checksum", func(t *testing.T) { ogg := buildOggContainer() ogg[22] = 0 _, _, err := NewWith(bytes.NewReader(ogg)) assert.Equal(t, err, errChecksumMismatch) }) } webrtc-3.1.56/pkg/media/oggwriter/000077500000000000000000000000001437620512100167465ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/oggwriter/oggwriter.go000066400000000000000000000205001437620512100213030ustar00rootroot00000000000000// Package oggwriter implements OGG media container writer package oggwriter import ( "encoding/binary" "errors" "io" "os" "github.com/pion/randutil" "github.com/pion/rtp" "github.com/pion/rtp/codecs" ) const ( pageHeaderTypeContinuationOfStream = 0x00 pageHeaderTypeBeginningOfStream = 0x02 pageHeaderTypeEndOfStream = 0x04 defaultPreSkip = 3840 // 3840 recommended in the RFC idPageSignature = "OpusHead" commentPageSignature = "OpusTags" pageHeaderSignature = "OggS" ) var ( errFileNotOpened = errors.New("file not opened") errInvalidNilPacket = errors.New("invalid nil packet") ) // OggWriter is used to take RTP packets and write them to an OGG on disk type OggWriter struct { stream io.Writer fd *os.File sampleRate uint32 channelCount uint16 serial uint32 pageIndex uint32 checksumTable *[256]uint32 previousGranulePosition uint64 previousTimestamp uint32 lastPayloadSize int } // New builds a new OGG Opus writer func New(fileName string, sampleRate uint32, channelCount uint16) (*OggWriter, error) { f, err := os.Create(fileName) //nolint:gosec if err != nil { return nil, err } writer, err := NewWith(f, sampleRate, channelCount) if err != nil { return nil, f.Close() } writer.fd = f return writer, nil } // NewWith initialize a new OGG Opus writer with an io.Writer output func NewWith(out io.Writer, sampleRate uint32, channelCount uint16) (*OggWriter, error) { if out == nil { return nil, errFileNotOpened } writer := &OggWriter{ stream: out, sampleRate: sampleRate, channelCount: channelCount, serial: randutil.NewMathRandomGenerator().Uint32(), checksumTable: generateChecksumTable(), // Timestamp and Granule MUST start from 1 // Only headers can have 0 values previousTimestamp: 1, previousGranulePosition: 1, } if err := writer.writeHeaders(); err != nil { return nil, err } return writer, nil } /* ref: https://tools.ietf.org/html/rfc7845.html https://git.xiph.org/?p=opus-tools.git;a=blob;f=src/opus_header.c#l219 Page 0 Pages 1 ... n Pages (n+1) ... +------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +-- | | | | | | | | | | | | | |+----------+| |+-----------------+| |+-------------------+ +----- |||ID Header|| || Comment Header || ||Audio Data Packet 1| | ... |+----------+| |+-----------------+| |+-------------------+ +----- | | | | | | | | | | | | | +------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +-- ^ ^ ^ | | | | | Mandatory Page Break | | | ID header is contained on a single page | 'Beginning Of Stream' Figure 1: Example Packet Organization for a Logical Ogg Opus Stream */ func (i *OggWriter) writeHeaders() error { // ID Header oggIDHeader := make([]byte, 19) copy(oggIDHeader[0:], idPageSignature) // Magic Signature 'OpusHead' oggIDHeader[8] = 1 // Version oggIDHeader[9] = uint8(i.channelCount) // Channel count binary.LittleEndian.PutUint16(oggIDHeader[10:], defaultPreSkip) // pre-skip binary.LittleEndian.PutUint32(oggIDHeader[12:], i.sampleRate) // original sample rate, any valid sample e.g 48000 binary.LittleEndian.PutUint16(oggIDHeader[16:], 0) // output gain oggIDHeader[18] = 0 // channel map 0 = one stream: mono or stereo // Reference: https://tools.ietf.org/html/rfc7845.html#page-6 // RFC specifies that the ID Header page should have a granule position of 0 and a Header Type set to 2 (StartOfStream) data := i.createPage(oggIDHeader, pageHeaderTypeBeginningOfStream, 0, i.pageIndex) if err := i.writeToStream(data); err != nil { return err } i.pageIndex++ // Comment Header oggCommentHeader := make([]byte, 21) copy(oggCommentHeader[0:], commentPageSignature) // Magic Signature 'OpusTags' binary.LittleEndian.PutUint32(oggCommentHeader[8:], 5) // Vendor Length copy(oggCommentHeader[12:], "pion") // Vendor name 'pion' binary.LittleEndian.PutUint32(oggCommentHeader[17:], 0) // User Comment List Length // RFC specifies that the page where the CommentHeader completes should have a granule position of 0 data = i.createPage(oggCommentHeader, pageHeaderTypeContinuationOfStream, 0, i.pageIndex) if err := i.writeToStream(data); err != nil { return err } i.pageIndex++ return nil } const ( pageHeaderSize = 27 ) func (i *OggWriter) createPage(payload []uint8, headerType uint8, granulePos uint64, pageIndex uint32) []byte { i.lastPayloadSize = len(payload) page := make([]byte, pageHeaderSize+1+i.lastPayloadSize) copy(page[0:], pageHeaderSignature) // page headers starts with 'OggS' page[4] = 0 // Version page[5] = headerType // 1 = continuation, 2 = beginning of stream, 4 = end of stream binary.LittleEndian.PutUint64(page[6:], granulePos) // granule position binary.LittleEndian.PutUint32(page[14:], i.serial) // Bitstream serial number binary.LittleEndian.PutUint32(page[18:], pageIndex) // Page sequence number page[26] = 1 // Number of segments in page, giving always 1 segment page[27] = uint8(i.lastPayloadSize) // Segment Table inserting at 27th position since page header length is 27 copy(page[28:], payload) // inserting at 28th since Segment Table(1) + header length(27) var checksum uint32 for index := range page { checksum = (checksum << 8) ^ i.checksumTable[byte(checksum>>24)^page[index]] } binary.LittleEndian.PutUint32(page[22:], checksum) // Checksum - generating for page data and inserting at 22th position into 32 bits return page } // WriteRTP adds a new packet and writes the appropriate headers for it func (i *OggWriter) WriteRTP(packet *rtp.Packet) error { if packet == nil { return errInvalidNilPacket } if len(packet.Payload) == 0 { return nil } opusPacket := codecs.OpusPacket{} if _, err := opusPacket.Unmarshal(packet.Payload); err != nil { // Only handle Opus packets return err } payload := opusPacket.Payload[0:] // Should be equivalent to sampleRate * duration if i.previousTimestamp != 1 { increment := packet.Timestamp - i.previousTimestamp i.previousGranulePosition += uint64(increment) } i.previousTimestamp = packet.Timestamp data := i.createPage(payload, pageHeaderTypeContinuationOfStream, i.previousGranulePosition, i.pageIndex) i.pageIndex++ return i.writeToStream(data) } // Close stops the recording func (i *OggWriter) Close() error { defer func() { i.fd = nil i.stream = nil }() // Returns no error has it may be convenient to call // Close() multiple times if i.fd == nil { // Close stream if we are operating on a stream if closer, ok := i.stream.(io.Closer); ok { return closer.Close() } return nil } // Seek back one page, we need to update the header and generate new CRC pageOffset, err := i.fd.Seek(-1*int64(i.lastPayloadSize+pageHeaderSize+1), 2) if err != nil { return err } payload := make([]byte, i.lastPayloadSize) if _, err := i.fd.ReadAt(payload, pageOffset+pageHeaderSize+1); err != nil { return err } data := i.createPage(payload, pageHeaderTypeEndOfStream, i.previousGranulePosition, i.pageIndex-1) if err := i.writeToStream(data); err != nil { return err } // Update the last page if we are operating on files // to mark it as the EOS return i.fd.Close() } // Wraps writing to the stream and maintains state // so we can set values for EOS func (i *OggWriter) writeToStream(p []byte) error { if i.stream == nil { return errFileNotOpened } _, err := i.stream.Write(p) return err } func generateChecksumTable() *[256]uint32 { var table [256]uint32 const poly = 0x04c11db7 for i := range table { r := uint32(i) << 24 for j := 0; j < 8; j++ { if (r & 0x80000000) != 0 { r = (r << 1) ^ poly } else { r <<= 1 } table[i] = (r & 0xffffffff) } } return &table } webrtc-3.1.56/pkg/media/oggwriter/oggwriter_test.go000066400000000000000000000073761437620512100223620ustar00rootroot00000000000000package oggwriter import ( "bytes" "io" "testing" "github.com/pion/rtp" "github.com/stretchr/testify/assert" ) type oggWriterPacketTest struct { buffer io.Writer message string messageClose string packet *rtp.Packet writer *OggWriter err error closeErr error } func TestOggWriter_AddPacketAndClose(t *testing.T) { rawPkt := []byte{ 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, } validPacket := &rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: true, ExtensionProfile: 1, Version: 2, PayloadType: 111, SequenceNumber: 27023, Timestamp: 3653407706, SSRC: 476325762, CSRC: []uint32{}, }, Payload: rawPkt[20:], } assert.NoError(t, validPacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF})) assert := assert.New(t) // The linter misbehave and thinks this code is the same as the tests in ivf-writer_test // nolint:dupl addPacketTestCase := []oggWriterPacketTest{ { buffer: &bytes.Buffer{}, message: "OggWriter shouldn't be able to write something to a closed file", messageClose: "OggWriter should be able to close an already closed file", packet: validPacket, err: errFileNotOpened, closeErr: nil, }, { buffer: &bytes.Buffer{}, message: "OggWriter shouldn't be able to write a nil packet", messageClose: "OggWriter should be able to close the file", packet: nil, err: errInvalidNilPacket, closeErr: nil, }, { buffer: &bytes.Buffer{}, message: "OggWriter should be able to write an Opus packet", messageClose: "OggWriter should be able to close the file", packet: validPacket, err: nil, closeErr: nil, }, { buffer: nil, message: "OggWriter shouldn't be able to write something to a closed file", messageClose: "OggWriter should be able to close an already closed file", packet: nil, err: errFileNotOpened, closeErr: nil, }, } // First test case has a 'nil' file descriptor writer, err := NewWith(addPacketTestCase[0].buffer, 48000, 2) assert.Nil(err, "OggWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") err = writer.Close() assert.Nil(err, "OggWriter should be able to close the file descriptor") writer.stream = nil addPacketTestCase[0].writer = writer // Second test writes tries to write an empty packet writer, err = NewWith(addPacketTestCase[1].buffer, 48000, 2) assert.Nil(err, "OggWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") addPacketTestCase[1].writer = writer // Third test writes tries to write a valid Opus packet writer, err = NewWith(addPacketTestCase[2].buffer, 48000, 2) assert.Nil(err, "OggWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") addPacketTestCase[2].writer = writer // Fourth test tries to write to a nil stream writer, err = NewWith(addPacketTestCase[3].buffer, 4800, 2) assert.NotNil(err, "IVFWriter shouldn't be created") assert.Nil(writer, "Writer should be nil") addPacketTestCase[3].writer = writer for _, t := range addPacketTestCase { if t.writer != nil { res := t.writer.WriteRTP(t.packet) assert.Equal(t.err, res, t.message) } } for _, t := range addPacketTestCase { if t.writer != nil { res := t.writer.Close() assert.Equal(t.closeErr, res, t.messageClose) } } } func TestOggWriter_EmptyPayload(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWith(buffer, 48000, 2) assert.NoError(t, err) assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{}})) } webrtc-3.1.56/pkg/media/rtpdump/000077500000000000000000000000001437620512100164305ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/rtpdump/reader.go000066400000000000000000000040571437620512100202270ustar00rootroot00000000000000package rtpdump import ( "bufio" "errors" "io" "regexp" "sync" ) // Reader reads the RTPDump file format type Reader struct { readerMu sync.Mutex reader io.Reader } // NewReader opens a new Reader and immediately reads the Header from the start // of the input stream. func NewReader(r io.Reader) (*Reader, Header, error) { var hdr Header bio := bufio.NewReader(r) // Look ahead to see if there's a valid preamble peek, err := bio.Peek(preambleLen) if errors.Is(err, io.EOF) { return nil, hdr, errMalformed } if err != nil { return nil, hdr, err } // The file starts with #!rtpplay1.0 address/port\n preambleRegexp := regexp.MustCompile(`#\!rtpplay1\.0 \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,5}\n`) if !preambleRegexp.Match(peek) { return nil, hdr, errMalformed } // consume the preamble _, _, err = bio.ReadLine() if errors.Is(err, io.EOF) { return nil, hdr, errMalformed } if err != nil { return nil, hdr, err } hBuf := make([]byte, headerLen) _, err = io.ReadFull(bio, hBuf) if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) { return nil, hdr, errMalformed } if err != nil { return nil, hdr, err } if err := hdr.Unmarshal(hBuf); err != nil { return nil, hdr, err } return &Reader{ reader: bio, }, hdr, nil } // Next returns the next Packet in the Reader input stream func (r *Reader) Next() (Packet, error) { r.readerMu.Lock() defer r.readerMu.Unlock() hBuf := make([]byte, pktHeaderLen) _, err := io.ReadFull(r.reader, hBuf) if errors.Is(err, io.ErrUnexpectedEOF) { return Packet{}, errMalformed } if err != nil { return Packet{}, err } var h packetHeader if err = h.Unmarshal(hBuf); err != nil { return Packet{}, err } if h.Length == 0 { return Packet{}, errMalformed } payload := make([]byte, h.Length-pktHeaderLen) _, err = io.ReadFull(r.reader, payload) if errors.Is(err, io.ErrUnexpectedEOF) { return Packet{}, errMalformed } if err != nil { return Packet{}, err } return Packet{ Offset: h.offset(), IsRTCP: h.PacketLength == 0, Payload: payload, }, nil } webrtc-3.1.56/pkg/media/rtpdump/reader_test.go000066400000000000000000000136121437620512100212630ustar00rootroot00000000000000package rtpdump import ( "bytes" "errors" "io" "net" "reflect" "testing" "time" ) func TestReader(t *testing.T) { validPreamble := []byte("#!rtpplay1.0 224.2.0.1/3456\n") for _, test := range []struct { Name string Data []byte WantHeader Header WantPackets []Packet WantErr error }{ { Name: "empty", Data: nil, WantErr: errMalformed, }, { Name: "hashbang missing ip/port", Data: append( []byte("#!rtpplay1.0 \n"), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ), WantErr: errMalformed, }, { Name: "hashbang missing port", Data: append( []byte("#!rtpplay1.0 0.0.0.0\n"), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ), WantErr: errMalformed, }, { Name: "valid empty file", Data: append( validPreamble, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x22, 0xB8, 0x00, 0x00, ), WantHeader: Header{ Start: time.Unix(1, 0).UTC(), Source: net.IPv4(1, 1, 1, 1), Port: 8888, }, }, { Name: "malformed packet header", Data: append( validPreamble, // header 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // packet header 0x00, ), WantHeader: Header{ Start: time.Unix(0, 0).UTC(), Source: net.IPv4(0, 0, 0, 0), Port: 0, }, WantErr: errMalformed, }, { Name: "short packet payload", Data: append( validPreamble, // header 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // packet header len=1048575 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // packet payload 0x00, ), WantHeader: Header{ Start: time.Unix(0, 0).UTC(), Source: net.IPv4(0, 0, 0, 0), Port: 0, }, WantErr: errMalformed, }, { Name: "empty packet payload", Data: append( validPreamble, // header 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // packet header len=0 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ), WantHeader: Header{ Start: time.Unix(0, 0).UTC(), Source: net.IPv4(0, 0, 0, 0), Port: 0, }, WantErr: errMalformed, }, { Name: "valid rtcp packet", Data: append( validPreamble, // header 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // packet header len=20, pLen=0, off=1 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // packet payload (BYE) 0x81, 0xcb, 0x00, 0x0c, 0x90, 0x2f, 0x9e, 0x2e, 0x03, 0x46, 0x4f, 0x4f, ), WantHeader: Header{ Start: time.Unix(0, 0).UTC(), Source: net.IPv4(0, 0, 0, 0), Port: 0, }, WantPackets: []Packet{ { Offset: time.Millisecond, IsRTCP: true, Payload: []byte{ 0x81, 0xcb, 0x00, 0x0c, 0x90, 0x2f, 0x9e, 0x2e, 0x03, 0x46, 0x4f, 0x4f, }, }, }, WantErr: nil, }, { Name: "truncated rtcp packet", Data: append( validPreamble, // header 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // packet header len=9, pLen=0, off=1 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // invalid payload 0x81, ), WantHeader: Header{ Start: time.Unix(0, 0).UTC(), Source: net.IPv4(0, 0, 0, 0), Port: 0, }, WantPackets: []Packet{ { Offset: time.Millisecond, IsRTCP: true, Payload: []byte{0x81}, }, }, }, { Name: "two valid packets", Data: append( validPreamble, // header 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // packet header len=20, pLen=0, off=1 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // packet payload (BYE) 0x81, 0xcb, 0x00, 0x0c, 0x90, 0x2f, 0x9e, 0x2e, 0x03, 0x46, 0x4f, 0x4f, // packet header len=33, pLen=0, off=2 0x00, 0x21, 0x00, 0x19, 0x00, 0x00, 0x00, 0x02, // packet payload (RTP) 0x90, 0x60, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, ), WantHeader: Header{ Start: time.Unix(0, 0).UTC(), Source: net.IPv4(0, 0, 0, 0), Port: 0, }, WantPackets: []Packet{ { Offset: time.Millisecond, IsRTCP: true, Payload: []byte{ 0x81, 0xcb, 0x00, 0x0c, 0x90, 0x2f, 0x9e, 0x2e, 0x03, 0x46, 0x4f, 0x4f, }, }, { Offset: 2 * time.Millisecond, IsRTCP: false, Payload: []byte{ 0x90, 0x60, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, }, }, }, WantErr: nil, }, } { r, hdr, err := NewReader(bytes.NewReader(test.Data)) if err != nil { if got, want := err, test.WantErr; !errors.Is(got, want) { t.Fatalf("NewReader(%s) err=%v want %v", test.Name, got, want) } continue } if got, want := hdr, test.WantHeader; !reflect.DeepEqual(got, want) { t.Fatalf("%q Header = %#v, want %#v", test.Name, got, want) } var nextErr error var packets []Packet for { pkt, err := r.Next() if errors.Is(err, io.EOF) { break } if err != nil { nextErr = err break } packets = append(packets, pkt) } if got, want := nextErr, test.WantErr; !errors.Is(got, want) { t.Fatalf("%s err=%v want %v", test.Name, got, want) } if got, want := packets, test.WantPackets; !reflect.DeepEqual(got, want) { t.Fatalf("%q packets=%#v, want %#v", test.Name, got, want) } } } webrtc-3.1.56/pkg/media/rtpdump/rtpdump.go000066400000000000000000000075771437620512100204720ustar00rootroot00000000000000// Package rtpdump implements the RTPDump file format documented at // https://www.cs.columbia.edu/irt/software/rtptools/ package rtpdump import ( "encoding/binary" "errors" "net" "time" ) const ( pktHeaderLen = 8 headerLen = 16 preambleLen = 36 ) var errMalformed = errors.New("malformed rtpdump") // Header is the binary header at the top of the RTPDump file. It contains // information about the source and start time of the packet stream included // in the file. type Header struct { // start of recording (GMT) Start time.Time // network source (multicast address) Source net.IP // UDP port Port uint16 } // Marshal encodes the Header as binary. func (h Header) Marshal() ([]byte, error) { d := make([]byte, headerLen) startNano := h.Start.UnixNano() startSec := uint32(startNano / int64(time.Second)) startUsec := uint32( (startNano % int64(time.Second)) / int64(time.Microsecond), ) binary.BigEndian.PutUint32(d[0:], startSec) binary.BigEndian.PutUint32(d[4:], startUsec) source := h.Source.To4() copy(d[8:], source) binary.BigEndian.PutUint16(d[12:], h.Port) return d, nil } // Unmarshal decodes the Header from binary. func (h *Header) Unmarshal(d []byte) error { if len(d) < headerLen { return errMalformed } // time as a `struct timeval` startSec := binary.BigEndian.Uint32(d[0:]) startUsec := binary.BigEndian.Uint32(d[4:]) h.Start = time.Unix(int64(startSec), int64(startUsec)*1e3).UTC() // ipv4 address h.Source = net.IPv4(d[8], d[9], d[10], d[11]) h.Port = binary.BigEndian.Uint16(d[12:]) // 2 bytes of padding (ignored) return nil } // Packet contains an RTP or RTCP packet along a time offset when it was logged // (relative to the Start of the recording in Header). The Payload may contain // truncated packets to support logging just the headers of RTP/RTCP packets. type Packet struct { // Offset is the time since the start of recording in millseconds Offset time.Duration // IsRTCP is true if the payload is RTCP, false if the payload is RTP IsRTCP bool // Payload is the binary RTP or or RTCP payload. The contents may not parse // as a valid packet if the contents have been truncated. Payload []byte } // Marshal encodes the Packet as binary. func (p Packet) Marshal() ([]byte, error) { packetLength := len(p.Payload) if p.IsRTCP { packetLength = 0 } hdr := packetHeader{ Length: uint16(len(p.Payload)) + 8, PacketLength: uint16(packetLength), Offset: p.offsetMs(), } hdrData, err := hdr.Marshal() if err != nil { return nil, err } return append(hdrData, p.Payload...), nil } // Unmarshal decodes the Packet from binary. func (p *Packet) Unmarshal(d []byte) error { var hdr packetHeader if err := hdr.Unmarshal(d); err != nil { return err } p.Offset = hdr.offset() p.IsRTCP = hdr.Length != 0 && hdr.PacketLength == 0 if hdr.Length < 8 { return errMalformed } if len(d) < int(hdr.Length) { return errMalformed } p.Payload = d[8:hdr.Length] return nil } func (p *Packet) offsetMs() uint32 { return uint32(p.Offset / time.Millisecond) } type packetHeader struct { // length of packet, including this header (may be smaller than // plen if not whole packet recorded) Length uint16 // Actual header+payload length for RTP, 0 for RTCP PacketLength uint16 // milliseconds since the start of recording Offset uint32 } func (p packetHeader) Marshal() ([]byte, error) { d := make([]byte, pktHeaderLen) binary.BigEndian.PutUint16(d[0:], p.Length) binary.BigEndian.PutUint16(d[2:], p.PacketLength) binary.BigEndian.PutUint32(d[4:], p.Offset) return d, nil } func (p *packetHeader) Unmarshal(d []byte) error { if len(d) < pktHeaderLen { return errMalformed } p.Length = binary.BigEndian.Uint16(d[0:]) p.PacketLength = binary.BigEndian.Uint16(d[2:]) p.Offset = binary.BigEndian.Uint32(d[4:]) return nil } func (p packetHeader) offset() time.Duration { return time.Duration(p.Offset) * time.Millisecond } webrtc-3.1.56/pkg/media/rtpdump/rtpdump_test.go000066400000000000000000000041671437620512100215210ustar00rootroot00000000000000package rtpdump import ( "errors" "net" "reflect" "testing" "time" ) func TestHeaderRoundTrip(t *testing.T) { for _, test := range []struct { Header Header }{ { Header: Header{ Start: time.Unix(0, 0).UTC(), Source: net.IPv4(0, 0, 0, 0), Port: 0, }, }, { Header: Header{ Start: time.Date(2019, 3, 25, 1, 1, 1, 0, time.UTC), Source: net.IPv4(1, 2, 3, 4), Port: 8080, }, }, } { d, err := test.Header.Marshal() if err != nil { t.Fatal(err) } var hdr Header if err := hdr.Unmarshal(d); err != nil { t.Fatal(err) } if got, want := hdr, test.Header; !reflect.DeepEqual(got, want) { t.Fatalf("Unmarshal(%v.Marshal()) = %v, want identical", got, want) } } } func TestMarshalHeader(t *testing.T) { for _, test := range []struct { Name string Header Header Want []byte WantErr error }{ { Name: "nil source", Header: Header{ Start: time.Unix(0, 0).UTC(), Source: nil, Port: 0, }, Want: []byte{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, } { data, err := test.Header.Marshal() if got, want := err, test.WantErr; !errors.Is(got, want) { t.Fatalf("Marshal(%q) err=%v, want %v", test.Name, got, want) } if got, want := data, test.Want; !reflect.DeepEqual(got, want) { t.Fatalf("Marshal(%q) = %v, want %v", test.Name, got, want) } } } func TestPacketRoundTrip(t *testing.T) { for _, test := range []struct { Packet Packet }{ { Packet: Packet{ Offset: 0, IsRTCP: false, Payload: []byte{0}, }, }, { Packet: Packet{ Offset: 0, IsRTCP: true, Payload: []byte{0}, }, }, { Packet: Packet{ Offset: 123 * time.Millisecond, IsRTCP: false, Payload: []byte{1, 2, 3, 4}, }, }, } { d, err := test.Packet.Marshal() if err != nil { t.Fatal(err) } var pkt Packet if err := pkt.Unmarshal(d); err != nil { t.Fatal(err) } if got, want := pkt, test.Packet; !reflect.DeepEqual(got, want) { t.Fatalf("Unmarshal(%v.Marshal()) = %v, want identical", got, want) } } } webrtc-3.1.56/pkg/media/rtpdump/writer.go000066400000000000000000000016331437620512100202760ustar00rootroot00000000000000package rtpdump import ( "fmt" "io" "sync" ) // Writer writes the RTPDump file format type Writer struct { writerMu sync.Mutex writer io.Writer } // NewWriter makes a new Writer and immediately writes the given Header // to begin the file. func NewWriter(w io.Writer, hdr Header) (*Writer, error) { preamble := fmt.Sprintf( "#!rtpplay1.0 %s/%d\n", hdr.Source.To4().String(), hdr.Port) if _, err := w.Write([]byte(preamble)); err != nil { return nil, err } hData, err := hdr.Marshal() if err != nil { return nil, err } if _, err := w.Write(hData); err != nil { return nil, err } return &Writer{writer: w}, nil } // WritePacket writes a Packet to the output func (w *Writer) WritePacket(p Packet) error { w.writerMu.Lock() defer w.writerMu.Unlock() data, err := p.Marshal() if err != nil { return err } if _, err := w.writer.Write(data); err != nil { return err } return nil } webrtc-3.1.56/pkg/media/rtpdump/writer_test.go000066400000000000000000000035151437620512100213360ustar00rootroot00000000000000package rtpdump import ( "bytes" "errors" "io" "net" "reflect" "testing" "time" ) func TestWriter(t *testing.T) { buf := bytes.NewBuffer(nil) writer, err := NewWriter(buf, Header{ Start: time.Unix(9, 0), Source: net.IPv4(2, 2, 2, 2), Port: 2222, }) if err != nil { t.Fatal(err) } if err := writer.WritePacket(Packet{ Offset: time.Millisecond, IsRTCP: false, Payload: []byte{9}, }); err != nil { t.Fatal(err) } expected := append( []byte("#!rtpplay1.0 2.2.2.2/2222\n"), // header 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x02, 0x02, 0x08, 0xae, 0x00, 0x00, // packet header 0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, ) if got, want := buf.Bytes(), expected; !reflect.DeepEqual(got, want) { t.Fatalf("wrote %v, want %v", got, want) } } func TestRoundTrip(t *testing.T) { buf := bytes.NewBuffer(nil) packets := []Packet{ { Offset: time.Millisecond, IsRTCP: false, Payload: []byte{9}, }, { Offset: 999 * time.Millisecond, IsRTCP: true, Payload: []byte{9}, }, } hdr := Header{ Start: time.Unix(9, 0).UTC(), Source: net.IPv4(2, 2, 2, 2), Port: 2222, } writer, err := NewWriter(buf, hdr) if err != nil { t.Fatal(err) } for _, pkt := range packets { if err = writer.WritePacket(pkt); err != nil { t.Fatal(err) } } reader, hdr2, err := NewReader(buf) if err != nil { t.Fatal(err) } if got, want := hdr2, hdr; !reflect.DeepEqual(got, want) { t.Fatalf("round trip: header=%v, want %v", got, want) } var packets2 []Packet for { pkt, err := reader.Next() if errors.Is(err, io.EOF) { break } if err != nil { t.Fatal(err) } packets2 = append(packets2, pkt) } if got, want := packets2, packets; !reflect.DeepEqual(got, want) { t.Fatalf("round trip: packets=%v, want %v", got, want) } } webrtc-3.1.56/pkg/media/samplebuilder/000077500000000000000000000000001437620512100175655ustar00rootroot00000000000000webrtc-3.1.56/pkg/media/samplebuilder/sampleSequenceLocation.go000066400000000000000000000020031437620512100245520ustar00rootroot00000000000000// Package samplebuilder provides functionality to reconstruct media frames from RTP packets. package samplebuilder type sampleSequenceLocation struct { // head is the first packet in a sequence head uint16 // tail is always set to one after the final sequence number, // so if head == tail then the sequence is empty tail uint16 } func (l sampleSequenceLocation) empty() bool { return l.head == l.tail } func (l sampleSequenceLocation) hasData() bool { return l.head != l.tail } func (l sampleSequenceLocation) count() uint16 { return seqnumDistance(l.head, l.tail) } const ( slCompareVoid = iota slCompareBefore slCompareInside slCompareAfter ) func (l sampleSequenceLocation) compare(pos uint16) int { if l.head == l.tail { return slCompareVoid } if l.head < l.tail { if l.head <= pos && pos < l.tail { return slCompareInside } } else { if l.head <= pos || pos < l.tail { return slCompareInside } } if l.head-pos <= pos-l.tail { return slCompareBefore } return slCompareAfter } webrtc-3.1.56/pkg/media/samplebuilder/sampleSequenceLocation_test.go000066400000000000000000000015631437620512100256230ustar00rootroot00000000000000package samplebuilder import ( "testing" "github.com/stretchr/testify/assert" ) func TestSampleSequenceLocationCompare(t *testing.T) { s1 := sampleSequenceLocation{32, 42} assert.Equal(t, slCompareBefore, s1.compare(16)) assert.Equal(t, slCompareInside, s1.compare(32)) assert.Equal(t, slCompareInside, s1.compare(38)) assert.Equal(t, slCompareInside, s1.compare(41)) assert.Equal(t, slCompareAfter, s1.compare(42)) assert.Equal(t, slCompareAfter, s1.compare(0x57)) s2 := sampleSequenceLocation{0xffa0, 32} assert.Equal(t, slCompareBefore, s2.compare(0xff00)) assert.Equal(t, slCompareInside, s2.compare(0xffa0)) assert.Equal(t, slCompareInside, s2.compare(0xffff)) assert.Equal(t, slCompareInside, s2.compare(0)) assert.Equal(t, slCompareInside, s2.compare(31)) assert.Equal(t, slCompareAfter, s2.compare(32)) assert.Equal(t, slCompareAfter, s2.compare(128)) } webrtc-3.1.56/pkg/media/samplebuilder/samplebuilder.go000066400000000000000000000225121437620512100227460ustar00rootroot00000000000000// Package samplebuilder provides functionality to reconstruct media frames from RTP packets. package samplebuilder import ( "math" "time" "github.com/pion/rtp" "github.com/pion/webrtc/v3/pkg/media" ) // SampleBuilder buffers packets until media frames are complete. type SampleBuilder struct { maxLate uint16 // how many packets to wait until we get a valid Sample maxLateTimestamp uint32 // max timestamp between old and new timestamps before dropping packets buffer [math.MaxUint16 + 1]*rtp.Packet preparedSamples [math.MaxUint16 + 1]*media.Sample // Interface that allows us to take RTP packets to samples depacketizer rtp.Depacketizer // sampleRate allows us to compute duration of media.SamplecA sampleRate uint32 // the handler to be called when the builder is about to remove the // reference to some packet. packetReleaseHandler func(*rtp.Packet) // filled contains the head/tail of the packets inserted into the buffer filled sampleSequenceLocation // active contains the active head/tail of the timestamp being actively processed active sampleSequenceLocation // prepared contains the samples that have been processed to date prepared sampleSequenceLocation // number of packets forced to be dropped droppedPackets uint16 } // New constructs a new SampleBuilder. // maxLate is how long to wait until we can construct a completed media.Sample. // maxLate is measured in RTP packet sequence numbers. // A large maxLate will result in less packet loss but higher latency. // The depacketizer extracts media samples from RTP packets. // Several depacketizers are available in package github.com/pion/rtp/codecs. func New(maxLate uint16, depacketizer rtp.Depacketizer, sampleRate uint32, opts ...Option) *SampleBuilder { s := &SampleBuilder{maxLate: maxLate, depacketizer: depacketizer, sampleRate: sampleRate} for _, o := range opts { o(s) } return s } func (s *SampleBuilder) tooOld(location sampleSequenceLocation) bool { if s.maxLateTimestamp == 0 { return false } var foundHead *rtp.Packet var foundTail *rtp.Packet for i := location.head; i != location.tail; i++ { if packet := s.buffer[i]; packet != nil { foundHead = packet break } } if foundHead == nil { return false } for i := location.tail - 1; i != location.head; i-- { if packet := s.buffer[i]; packet != nil { foundTail = packet break } } if foundTail == nil { return false } return timestampDistance(foundHead.Timestamp, foundTail.Timestamp) > s.maxLateTimestamp } // fetchTimestamp returns the timestamp associated with a given sample location func (s *SampleBuilder) fetchTimestamp(location sampleSequenceLocation) (timestamp uint32, hasData bool) { if location.empty() { return 0, false } packet := s.buffer[location.head] if packet == nil { return 0, false } return packet.Timestamp, true } func (s *SampleBuilder) releasePacket(i uint16) { var p *rtp.Packet p, s.buffer[i] = s.buffer[i], nil if p != nil && s.packetReleaseHandler != nil { s.packetReleaseHandler(p) } } // purgeConsumedBuffers clears all buffers that have already been consumed by // popping. func (s *SampleBuilder) purgeConsumedBuffers() { s.purgeConsumedLocation(s.active, false) } // purgeConsumedLocation clears all buffers that have already been consumed // during a sample building method. func (s *SampleBuilder) purgeConsumedLocation(consume sampleSequenceLocation, forceConsume bool) { if !s.filled.hasData() { return } switch consume.compare(s.filled.head) { case slCompareInside: if !forceConsume { break } fallthrough case slCompareBefore: s.releasePacket(s.filled.head) s.filled.head++ } } // purgeBuffers flushes all buffers that are already consumed or those buffers // that are too late to consume. func (s *SampleBuilder) purgeBuffers() { s.purgeConsumedBuffers() for (s.tooOld(s.filled) || (s.filled.count() > s.maxLate)) && s.filled.hasData() { if s.active.empty() { // refill the active based on the filled packets s.active = s.filled } if s.active.hasData() && (s.active.head == s.filled.head) { // attempt to force the active packet to be consumed even though // outstanding data may be pending arrival if s.buildSample(true) != nil { continue } // could not build the sample so drop it s.active.head++ s.droppedPackets++ } s.releasePacket(s.filled.head) s.filled.head++ } } // Push adds an RTP Packet to s's buffer. // // Push does not copy the input. If you wish to reuse // this memory make sure to copy before calling Push func (s *SampleBuilder) Push(p *rtp.Packet) { s.buffer[p.SequenceNumber] = p switch s.filled.compare(p.SequenceNumber) { case slCompareVoid: s.filled.head = p.SequenceNumber s.filled.tail = p.SequenceNumber + 1 case slCompareBefore: s.filled.head = p.SequenceNumber case slCompareAfter: s.filled.tail = p.SequenceNumber + 1 case slCompareInside: break } s.purgeBuffers() } const secondToNanoseconds = 1000000000 // buildSample creates a sample from a valid collection of RTP Packets by // walking forwards building a sample if everything looks good clear and // update buffer+values func (s *SampleBuilder) buildSample(purgingBuffers bool) *media.Sample { if s.active.empty() { s.active = s.filled } if s.active.empty() { return nil } if s.filled.compare(s.active.tail) == slCompareInside { s.active.tail = s.filled.tail } var consume sampleSequenceLocation for i := s.active.head; s.buffer[i] != nil && s.active.compare(i) != slCompareAfter; i++ { if s.depacketizer.IsPartitionTail(s.buffer[i].Marker, s.buffer[i].Payload) { consume.head = s.active.head consume.tail = i + 1 break } headTimestamp, hasData := s.fetchTimestamp(s.active) if hasData && s.buffer[i].Timestamp != headTimestamp { consume.head = s.active.head consume.tail = i break } } if consume.empty() { return nil } if !purgingBuffers && s.buffer[consume.tail] == nil { // wait for the next packet after this set of packets to arrive // to ensure at least one post sample timestamp is known // (unless we have to release right now) return nil } sampleTimestamp, _ := s.fetchTimestamp(s.active) afterTimestamp := sampleTimestamp // scan for any packet after the current and use that time stamp as the diff point for i := consume.tail; i < s.active.tail; i++ { if s.buffer[i] != nil { afterTimestamp = s.buffer[i].Timestamp break } } // the head set of packets is now fully consumed s.active.head = consume.tail // prior to decoding all the packets, check if this packet // would end being disposed anyway if !s.depacketizer.IsPartitionHead(s.buffer[consume.head].Payload) { s.droppedPackets += consume.count() s.purgeConsumedLocation(consume, true) s.purgeConsumedBuffers() return nil } // merge all the buffers into a sample data := []byte{} for i := consume.head; i != consume.tail; i++ { p, err := s.depacketizer.Unmarshal(s.buffer[i].Payload) if err != nil { return nil } data = append(data, p...) } samples := afterTimestamp - sampleTimestamp sample := &media.Sample{ Data: data, Duration: time.Duration((float64(samples)/float64(s.sampleRate))*secondToNanoseconds) * time.Nanosecond, PacketTimestamp: sampleTimestamp, PrevDroppedPackets: s.droppedPackets, } s.droppedPackets = 0 s.preparedSamples[s.prepared.tail] = sample s.prepared.tail++ s.purgeConsumedLocation(consume, true) s.purgeConsumedBuffers() return sample } // Pop compiles pushed RTP packets into media samples and then // returns the next valid sample (or nil if no sample is compiled). func (s *SampleBuilder) Pop() *media.Sample { _ = s.buildSample(false) if s.prepared.empty() { return nil } var result *media.Sample result, s.preparedSamples[s.prepared.head] = s.preparedSamples[s.prepared.head], nil s.prepared.head++ return result } // PopWithTimestamp compiles pushed RTP packets into media samples and then // returns the next valid sample with its associated RTP timestamp (or nil, 0 if // no sample is compiled). func (s *SampleBuilder) PopWithTimestamp() (*media.Sample, uint32) { sample := s.Pop() if sample == nil { return nil, 0 } return sample, sample.PacketTimestamp } // seqnumDistance computes the distance between two sequence numbers func seqnumDistance(x, y uint16) uint16 { diff := int16(x - y) if diff < 0 { return uint16(-diff) } return uint16(diff) } // timestampDistance computes the distance between two timestamps func timestampDistance(x, y uint32) uint32 { diff := int32(x - y) if diff < 0 { return uint32(-diff) } return uint32(diff) } // An Option configures a SampleBuilder. type Option func(o *SampleBuilder) // WithPartitionHeadChecker is obsolete, it does nothing. func WithPartitionHeadChecker(checker interface{}) Option { return func(o *SampleBuilder) { } } // WithPacketReleaseHandler set a callback when the builder is about to release // some packet. func WithPacketReleaseHandler(h func(*rtp.Packet)) Option { return func(o *SampleBuilder) { o.packetReleaseHandler = h } } // WithMaxTimeDelay ensures that packets that are too old in the buffer get // purged based on time rather than building up an extraordinarily long delay. func WithMaxTimeDelay(maxLateDuration time.Duration) Option { return func(o *SampleBuilder) { totalMillis := maxLateDuration.Milliseconds() o.maxLateTimestamp = uint32(int64(o.sampleRate) * totalMillis / 1000) } } webrtc-3.1.56/pkg/media/samplebuilder/samplebuilder_test.go000066400000000000000000000444511437620512100240130ustar00rootroot00000000000000package samplebuilder import ( "fmt" "testing" "time" "github.com/pion/rtp" "github.com/pion/webrtc/v3/pkg/media" "github.com/stretchr/testify/assert" ) type sampleBuilderTest struct { message string packets []*rtp.Packet withHeadChecker bool headBytes []byte samples []*media.Sample maxLate uint16 maxLateTimestamp uint32 } type fakeDepacketizer struct { headChecker bool headBytes []byte } func (f *fakeDepacketizer) Unmarshal(r []byte) ([]byte, error) { return r, nil } func (f *fakeDepacketizer) IsPartitionHead(payload []byte) bool { if !f.headChecker { // simulates a bug in the 3.0 version // the tests should be fixed to not assume the bug return true } for _, b := range f.headBytes { if payload[0] == b { return true } } return false } func (f *fakeDepacketizer) IsPartitionTail(marker bool, payload []byte) bool { return marker } func TestSampleBuilder(t *testing.T) { testData := []sampleBuilderTest{ { message: "SampleBuilder shouldn't emit anything if only one RTP packet has been pushed", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}}, }, samples: []*media.Sample{}, maxLate: 50, maxLateTimestamp: 0, }, { message: "SampleBuilder shouldn't emit anything if only one RTP packet has been pushed even if the market bit is set", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5, Marker: true}, Payload: []byte{0x01}}, }, samples: []*media.Sample{}, maxLate: 50, maxLateTimestamp: 0, }, { message: "SampleBuilder should emit two packets, we had three packets with unique timestamps", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5001, Timestamp: 6}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7}, Payload: []byte{0x03}}, }, samples: []*media.Sample{ {Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 5}, {Data: []byte{0x02}, Duration: time.Second, PacketTimestamp: 6}, }, maxLate: 50, maxLateTimestamp: 0, }, { message: "SampleBuilder should emit one packet, we had a packet end of sequence marker and run out of space", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5, Marker: true}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5004, Timestamp: 9}, Payload: []byte{0x03}}, {Header: rtp.Header{SequenceNumber: 5006, Timestamp: 11}, Payload: []byte{0x04}}, {Header: rtp.Header{SequenceNumber: 5008, Timestamp: 13}, Payload: []byte{0x05}}, {Header: rtp.Header{SequenceNumber: 5010, Timestamp: 15}, Payload: []byte{0x06}}, {Header: rtp.Header{SequenceNumber: 5012, Timestamp: 17}, Payload: []byte{0x07}}, }, samples: []*media.Sample{ {Data: []byte{0x01}, Duration: time.Second * 2, PacketTimestamp: 5}, }, maxLate: 5, maxLateTimestamp: 0, }, { message: "SampleBuilder shouldn't emit any packet, we do not have a valid end of sequence and run out of space", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5004, Timestamp: 9}, Payload: []byte{0x03}}, {Header: rtp.Header{SequenceNumber: 5006, Timestamp: 11}, Payload: []byte{0x04}}, {Header: rtp.Header{SequenceNumber: 5008, Timestamp: 13}, Payload: []byte{0x05}}, {Header: rtp.Header{SequenceNumber: 5010, Timestamp: 15}, Payload: []byte{0x06}}, {Header: rtp.Header{SequenceNumber: 5012, Timestamp: 17}, Payload: []byte{0x07}}, }, samples: []*media.Sample{}, maxLate: 5, maxLateTimestamp: 0, }, { message: "SampleBuilder should emit one packet, we had a packet end of sequence marker and run out of space", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5, Marker: true}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7, Marker: true}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5004, Timestamp: 9}, Payload: []byte{0x03}}, {Header: rtp.Header{SequenceNumber: 5006, Timestamp: 11}, Payload: []byte{0x04}}, {Header: rtp.Header{SequenceNumber: 5008, Timestamp: 13}, Payload: []byte{0x05}}, {Header: rtp.Header{SequenceNumber: 5010, Timestamp: 15}, Payload: []byte{0x06}}, {Header: rtp.Header{SequenceNumber: 5012, Timestamp: 17}, Payload: []byte{0x07}}, }, samples: []*media.Sample{ {Data: []byte{0x01}, Duration: time.Second * 2, PacketTimestamp: 5}, {Data: []byte{0x02}, Duration: time.Second * 2, PacketTimestamp: 7, PrevDroppedPackets: 1}, }, maxLate: 5, maxLateTimestamp: 0, }, { message: "SampleBuilder should emit one packet, we had two packets but two with duplicate timestamps", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5001, Timestamp: 6}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5002, Timestamp: 6}, Payload: []byte{0x03}}, {Header: rtp.Header{SequenceNumber: 5003, Timestamp: 7}, Payload: []byte{0x04}}, }, samples: []*media.Sample{ {Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 5}, {Data: []byte{0x02, 0x03}, Duration: time.Second, PacketTimestamp: 6}, }, maxLate: 50, maxLateTimestamp: 0, }, { message: "SampleBuilder shouldn't emit a packet because we have a gap before a valid one", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5007, Timestamp: 6}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5008, Timestamp: 7}, Payload: []byte{0x03}}, }, samples: []*media.Sample{}, maxLate: 50, maxLateTimestamp: 0, }, { message: "SampleBuilder shouldn't emit a packet after a gap as there are gaps and have not reached maxLate yet", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5007, Timestamp: 6}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5008, Timestamp: 7}, Payload: []byte{0x03}}, }, withHeadChecker: true, headBytes: []byte{0x02}, samples: []*media.Sample{}, maxLate: 50, maxLateTimestamp: 0, }, { message: "SampleBuilder shouldn't emit a packet after a gap if PartitionHeadChecker doesn't assume it head", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5007, Timestamp: 6}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5008, Timestamp: 7}, Payload: []byte{0x03}}, }, withHeadChecker: true, headBytes: []byte{}, samples: []*media.Sample{}, maxLate: 50, maxLateTimestamp: 0, }, { message: "SampleBuilder should emit multiple valid packets", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 1}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5001, Timestamp: 2}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5002, Timestamp: 3}, Payload: []byte{0x03}}, {Header: rtp.Header{SequenceNumber: 5003, Timestamp: 4}, Payload: []byte{0x04}}, {Header: rtp.Header{SequenceNumber: 5004, Timestamp: 5}, Payload: []byte{0x05}}, {Header: rtp.Header{SequenceNumber: 5005, Timestamp: 6}, Payload: []byte{0x06}}, }, samples: []*media.Sample{ {Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 1}, {Data: []byte{0x02}, Duration: time.Second, PacketTimestamp: 2}, {Data: []byte{0x03}, Duration: time.Second, PacketTimestamp: 3}, {Data: []byte{0x04}, Duration: time.Second, PacketTimestamp: 4}, {Data: []byte{0x05}, Duration: time.Second, PacketTimestamp: 5}, }, maxLate: 50, maxLateTimestamp: 0, }, { message: "SampleBuilder should skip time stamps too old", packets: []*rtp.Packet{ {Header: rtp.Header{SequenceNumber: 5000, Timestamp: 1}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 5001, Timestamp: 2}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 5002, Timestamp: 3}, Payload: []byte{0x03}}, {Header: rtp.Header{SequenceNumber: 5013, Timestamp: 4000}, Payload: []byte{0x04}}, {Header: rtp.Header{SequenceNumber: 5014, Timestamp: 4000}, Payload: []byte{0x05}}, {Header: rtp.Header{SequenceNumber: 5015, Timestamp: 4002}, Payload: []byte{0x06}}, {Header: rtp.Header{SequenceNumber: 5016, Timestamp: 7000}, Payload: []byte{0x04}}, {Header: rtp.Header{SequenceNumber: 5017, Timestamp: 7001}, Payload: []byte{0x05}}, }, samples: []*media.Sample{ {Data: []byte{0x04, 0x05}, Duration: time.Second * time.Duration(2), PacketTimestamp: 4000, PrevDroppedPackets: 13}, }, withHeadChecker: true, headBytes: []byte{0x04}, maxLate: 50, maxLateTimestamp: 2000, }, } t.Run("Pop", func(t *testing.T) { assert := assert.New(t) for _, t := range testData { var opts []Option if t.maxLateTimestamp != 0 { opts = append(opts, WithMaxTimeDelay( time.Millisecond*time.Duration(int64(t.maxLateTimestamp)), )) } d := &fakeDepacketizer{ headChecker: t.withHeadChecker, headBytes: t.headBytes, } s := New(t.maxLate, d, 1, opts...) samples := []*media.Sample{} for _, p := range t.packets { s.Push(p) } for sample := s.Pop(); sample != nil; sample = s.Pop() { samples = append(samples, sample) } assert.Equal(t.samples, samples, t.message) } }) } // SampleBuilder should respect maxLate if we popped successfully but then have a gap larger then maxLate func TestSampleBuilderMaxLate(t *testing.T) { assert := assert.New(t) s := New(50, &fakeDepacketizer{}, 1) s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 0, Timestamp: 1}, Payload: []byte{0x01}}) s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1, Timestamp: 2}, Payload: []byte{0x01}}) s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 2, Timestamp: 3}, Payload: []byte{0x01}}) assert.Equal(&media.Sample{Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 1}, s.Pop(), "Failed to build samples before gap") s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}}) s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 501}, Payload: []byte{0x02}}) s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 502}, Payload: []byte{0x02}}) assert.Equal(&media.Sample{Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 2}, s.Pop(), "Failed to build samples after large gap") assert.Equal((*media.Sample)(nil), s.Pop(), "Failed to build samples after large gap") s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 6000, Timestamp: 600}, Payload: []byte{0x03}}) assert.Equal(&media.Sample{Data: []byte{0x02}, Duration: time.Second, PacketTimestamp: 500, PrevDroppedPackets: 4998}, s.Pop(), "Failed to build samples after large gap") assert.Equal(&media.Sample{Data: []byte{0x02}, Duration: time.Second, PacketTimestamp: 501}, s.Pop(), "Failed to build samples after large gap") } func TestSeqnumDistance(t *testing.T) { testData := []struct { x uint16 y uint16 d uint16 }{ {0x0001, 0x0003, 0x0002}, {0x0003, 0x0001, 0x0002}, {0xFFF3, 0xFFF1, 0x0002}, {0xFFF1, 0xFFF3, 0x0002}, {0xFFFF, 0x0001, 0x0002}, {0x0001, 0xFFFF, 0x0002}, } for _, data := range testData { if ret := seqnumDistance(data.x, data.y); ret != data.d { t.Errorf("seqnumDistance(%d, %d) returned %d which must be %d", data.x, data.y, ret, data.d) } } } func TestSampleBuilderCleanReference(t *testing.T) { for _, seqStart := range []uint16{ 0, 0xFFF8, // check upper boundary 0xFFFE, // check upper boundary } { seqStart := seqStart t.Run(fmt.Sprintf("From%d", seqStart), func(t *testing.T) { s := New(10, &fakeDepacketizer{}, 1) s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 0 + seqStart, Timestamp: 0}, Payload: []byte{0x01}}) s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1 + seqStart, Timestamp: 0}, Payload: []byte{0x02}}) s.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 2 + seqStart, Timestamp: 0}, Payload: []byte{0x03}}) pkt4 := &rtp.Packet{Header: rtp.Header{SequenceNumber: 14 + seqStart, Timestamp: 120}, Payload: []byte{0x04}} s.Push(pkt4) pkt5 := &rtp.Packet{Header: rtp.Header{SequenceNumber: 12 + seqStart, Timestamp: 120}, Payload: []byte{0x05}} s.Push(pkt5) for i := 0; i < 3; i++ { if s.buffer[(i+int(seqStart))%0x10000] != nil { t.Errorf("Old packet (%d) is not unreferenced (maxLate: 10, pushed: 12)", i) } } if s.buffer[(14+int(seqStart))%0x10000] != pkt4 { t.Error("New packet must be referenced after jump") } if s.buffer[(12+int(seqStart))%0x10000] != pkt5 { t.Error("New packet must be referenced after jump") } }) } } func TestSampleBuilderPushMaxZero(t *testing.T) { // Test packets released via 'maxLate' of zero. pkts := []rtp.Packet{ {Header: rtp.Header{SequenceNumber: 0, Timestamp: 0, Marker: true}, Payload: []byte{0x01}}, } d := &fakeDepacketizer{ headChecker: true, headBytes: []byte{0x01}, } s := New(0, d, 1) s.Push(&pkts[0]) if sample := s.Pop(); sample == nil { t.Error("Should expect a popped sample") } } func TestSampleBuilderWithPacketReleaseHandler(t *testing.T) { var released []*rtp.Packet fakePacketReleaseHandler := func(p *rtp.Packet) { released = append(released, p) } // Test packets released via 'maxLate'. pkts := []rtp.Packet{ {Header: rtp.Header{SequenceNumber: 0, Timestamp: 0}, Payload: []byte{0x01}}, {Header: rtp.Header{SequenceNumber: 11, Timestamp: 120}, Payload: []byte{0x02}}, {Header: rtp.Header{SequenceNumber: 12, Timestamp: 121}, Payload: []byte{0x03}}, {Header: rtp.Header{SequenceNumber: 13, Timestamp: 122}, Payload: []byte{0x04}}, {Header: rtp.Header{SequenceNumber: 21, Timestamp: 200}, Payload: []byte{0x05}}, } s := New(10, &fakeDepacketizer{}, 1, WithPacketReleaseHandler(fakePacketReleaseHandler)) s.Push(&pkts[0]) s.Push(&pkts[1]) if len(released) == 0 { t.Errorf("Old packet is not released") } if len(released) > 0 && released[0].SequenceNumber != pkts[0].SequenceNumber { t.Errorf("Unexpected packet released by maxLate") } // Test packets released after samples built. s.Push(&pkts[2]) s.Push(&pkts[3]) s.Push(&pkts[4]) if s.Pop() == nil { t.Errorf("Should have some sample here.") } if len(released) < 3 { t.Errorf("packet built with sample is not released") } if len(released) >= 2 && released[2].SequenceNumber != pkts[2].SequenceNumber { t.Errorf("Unexpected packet released by samples built") } } func TestPopWithTimestamp(t *testing.T) { t.Run("Crash on nil", func(t *testing.T) { s := New(0, &fakeDepacketizer{}, 1) sample, timestamp := s.PopWithTimestamp() assert.Nil(t, sample) assert.Equal(t, uint32(0), timestamp) }) } type truePartitionHeadChecker struct{} func (f *truePartitionHeadChecker) IsPartitionHead(payload []byte) bool { return true } func TestSampleBuilderData(t *testing.T) { s := New(10, &fakeDepacketizer{}, 1, WithPartitionHeadChecker(&truePartitionHeadChecker{}), ) j := 0 for i := 0; i < 0x20000; i++ { p := rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), Timestamp: uint32(i + 42), }, Payload: []byte{byte(i)}, } s.Push(&p) for { sample, ts := s.PopWithTimestamp() if sample == nil { break } assert.Equal(t, ts, uint32(j+42), "timestamp") assert.Equal(t, len(sample.Data), 1, "data length") assert.Equal(t, byte(j), sample.Data[0], "data") j++ } } // only the last packet should be dropped assert.Equal(t, j, 0x1FFFF) } func BenchmarkSampleBuilderSequential(b *testing.B) { s := New(100, &fakeDepacketizer{}, 1) b.ResetTimer() j := 0 for i := 0; i < b.N; i++ { p := rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), Timestamp: uint32(i + 42), }, Payload: make([]byte, 50), } s.Push(&p) for { s := s.Pop() if s == nil { break } j++ } } if b.N > 200 && j < b.N-100 { b.Errorf("Got %v (N=%v)", j, b.N) } } func BenchmarkSampleBuilderLoss(b *testing.B) { s := New(100, &fakeDepacketizer{}, 1) b.ResetTimer() j := 0 for i := 0; i < b.N; i++ { if i%13 == 0 { continue } p := rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), Timestamp: uint32(i + 42), }, Payload: make([]byte, 50), } s.Push(&p) for { s := s.Pop() if s == nil { break } j++ } } if b.N > 200 && j < b.N/2-100 { b.Errorf("Got %v (N=%v)", j, b.N) } } func BenchmarkSampleBuilderReordered(b *testing.B) { s := New(100, &fakeDepacketizer{}, 1) b.ResetTimer() j := 0 for i := 0; i < b.N; i++ { p := rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i ^ 3), Timestamp: uint32((i ^ 3) + 42), }, Payload: make([]byte, 50), } s.Push(&p) for { s := s.Pop() if s == nil { break } j++ } } if b.N > 2 && j < b.N-5 && j > b.N { b.Errorf("Got %v (N=%v)", j, b.N) } } func BenchmarkSampleBuilderFragmented(b *testing.B) { s := New(100, &fakeDepacketizer{}, 1) b.ResetTimer() j := 0 for i := 0; i < b.N; i++ { p := rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), Timestamp: uint32(i/2 + 42), }, Payload: make([]byte, 50), } s.Push(&p) for { s := s.Pop() if s == nil { break } j++ } } if b.N > 200 && j < b.N/2-100 { b.Errorf("Got %v (N=%v)", j, b.N) } } func BenchmarkSampleBuilderFragmentedLoss(b *testing.B) { s := New(100, &fakeDepacketizer{}, 1) b.ResetTimer() j := 0 for i := 0; i < b.N; i++ { if i%13 == 0 { continue } p := rtp.Packet{ Header: rtp.Header{ SequenceNumber: uint16(i), Timestamp: uint32(i/2 + 42), }, Payload: make([]byte, 50), } s.Push(&p) for { s := s.Pop() if s == nil { break } j++ } } if b.N > 200 && j < b.N/3-100 { b.Errorf("Got %v (N=%v)", j, b.N) } } webrtc-3.1.56/pkg/null/000077500000000000000000000000001437620512100146305ustar00rootroot00000000000000webrtc-3.1.56/pkg/null/null.go000066400000000000000000000106271437620512100161370ustar00rootroot00000000000000// Package null is used to represent values where the 0 value is significant // This pattern is common in ECMAScript, this allows us to maintain a matching API package null // Bool is used to represent a bool that may be null type Bool struct { Valid bool Bool bool } // NewBool turns a bool into a valid null.Bool func NewBool(value bool) Bool { return Bool{Valid: true, Bool: value} } // Byte is used to represent a byte that may be null type Byte struct { Valid bool Byte byte } // NewByte turns a byte into a valid null.Byte func NewByte(value byte) Byte { return Byte{Valid: true, Byte: value} } // Complex128 is used to represent a complex128 that may be null type Complex128 struct { Valid bool Complex128 complex128 } // NewComplex128 turns a complex128 into a valid null.Complex128 func NewComplex128(value complex128) Complex128 { return Complex128{Valid: true, Complex128: value} } // Complex64 is used to represent a complex64 that may be null type Complex64 struct { Valid bool Complex64 complex64 } // NewComplex64 turns a complex64 into a valid null.Complex64 func NewComplex64(value complex64) Complex64 { return Complex64{Valid: true, Complex64: value} } // Float32 is used to represent a float32 that may be null type Float32 struct { Valid bool Float32 float32 } // NewFloat32 turns a float32 into a valid null.Float32 func NewFloat32(value float32) Float32 { return Float32{Valid: true, Float32: value} } // Float64 is used to represent a float64 that may be null type Float64 struct { Valid bool Float64 float64 } // NewFloat64 turns a float64 into a valid null.Float64 func NewFloat64(value float64) Float64 { return Float64{Valid: true, Float64: value} } // Int is used to represent a int that may be null type Int struct { Valid bool Int int } // NewInt turns a int into a valid null.Int func NewInt(value int) Int { return Int{Valid: true, Int: value} } // Int16 is used to represent a int16 that may be null type Int16 struct { Valid bool Int16 int16 } // NewInt16 turns a int16 into a valid null.Int16 func NewInt16(value int16) Int16 { return Int16{Valid: true, Int16: value} } // Int32 is used to represent a int32 that may be null type Int32 struct { Valid bool Int32 int32 } // NewInt32 turns a int32 into a valid null.Int32 func NewInt32(value int32) Int32 { return Int32{Valid: true, Int32: value} } // Int64 is used to represent a int64 that may be null type Int64 struct { Valid bool Int64 int64 } // NewInt64 turns a int64 into a valid null.Int64 func NewInt64(value int64) Int64 { return Int64{Valid: true, Int64: value} } // Int8 is used to represent a int8 that may be null type Int8 struct { Valid bool Int8 int8 } // NewInt8 turns a int8 into a valid null.Int8 func NewInt8(value int8) Int8 { return Int8{Valid: true, Int8: value} } // Rune is used to represent a rune that may be null type Rune struct { Valid bool Rune rune } // NewRune turns a rune into a valid null.Rune func NewRune(value rune) Rune { return Rune{Valid: true, Rune: value} } // String is used to represent a string that may be null type String struct { Valid bool String string } // NewString turns a string into a valid null.String func NewString(value string) String { return String{Valid: true, String: value} } // Uint is used to represent a uint that may be null type Uint struct { Valid bool Uint uint } // NewUint turns a uint into a valid null.Uint func NewUint(value uint) Uint { return Uint{Valid: true, Uint: value} } // Uint16 is used to represent a uint16 that may be null type Uint16 struct { Valid bool Uint16 uint16 } // NewUint16 turns a uint16 into a valid null.Uint16 func NewUint16(value uint16) Uint16 { return Uint16{Valid: true, Uint16: value} } // Uint32 is used to represent a uint32 that may be null type Uint32 struct { Valid bool Uint32 uint32 } // NewUint32 turns a uint32 into a valid null.Uint32 func NewUint32(value uint32) Uint32 { return Uint32{Valid: true, Uint32: value} } // Uint64 is used to represent a uint64 that may be null type Uint64 struct { Valid bool Uint64 uint64 } // NewUint64 turns a uint64 into a valid null.Uint64 func NewUint64(value uint64) Uint64 { return Uint64{Valid: true, Uint64: value} } // Uint8 is used to represent a uint8 that may be null type Uint8 struct { Valid bool Uint8 uint8 } // NewUint8 turns a uint8 into a valid null.Uint8 func NewUint8(value uint8) Uint8 { return Uint8{Valid: true, Uint8: value} } webrtc-3.1.56/pkg/null/null_test.go000066400000000000000000000077251437620512100172030ustar00rootroot00000000000000package null import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewBool(t *testing.T) { value := bool(true) nullable := NewBool(value) assert.Equal(t, true, nullable.Valid, "valid: Bool", ) assert.Equal(t, value, nullable.Bool, "value: Bool", ) } func TestNewByte(t *testing.T) { value := byte('a') nullable := NewByte(value) assert.Equal(t, true, nullable.Valid, "valid: Byte", ) assert.Equal(t, value, nullable.Byte, "value: Byte", ) } func TestNewComplex128(t *testing.T) { value := complex128(-5 + 12i) nullable := NewComplex128(value) assert.Equal(t, true, nullable.Valid, "valid: Complex128", ) assert.Equal(t, value, nullable.Complex128, "value: Complex128", ) } func TestNewComplex64(t *testing.T) { value := complex64(-5 + 12i) nullable := NewComplex64(value) assert.Equal(t, true, nullable.Valid, "valid: Complex64", ) assert.Equal(t, value, nullable.Complex64, "value: Complex64", ) } func TestNewFloat32(t *testing.T) { value := float32(0.5) nullable := NewFloat32(value) assert.Equal(t, true, nullable.Valid, "valid: Float32", ) assert.Equal(t, value, nullable.Float32, "value: Float32", ) } func TestNewFloat64(t *testing.T) { value := float64(0.5) nullable := NewFloat64(value) assert.Equal(t, true, nullable.Valid, "valid: Float64", ) assert.Equal(t, value, nullable.Float64, "value: Float64", ) } func TestNewInt(t *testing.T) { value := int(1) nullable := NewInt(value) assert.Equal(t, true, nullable.Valid, "valid: Int", ) assert.Equal(t, value, nullable.Int, "value: Int", ) } func TestNewInt16(t *testing.T) { value := int16(1) nullable := NewInt16(value) assert.Equal(t, true, nullable.Valid, "valid: Int16", ) assert.Equal(t, value, nullable.Int16, "value: Int16", ) } func TestNewInt32(t *testing.T) { value := int32(1) nullable := NewInt32(value) assert.Equal(t, true, nullable.Valid, "valid: Int32", ) assert.Equal(t, value, nullable.Int32, "value: Int32", ) } func TestNewInt64(t *testing.T) { value := int64(1) nullable := NewInt64(value) assert.Equal(t, true, nullable.Valid, "valid: Int64", ) assert.Equal(t, value, nullable.Int64, "value: Int64", ) } func TestNewInt8(t *testing.T) { value := int8(1) nullable := NewInt8(value) assert.Equal(t, true, nullable.Valid, "valid: Int8", ) assert.Equal(t, value, nullable.Int8, "value: Int8", ) } func TestNewRune(t *testing.T) { value := rune('p') nullable := NewRune(value) assert.Equal(t, true, nullable.Valid, "valid: Rune", ) assert.Equal(t, value, nullable.Rune, "value: Rune", ) } func TestNewString(t *testing.T) { value := string("pion") nullable := NewString(value) assert.Equal(t, true, nullable.Valid, "valid: String", ) assert.Equal(t, value, nullable.String, "value: String", ) } func TestNewUint(t *testing.T) { value := uint(1) nullable := NewUint(value) assert.Equal(t, true, nullable.Valid, "valid: Uint", ) assert.Equal(t, value, nullable.Uint, "value: Uint", ) } func TestNewUint16(t *testing.T) { value := uint16(1) nullable := NewUint16(value) assert.Equal(t, true, nullable.Valid, "valid: Uint16", ) assert.Equal(t, value, nullable.Uint16, "value: Uint16", ) } func TestNewUint32(t *testing.T) { value := uint32(1) nullable := NewUint32(value) assert.Equal(t, true, nullable.Valid, "valid: Uint32", ) assert.Equal(t, value, nullable.Uint32, "value: Uint32", ) } func TestNewUint64(t *testing.T) { value := uint64(1) nullable := NewUint64(value) assert.Equal(t, true, nullable.Valid, "valid: Uint64", ) assert.Equal(t, value, nullable.Uint64, "value: Uint64", ) } func TestNewUint8(t *testing.T) { value := uint8(1) nullable := NewUint8(value) assert.Equal(t, true, nullable.Valid, "valid: Uint8", ) assert.Equal(t, value, nullable.Uint8, "value: Uint8", ) } webrtc-3.1.56/pkg/rtcerr/000077500000000000000000000000001437620512100151575ustar00rootroot00000000000000webrtc-3.1.56/pkg/rtcerr/errors.go000066400000000000000000000105721437620512100170270ustar00rootroot00000000000000// Package rtcerr implements the error wrappers defined throughout the // WebRTC 1.0 specifications. package rtcerr import ( "fmt" ) // UnknownError indicates the operation failed for an unknown transient reason. type UnknownError struct { Err error } func (e *UnknownError) Error() string { return fmt.Sprintf("UnknownError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *UnknownError) Unwrap() error { return e.Err } // InvalidStateError indicates the object is in an invalid state. type InvalidStateError struct { Err error } func (e *InvalidStateError) Error() string { return fmt.Sprintf("InvalidStateError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *InvalidStateError) Unwrap() error { return e.Err } // InvalidAccessError indicates the object does not support the operation or // argument. type InvalidAccessError struct { Err error } func (e *InvalidAccessError) Error() string { return fmt.Sprintf("InvalidAccessError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *InvalidAccessError) Unwrap() error { return e.Err } // NotSupportedError indicates the operation is not supported. type NotSupportedError struct { Err error } func (e *NotSupportedError) Error() string { return fmt.Sprintf("NotSupportedError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *NotSupportedError) Unwrap() error { return e.Err } // InvalidModificationError indicates the object cannot be modified in this way. type InvalidModificationError struct { Err error } func (e *InvalidModificationError) Error() string { return fmt.Sprintf("InvalidModificationError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *InvalidModificationError) Unwrap() error { return e.Err } // SyntaxError indicates the string did not match the expected pattern. type SyntaxError struct { Err error } func (e *SyntaxError) Error() string { return fmt.Sprintf("SyntaxError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *SyntaxError) Unwrap() error { return e.Err } // TypeError indicates an error when a value is not of the expected type. type TypeError struct { Err error } func (e *TypeError) Error() string { return fmt.Sprintf("TypeError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *TypeError) Unwrap() error { return e.Err } // OperationError indicates the operation failed for an operation-specific // reason. type OperationError struct { Err error } func (e *OperationError) Error() string { return fmt.Sprintf("OperationError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *OperationError) Unwrap() error { return e.Err } // NotReadableError indicates the input/output read operation failed. type NotReadableError struct { Err error } func (e *NotReadableError) Error() string { return fmt.Sprintf("NotReadableError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *NotReadableError) Unwrap() error { return e.Err } // RangeError indicates an error when a value is not in the set or range // of allowed values. type RangeError struct { Err error } func (e *RangeError) Error() string { return fmt.Sprintf("RangeError: %v", e.Err) } // Unwrap returns the result of calling the Unwrap method on err, if err's type contains // an Unwrap method returning error. Otherwise, Unwrap returns nil. func (e *RangeError) Unwrap() error { return e.Err } webrtc-3.1.56/renovate.json000066400000000000000000000001731437620512100156140ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>pion/renovate-config" ] } webrtc-3.1.56/rtcpfeedback.go000066400000000000000000000013551437620512100160450ustar00rootroot00000000000000package webrtc const ( // TypeRTCPFBTransportCC .. TypeRTCPFBTransportCC = "transport-cc" // TypeRTCPFBGoogREMB .. TypeRTCPFBGoogREMB = "goog-remb" // TypeRTCPFBACK .. TypeRTCPFBACK = "ack" // TypeRTCPFBCCM .. TypeRTCPFBCCM = "ccm" // TypeRTCPFBNACK .. TypeRTCPFBNACK = "nack" ) // RTCPFeedback signals the connection to use additional RTCP packet types. // https://draft.ortc.org/#dom-rtcrtcpfeedback type RTCPFeedback struct { // Type is the type of feedback. // see: https://draft.ortc.org/#dom-rtcrtcpfeedback // valid: ack, ccm, nack, goog-remb, transport-cc Type string // The parameter value depends on the type. // For example, type="nack" parameter="pli" will send Picture Loss Indicator packets. Parameter string } webrtc-3.1.56/rtcpmuxpolicy.go000066400000000000000000000032421437620512100163470ustar00rootroot00000000000000package webrtc import ( "encoding/json" ) // RTCPMuxPolicy affects what ICE candidates are gathered to support // non-multiplexed RTCP. type RTCPMuxPolicy int const ( // RTCPMuxPolicyNegotiate indicates to gather ICE candidates for both // RTP and RTCP candidates. If the remote-endpoint is capable of // multiplexing RTCP, multiplex RTCP on the RTP candidates. If it is not, // use both the RTP and RTCP candidates separately. RTCPMuxPolicyNegotiate RTCPMuxPolicy = iota + 1 // RTCPMuxPolicyRequire indicates to gather ICE candidates only for // RTP and multiplex RTCP on the RTP candidates. If the remote endpoint is // not capable of rtcp-mux, session negotiation will fail. RTCPMuxPolicyRequire ) // This is done this way because of a linter. const ( rtcpMuxPolicyNegotiateStr = "negotiate" rtcpMuxPolicyRequireStr = "require" ) func newRTCPMuxPolicy(raw string) RTCPMuxPolicy { switch raw { case rtcpMuxPolicyNegotiateStr: return RTCPMuxPolicyNegotiate case rtcpMuxPolicyRequireStr: return RTCPMuxPolicyRequire default: return RTCPMuxPolicy(Unknown) } } func (t RTCPMuxPolicy) String() string { switch t { case RTCPMuxPolicyNegotiate: return rtcpMuxPolicyNegotiateStr case RTCPMuxPolicyRequire: return rtcpMuxPolicyRequireStr default: return ErrUnknownType.Error() } } // UnmarshalJSON parses the JSON-encoded data and stores the result func (t *RTCPMuxPolicy) UnmarshalJSON(b []byte) error { var val string if err := json.Unmarshal(b, &val); err != nil { return err } *t = newRTCPMuxPolicy(val) return nil } // MarshalJSON returns the JSON encoding func (t RTCPMuxPolicy) MarshalJSON() ([]byte, error) { return json.Marshal(t.String()) } webrtc-3.1.56/rtcpmuxpolicy_test.go000066400000000000000000000016141437620512100174070ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewRTCPMuxPolicy(t *testing.T) { testCases := []struct { policyString string expectedPolicy RTCPMuxPolicy }{ {unknownStr, RTCPMuxPolicy(Unknown)}, {"negotiate", RTCPMuxPolicyNegotiate}, {"require", RTCPMuxPolicyRequire}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedPolicy, newRTCPMuxPolicy(testCase.policyString), "testCase: %d %v", i, testCase, ) } } func TestRTCPMuxPolicy_String(t *testing.T) { testCases := []struct { policy RTCPMuxPolicy expectedString string }{ {RTCPMuxPolicy(Unknown), unknownStr}, {RTCPMuxPolicyNegotiate, "negotiate"}, {RTCPMuxPolicyRequire, "require"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.policy.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/rtpcapabilities.go000066400000000000000000000004021437620512100165770ustar00rootroot00000000000000package webrtc // RTPCapabilities represents the capabilities of a transceiver // // https://w3c.github.io/webrtc-pc/#rtcrtpcapabilities type RTPCapabilities struct { Codecs []RTPCodecCapability HeaderExtensions []RTPHeaderExtensionCapability } webrtc-3.1.56/rtpcodec.go000066400000000000000000000063701437620512100152350ustar00rootroot00000000000000package webrtc import ( "strings" "github.com/pion/webrtc/v3/internal/fmtp" ) // RTPCodecType determines the type of a codec type RTPCodecType int const ( // RTPCodecTypeAudio indicates this is an audio codec RTPCodecTypeAudio RTPCodecType = iota + 1 // RTPCodecTypeVideo indicates this is a video codec RTPCodecTypeVideo ) func (t RTPCodecType) String() string { switch t { case RTPCodecTypeAudio: return "audio" case RTPCodecTypeVideo: return "video" //nolint: goconst default: return ErrUnknownType.Error() } } // NewRTPCodecType creates a RTPCodecType from a string func NewRTPCodecType(r string) RTPCodecType { switch { case strings.EqualFold(r, RTPCodecTypeAudio.String()): return RTPCodecTypeAudio case strings.EqualFold(r, RTPCodecTypeVideo.String()): return RTPCodecTypeVideo default: return RTPCodecType(0) } } // RTPCodecCapability provides information about codec capabilities. // // https://w3c.github.io/webrtc-pc/#dictionary-rtcrtpcodeccapability-members type RTPCodecCapability struct { MimeType string ClockRate uint32 Channels uint16 SDPFmtpLine string RTCPFeedback []RTCPFeedback } // RTPHeaderExtensionCapability is used to define a RFC5285 RTP header extension supported by the codec. // // https://w3c.github.io/webrtc-pc/#dom-rtcrtpcapabilities-headerextensions type RTPHeaderExtensionCapability struct { URI string } // RTPHeaderExtensionParameter represents a negotiated RFC5285 RTP header extension. // // https://w3c.github.io/webrtc-pc/#dictionary-rtcrtpheaderextensionparameters-members type RTPHeaderExtensionParameter struct { URI string ID int } // RTPCodecParameters is a sequence containing the media codecs that an RtpSender // will choose from, as well as entries for RTX, RED and FEC mechanisms. This also // includes the PayloadType that has been negotiated // // https://w3c.github.io/webrtc-pc/#rtcrtpcodecparameters type RTPCodecParameters struct { RTPCodecCapability PayloadType PayloadType statsID string } // RTPParameters is a list of negotiated codecs and header extensions // // https://w3c.github.io/webrtc-pc/#dictionary-rtcrtpparameters-members type RTPParameters struct { HeaderExtensions []RTPHeaderExtensionParameter Codecs []RTPCodecParameters } type codecMatchType int const ( codecMatchNone codecMatchType = 0 codecMatchPartial codecMatchType = 1 codecMatchExact codecMatchType = 2 ) // Do a fuzzy find for a codec in the list of codecs // Used for lookup up a codec in an existing list to find a match // Returns codecMatchExact, codecMatchPartial, or codecMatchNone func codecParametersFuzzySearch(needle RTPCodecParameters, haystack []RTPCodecParameters) (RTPCodecParameters, codecMatchType) { needleFmtp := fmtp.Parse(needle.RTPCodecCapability.MimeType, needle.RTPCodecCapability.SDPFmtpLine) // First attempt to match on MimeType + SDPFmtpLine for _, c := range haystack { cfmtp := fmtp.Parse(c.RTPCodecCapability.MimeType, c.RTPCodecCapability.SDPFmtpLine) if needleFmtp.Match(cfmtp) { return c, codecMatchExact } } // Fallback to just MimeType for _, c := range haystack { if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) { return c, codecMatchPartial } } return RTPCodecParameters{}, codecMatchNone } webrtc-3.1.56/rtpcodingparameters.go000066400000000000000000000012331437620512100175000ustar00rootroot00000000000000package webrtc // RTPRtxParameters dictionary contains information relating to retransmission (RTX) settings. // https://draft.ortc.org/#dom-rtcrtprtxparameters type RTPRtxParameters struct { SSRC SSRC `json:"ssrc"` } // RTPCodingParameters provides information relating to both encoding and decoding. // This is a subset of the RFC since Pion WebRTC doesn't implement encoding/decoding itself // http://draft.ortc.org/#dom-rtcrtpcodingparameters type RTPCodingParameters struct { RID string `json:"rid"` SSRC SSRC `json:"ssrc"` PayloadType PayloadType `json:"payloadType"` RTX RTPRtxParameters `json:"rtx"` } webrtc-3.1.56/rtpdecodingparameters.go000066400000000000000000000004531437620512100200140ustar00rootroot00000000000000package webrtc // RTPDecodingParameters provides information relating to both encoding and decoding. // This is a subset of the RFC since Pion WebRTC doesn't implement decoding itself // http://draft.ortc.org/#dom-rtcrtpdecodingparameters type RTPDecodingParameters struct { RTPCodingParameters } webrtc-3.1.56/rtpencodingparameters.go000066400000000000000000000004531437620512100200260ustar00rootroot00000000000000package webrtc // RTPEncodingParameters provides information relating to both encoding and decoding. // This is a subset of the RFC since Pion WebRTC doesn't implement encoding itself // http://draft.ortc.org/#dom-rtcrtpencodingparameters type RTPEncodingParameters struct { RTPCodingParameters } webrtc-3.1.56/rtpreceiveparameters.go000066400000000000000000000002421437620512100176560ustar00rootroot00000000000000package webrtc // RTPReceiveParameters contains the RTP stack settings used by receivers type RTPReceiveParameters struct { Encodings []RTPDecodingParameters } webrtc-3.1.56/rtpreceiver.go000066400000000000000000000274401437620512100157650ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "fmt" "io" "sync" "time" "github.com/pion/interceptor" "github.com/pion/rtcp" "github.com/pion/srtp/v2" "github.com/pion/webrtc/v3/internal/util" ) // trackStreams maintains a mapping of RTP/RTCP streams to a specific track // a RTPReceiver may contain multiple streams if we are dealing with Simulcast type trackStreams struct { track *TrackRemote streamInfo, repairStreamInfo *interceptor.StreamInfo rtpReadStream *srtp.ReadStreamSRTP rtpInterceptor interceptor.RTPReader rtcpReadStream *srtp.ReadStreamSRTCP rtcpInterceptor interceptor.RTCPReader repairReadStream *srtp.ReadStreamSRTP repairInterceptor interceptor.RTPReader repairRtcpReadStream *srtp.ReadStreamSRTCP repairRtcpInterceptor interceptor.RTCPReader } // RTPReceiver allows an application to inspect the receipt of a TrackRemote type RTPReceiver struct { kind RTPCodecType transport *DTLSTransport tracks []trackStreams closed, received chan interface{} mu sync.RWMutex tr *RTPTransceiver // A reference to the associated api object api *API } // NewRTPReceiver constructs a new RTPReceiver func (api *API) NewRTPReceiver(kind RTPCodecType, transport *DTLSTransport) (*RTPReceiver, error) { if transport == nil { return nil, errRTPReceiverDTLSTransportNil } r := &RTPReceiver{ kind: kind, transport: transport, api: api, closed: make(chan interface{}), received: make(chan interface{}), tracks: []trackStreams{}, } return r, nil } func (r *RTPReceiver) setRTPTransceiver(tr *RTPTransceiver) { r.mu.Lock() defer r.mu.Unlock() r.tr = tr } // Transport returns the currently-configured *DTLSTransport or nil // if one has not yet been configured func (r *RTPReceiver) Transport() *DTLSTransport { r.mu.RLock() defer r.mu.RUnlock() return r.transport } func (r *RTPReceiver) getParameters() RTPParameters { parameters := r.api.mediaEngine.getRTPParametersByKind(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}) if r.tr != nil { parameters.Codecs = r.tr.getCodecs() } return parameters } // GetParameters describes the current configuration for the encoding and // transmission of media on the receiver's track. func (r *RTPReceiver) GetParameters() RTPParameters { r.mu.RLock() defer r.mu.RUnlock() return r.getParameters() } // Track returns the RtpTransceiver TrackRemote func (r *RTPReceiver) Track() *TrackRemote { r.mu.RLock() defer r.mu.RUnlock() if len(r.tracks) != 1 { return nil } return r.tracks[0].track } // Tracks returns the RtpTransceiver tracks // A RTPReceiver to support Simulcast may now have multiple tracks func (r *RTPReceiver) Tracks() []*TrackRemote { r.mu.RLock() defer r.mu.RUnlock() var tracks []*TrackRemote for i := range r.tracks { tracks = append(tracks, r.tracks[i].track) } return tracks } // configureReceive initialize the track func (r *RTPReceiver) configureReceive(parameters RTPReceiveParameters) { r.mu.Lock() defer r.mu.Unlock() for i := range parameters.Encodings { t := trackStreams{ track: newTrackRemote( r.kind, parameters.Encodings[i].SSRC, parameters.Encodings[i].RID, r, ), } r.tracks = append(r.tracks, t) } } // startReceive starts all the transports func (r *RTPReceiver) startReceive(parameters RTPReceiveParameters) error { r.mu.Lock() defer r.mu.Unlock() select { case <-r.received: return errRTPReceiverReceiveAlreadyCalled default: } defer close(r.received) globalParams := r.getParameters() codec := RTPCodecCapability{} if len(globalParams.Codecs) != 0 { codec = globalParams.Codecs[0].RTPCodecCapability } for i := range parameters.Encodings { if parameters.Encodings[i].RID != "" { // RID based tracks will be set up in receiveForRid continue } var t *trackStreams for idx, ts := range r.tracks { if ts.track != nil && parameters.Encodings[i].SSRC != 0 && ts.track.SSRC() == parameters.Encodings[i].SSRC { t = &r.tracks[idx] break } } if t == nil { return fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, parameters.Encodings[i].SSRC) } if parameters.Encodings[i].SSRC != 0 { t.streamInfo = createStreamInfo("", parameters.Encodings[i].SSRC, 0, codec, globalParams.HeaderExtensions) var err error if t.rtpReadStream, t.rtpInterceptor, t.rtcpReadStream, t.rtcpInterceptor, err = r.transport.streamsForSSRC(parameters.Encodings[i].SSRC, *t.streamInfo); err != nil { return err } } if rtxSsrc := parameters.Encodings[i].RTX.SSRC; rtxSsrc != 0 { streamInfo := createStreamInfo("", rtxSsrc, 0, codec, globalParams.HeaderExtensions) rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor, err := r.transport.streamsForSSRC(rtxSsrc, *streamInfo) if err != nil { return err } if err = r.receiveForRtx(rtxSsrc, "", streamInfo, rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor); err != nil { return err } } } return nil } // Receive initialize the track and starts all the transports func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error { r.configureReceive(parameters) return r.startReceive(parameters) } // Read reads incoming RTCP for this RTPReceiver func (r *RTPReceiver) Read(b []byte) (n int, a interceptor.Attributes, err error) { select { case <-r.received: return r.tracks[0].rtcpInterceptor.Read(b, a) case <-r.closed: return 0, nil, io.ErrClosedPipe } } // ReadSimulcast reads incoming RTCP for this RTPReceiver for given rid func (r *RTPReceiver) ReadSimulcast(b []byte, rid string) (n int, a interceptor.Attributes, err error) { select { case <-r.received: for _, t := range r.tracks { if t.track != nil && t.track.rid == rid { return t.rtcpInterceptor.Read(b, a) } } return 0, nil, fmt.Errorf("%w: %s", errRTPReceiverForRIDTrackStreamNotFound, rid) case <-r.closed: return 0, nil, io.ErrClosedPipe } } // ReadRTCP is a convenience method that wraps Read and unmarshal for you. // It also runs any configured interceptors. func (r *RTPReceiver) ReadRTCP() ([]rtcp.Packet, interceptor.Attributes, error) { b := make([]byte, r.api.settingEngine.getReceiveMTU()) i, attributes, err := r.Read(b) if err != nil { return nil, nil, err } pkts, err := rtcp.Unmarshal(b[:i]) if err != nil { return nil, nil, err } return pkts, attributes, nil } // ReadSimulcastRTCP is a convenience method that wraps ReadSimulcast and unmarshal for you func (r *RTPReceiver) ReadSimulcastRTCP(rid string) ([]rtcp.Packet, interceptor.Attributes, error) { b := make([]byte, r.api.settingEngine.getReceiveMTU()) i, attributes, err := r.ReadSimulcast(b, rid) if err != nil { return nil, nil, err } pkts, err := rtcp.Unmarshal(b[:i]) return pkts, attributes, err } func (r *RTPReceiver) haveReceived() bool { select { case <-r.received: return true default: return false } } // Stop irreversibly stops the RTPReceiver func (r *RTPReceiver) Stop() error { r.mu.Lock() defer r.mu.Unlock() var err error select { case <-r.closed: return err default: } select { case <-r.received: for i := range r.tracks { errs := []error{} if r.tracks[i].rtcpReadStream != nil { errs = append(errs, r.tracks[i].rtcpReadStream.Close()) } if r.tracks[i].rtpReadStream != nil { errs = append(errs, r.tracks[i].rtpReadStream.Close()) } if r.tracks[i].repairReadStream != nil { errs = append(errs, r.tracks[i].repairReadStream.Close()) } if r.tracks[i].repairRtcpReadStream != nil { errs = append(errs, r.tracks[i].repairRtcpReadStream.Close()) } if r.tracks[i].streamInfo != nil { r.api.interceptor.UnbindRemoteStream(r.tracks[i].streamInfo) } if r.tracks[i].repairStreamInfo != nil { r.api.interceptor.UnbindRemoteStream(r.tracks[i].repairStreamInfo) } err = util.FlattenErrs(errs) } default: } close(r.closed) return err } func (r *RTPReceiver) streamsForTrack(t *TrackRemote) *trackStreams { for i := range r.tracks { if r.tracks[i].track == t { return &r.tracks[i] } } return nil } // readRTP should only be called by a track, this only exists so we can keep state in one place func (r *RTPReceiver) readRTP(b []byte, reader *TrackRemote) (n int, a interceptor.Attributes, err error) { <-r.received if t := r.streamsForTrack(reader); t != nil { return t.rtpInterceptor.Read(b, a) } return 0, nil, fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, reader.SSRC()) } // receiveForRid is the sibling of Receive expect for RIDs instead of SSRCs // It populates all the internal state for the given RID func (r *RTPReceiver) receiveForRid(rid string, params RTPParameters, streamInfo *interceptor.StreamInfo, rtpReadStream *srtp.ReadStreamSRTP, rtpInterceptor interceptor.RTPReader, rtcpReadStream *srtp.ReadStreamSRTCP, rtcpInterceptor interceptor.RTCPReader) (*TrackRemote, error) { r.mu.Lock() defer r.mu.Unlock() for i := range r.tracks { if r.tracks[i].track.RID() == rid { r.tracks[i].track.mu.Lock() r.tracks[i].track.kind = r.kind r.tracks[i].track.codec = params.Codecs[0] r.tracks[i].track.params = params r.tracks[i].track.ssrc = SSRC(streamInfo.SSRC) r.tracks[i].track.mu.Unlock() r.tracks[i].streamInfo = streamInfo r.tracks[i].rtpReadStream = rtpReadStream r.tracks[i].rtpInterceptor = rtpInterceptor r.tracks[i].rtcpReadStream = rtcpReadStream r.tracks[i].rtcpInterceptor = rtcpInterceptor return r.tracks[i].track, nil } } return nil, fmt.Errorf("%w: %s", errRTPReceiverForRIDTrackStreamNotFound, rid) } // receiveForRtx starts a routine that processes the repair stream // These packets aren't exposed to the user yet, but we need to process them for // TWCC func (r *RTPReceiver) receiveForRtx(ssrc SSRC, rsid string, streamInfo *interceptor.StreamInfo, rtpReadStream *srtp.ReadStreamSRTP, rtpInterceptor interceptor.RTPReader, rtcpReadStream *srtp.ReadStreamSRTCP, rtcpInterceptor interceptor.RTCPReader) error { var track *trackStreams if ssrc != 0 && len(r.tracks) == 1 { track = &r.tracks[0] } else { for i := range r.tracks { if r.tracks[i].track.RID() == rsid { track = &r.tracks[i] } } } if track == nil { return fmt.Errorf("%w: ssrc(%d) rsid(%s)", errRTPReceiverForRIDTrackStreamNotFound, ssrc, rsid) } track.repairStreamInfo = streamInfo track.repairReadStream = rtpReadStream track.repairInterceptor = rtpInterceptor track.repairRtcpReadStream = rtcpReadStream track.repairRtcpInterceptor = rtcpInterceptor go func() { b := make([]byte, r.api.settingEngine.getReceiveMTU()) for { if _, _, readErr := track.repairInterceptor.Read(b, nil); readErr != nil { return } } }() return nil } // SetReadDeadline sets the max amount of time the RTCP stream will block before returning. 0 is forever. func (r *RTPReceiver) SetReadDeadline(t time.Time) error { r.mu.RLock() defer r.mu.RUnlock() if err := r.tracks[0].rtcpReadStream.SetReadDeadline(t); err != nil { return err } return nil } // SetReadDeadlineSimulcast sets the max amount of time the RTCP stream for a given rid will block before returning. 0 is forever. func (r *RTPReceiver) SetReadDeadlineSimulcast(deadline time.Time, rid string) error { r.mu.RLock() defer r.mu.RUnlock() for _, t := range r.tracks { if t.track != nil && t.track.rid == rid { return t.rtcpReadStream.SetReadDeadline(deadline) } } return fmt.Errorf("%w: %s", errRTPReceiverForRIDTrackStreamNotFound, rid) } // setRTPReadDeadline sets the max amount of time the RTP stream will block before returning. 0 is forever. // This should be fired by calling SetReadDeadline on the TrackRemote func (r *RTPReceiver) setRTPReadDeadline(deadline time.Time, reader *TrackRemote) error { r.mu.RLock() defer r.mu.RUnlock() if t := r.streamsForTrack(reader); t != nil { return t.rtpReadStream.SetReadDeadline(deadline) } return fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, reader.SSRC()) } webrtc-3.1.56/rtpreceiver_go.go000066400000000000000000000016771437620512100164560ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import "github.com/pion/interceptor" // SetRTPParameters applies provided RTPParameters the RTPReceiver's tracks. // // This method is part of the ORTC API. It is not // meant to be used together with the basic WebRTC API. // // The amount of provided codecs must match the number of tracks on the receiver. func (r *RTPReceiver) SetRTPParameters(params RTPParameters) { headerExtensions := make([]interceptor.RTPHeaderExtension, 0, len(params.HeaderExtensions)) for _, h := range params.HeaderExtensions { headerExtensions = append(headerExtensions, interceptor.RTPHeaderExtension{ID: h.ID, URI: h.URI}) } r.mu.Lock() defer r.mu.Unlock() for ndx, codec := range params.Codecs { currentTrack := r.tracks[ndx].track r.tracks[ndx].streamInfo.RTPHeaderExtensions = headerExtensions currentTrack.mu.Lock() currentTrack.codec = codec currentTrack.params = params currentTrack.mu.Unlock() } } webrtc-3.1.56/rtpreceiver_go_test.go000066400000000000000000000042451437620512100175070ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "context" "testing" "time" "github.com/pion/webrtc/v3/pkg/media" "github.com/stretchr/testify/assert" ) func TestSetRTPParameters(t *testing.T) { sender, receiver, wan := createVNetPair(t) outgoingTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = sender.AddTrack(outgoingTrack) assert.NoError(t, err) // Those parameters wouldn't make sense in a real application, // but for the sake of the test we just need different values. p := RTPParameters{ Codecs: []RTPCodecParameters{ { RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", []RTCPFeedback{{"nack", ""}}}, PayloadType: 111, }, }, HeaderExtensions: []RTPHeaderExtensionParameter{ {URI: "urn:ietf:params:rtp-hdrext:sdes:mid"}, {URI: "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"}, {URI: "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"}, }, } seenPacket, seenPacketCancel := context.WithCancel(context.Background()) receiver.OnTrack(func(trackRemote *TrackRemote, r *RTPReceiver) { r.SetRTPParameters(p) incomingTrackCodecs := r.Track().Codec() assert.EqualValues(t, p.HeaderExtensions, r.Track().params.HeaderExtensions) assert.EqualValues(t, p.Codecs[0].MimeType, incomingTrackCodecs.MimeType) assert.EqualValues(t, p.Codecs[0].ClockRate, incomingTrackCodecs.ClockRate) assert.EqualValues(t, p.Codecs[0].Channels, incomingTrackCodecs.Channels) assert.EqualValues(t, p.Codecs[0].SDPFmtpLine, incomingTrackCodecs.SDPFmtpLine) assert.EqualValues(t, p.Codecs[0].RTCPFeedback, incomingTrackCodecs.RTCPFeedback) assert.EqualValues(t, p.Codecs[0].PayloadType, incomingTrackCodecs.PayloadType) seenPacketCancel() }) peerConnectionsConnected := untilConnectionState(PeerConnectionStateConnected, sender, receiver) assert.NoError(t, signalPair(sender, receiver)) peerConnectionsConnected.Wait() assert.NoError(t, outgoingTrack.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second})) <-seenPacket.Done() assert.NoError(t, wan.Stop()) closePairNow(t, sender, receiver) } webrtc-3.1.56/rtpreceiver_js.go000066400000000000000000000004141437620512100164510ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import "syscall/js" // RTPReceiver allows an application to inspect the receipt of a TrackRemote type RTPReceiver struct { // Pointer to the underlying JavaScript RTCRTPReceiver object. underlying js.Value } webrtc-3.1.56/rtpreceiver_test.go000066400000000000000000000032211437620512100170130ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "context" "testing" "time" "github.com/pion/transport/v2/test" "github.com/pion/webrtc/v3/pkg/media" "github.com/stretchr/testify/assert" ) // Assert that SetReadDeadline works as expected // This test uses VNet since we must have zero loss func Test_RTPReceiver_SetReadDeadline(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() sender, receiver, wan := createVNetPair(t) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = sender.AddTrack(track) assert.NoError(t, err) seenPacket, seenPacketCancel := context.WithCancel(context.Background()) receiver.OnTrack(func(trackRemote *TrackRemote, r *RTPReceiver) { // Set Deadline for both RTP and RTCP Stream assert.NoError(t, r.SetReadDeadline(time.Now().Add(time.Second))) assert.NoError(t, trackRemote.SetReadDeadline(time.Now().Add(time.Second))) // First call will not error because we cache for probing _, _, readErr := trackRemote.ReadRTP() assert.NoError(t, readErr) _, _, readErr = trackRemote.ReadRTP() assert.Error(t, readErr) _, _, readErr = r.ReadRTCP() assert.Error(t, readErr) seenPacketCancel() }) peerConnectionsConnected := untilConnectionState(PeerConnectionStateConnected, sender, receiver) assert.NoError(t, signalPair(sender, receiver)) peerConnectionsConnected.Wait() assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second})) <-seenPacket.Done() assert.NoError(t, wan.Stop()) closePairNow(t, sender, receiver) } webrtc-3.1.56/rtpsender.go000066400000000000000000000261001437620512100154310ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "fmt" "io" "sync" "time" "github.com/pion/interceptor" "github.com/pion/randutil" "github.com/pion/rtcp" "github.com/pion/rtp" "github.com/pion/webrtc/v3/internal/util" ) type trackEncoding struct { track TrackLocal srtpStream *srtpWriterFuture rtcpInterceptor interceptor.RTCPReader streamInfo interceptor.StreamInfo context TrackLocalContext ssrc SSRC } // RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer type RTPSender struct { trackEncodings []*trackEncoding transport *DTLSTransport payloadType PayloadType kind RTPCodecType // nolint:godox // TODO(sgotti) remove this when in future we'll avoid replacing // a transceiver sender since we can just check the // transceiver negotiation status negotiated bool // A reference to the associated api object api *API id string rtpTransceiver *RTPTransceiver mu sync.RWMutex sendCalled, stopCalled chan struct{} } // NewRTPSender constructs a new RTPSender func (api *API) NewRTPSender(track TrackLocal, transport *DTLSTransport) (*RTPSender, error) { if track == nil { return nil, errRTPSenderTrackNil } else if transport == nil { return nil, errRTPSenderDTLSTransportNil } id, err := randutil.GenerateCryptoRandomString(32, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") if err != nil { return nil, err } r := &RTPSender{ transport: transport, api: api, sendCalled: make(chan struct{}), stopCalled: make(chan struct{}), id: id, kind: track.Kind(), } r.addEncoding(track) return r, nil } func (r *RTPSender) isNegotiated() bool { r.mu.RLock() defer r.mu.RUnlock() return r.negotiated } func (r *RTPSender) setNegotiated() { r.mu.Lock() defer r.mu.Unlock() r.negotiated = true } func (r *RTPSender) setRTPTransceiver(rtpTransceiver *RTPTransceiver) { r.mu.Lock() defer r.mu.Unlock() r.rtpTransceiver = rtpTransceiver } // Transport returns the currently-configured *DTLSTransport or nil // if one has not yet been configured func (r *RTPSender) Transport() *DTLSTransport { r.mu.RLock() defer r.mu.RUnlock() return r.transport } func (r *RTPSender) getParameters() RTPSendParameters { var encodings []RTPEncodingParameters for _, trackEncoding := range r.trackEncodings { var rid string if trackEncoding.track != nil { rid = trackEncoding.track.RID() } encodings = append(encodings, RTPEncodingParameters{ RTPCodingParameters: RTPCodingParameters{ RID: rid, SSRC: trackEncoding.ssrc, PayloadType: r.payloadType, }, }) } sendParameters := RTPSendParameters{ RTPParameters: r.api.mediaEngine.getRTPParametersByKind( r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}, ), Encodings: encodings, } if r.rtpTransceiver != nil { sendParameters.Codecs = r.rtpTransceiver.getCodecs() } else { sendParameters.Codecs = r.api.mediaEngine.getCodecsByKind(r.kind) } return sendParameters } // GetParameters describes the current configuration for the encoding and // transmission of media on the sender's track. func (r *RTPSender) GetParameters() RTPSendParameters { r.mu.RLock() defer r.mu.RUnlock() return r.getParameters() } // AddEncoding adds an encoding to RTPSender. Used by simulcast senders. func (r *RTPSender) AddEncoding(track TrackLocal) error { r.mu.Lock() defer r.mu.Unlock() if track == nil { return errRTPSenderTrackNil } if track.RID() == "" { return errRTPSenderRidNil } if r.hasStopped() { return errRTPSenderStopped } if r.hasSent() { return errRTPSenderSendAlreadyCalled } var refTrack TrackLocal if len(r.trackEncodings) != 0 { refTrack = r.trackEncodings[0].track } if refTrack == nil || refTrack.RID() == "" { return errRTPSenderNoBaseEncoding } if refTrack.ID() != track.ID() || refTrack.StreamID() != track.StreamID() || refTrack.Kind() != track.Kind() { return errRTPSenderBaseEncodingMismatch } for _, encoding := range r.trackEncodings { if encoding.track == nil { continue } if encoding.track.RID() == track.RID() { return errRTPSenderRIDCollision } } r.addEncoding(track) return nil } func (r *RTPSender) addEncoding(track TrackLocal) { ssrc := SSRC(randutil.NewMathRandomGenerator().Uint32()) trackEncoding := &trackEncoding{ track: track, srtpStream: &srtpWriterFuture{ssrc: ssrc}, ssrc: ssrc, } trackEncoding.srtpStream.rtpSender = r trackEncoding.rtcpInterceptor = r.api.interceptor.BindRTCPReader( interceptor.RTPReaderFunc(func(in []byte, a interceptor.Attributes) (n int, attributes interceptor.Attributes, err error) { n, err = trackEncoding.srtpStream.Read(in) return n, a, err }), ) r.trackEncodings = append(r.trackEncodings, trackEncoding) } // Track returns the RTCRtpTransceiver track, or nil func (r *RTPSender) Track() TrackLocal { r.mu.RLock() defer r.mu.RUnlock() if len(r.trackEncodings) == 0 { return nil } return r.trackEncodings[0].track } // ReplaceTrack replaces the track currently being used as the sender's source with a new TrackLocal. // The new track must be of the same media kind (audio, video, etc) and switching the track should not // require negotiation. func (r *RTPSender) ReplaceTrack(track TrackLocal) error { r.mu.Lock() defer r.mu.Unlock() if track != nil && r.kind != track.Kind() { return ErrRTPSenderNewTrackHasIncorrectKind } // cannot replace simulcast envelope if track != nil && len(r.trackEncodings) > 1 { return ErrRTPSenderNewTrackHasIncorrectEnvelope } var replacedTrack TrackLocal var context *TrackLocalContext if len(r.trackEncodings) != 0 { replacedTrack = r.trackEncodings[0].track context = &r.trackEncodings[0].context } if r.hasSent() && replacedTrack != nil { if err := replacedTrack.Unbind(*context); err != nil { return err } } if !r.hasSent() || track == nil { r.trackEncodings[0].track = track return nil } codec, err := track.Bind(TrackLocalContext{ id: context.id, params: r.api.mediaEngine.getRTPParametersByKind(track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}), ssrc: context.ssrc, writeStream: context.writeStream, rtcpInterceptor: context.rtcpInterceptor, }) if err != nil { // Re-bind the original track if _, reBindErr := replacedTrack.Bind(*context); reBindErr != nil { return reBindErr } return err } // Codec has changed if r.payloadType != codec.PayloadType { context.params.Codecs = []RTPCodecParameters{codec} } r.trackEncodings[0].track = track return nil } // Send Attempts to set the parameters controlling the sending of media. func (r *RTPSender) Send(parameters RTPSendParameters) error { r.mu.Lock() defer r.mu.Unlock() switch { case r.hasSent(): return errRTPSenderSendAlreadyCalled case r.trackEncodings[0].track == nil: return errRTPSenderTrackRemoved } for idx, trackEncoding := range r.trackEncodings { writeStream := &interceptorToTrackLocalWriter{} trackEncoding.context = TrackLocalContext{ id: r.id, params: r.api.mediaEngine.getRTPParametersByKind(trackEncoding.track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}), ssrc: parameters.Encodings[idx].SSRC, writeStream: writeStream, rtcpInterceptor: trackEncoding.rtcpInterceptor, } codec, err := trackEncoding.track.Bind(trackEncoding.context) if err != nil { return err } trackEncoding.context.params.Codecs = []RTPCodecParameters{codec} trackEncoding.streamInfo = *createStreamInfo( r.id, parameters.Encodings[idx].SSRC, codec.PayloadType, codec.RTPCodecCapability, parameters.HeaderExtensions, ) srtpStream := trackEncoding.srtpStream rtpInterceptor := r.api.interceptor.BindLocalStream( &trackEncoding.streamInfo, interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) { return srtpStream.WriteRTP(header, payload) }), ) writeStream.interceptor.Store(rtpInterceptor) } close(r.sendCalled) return nil } // Stop irreversibly stops the RTPSender func (r *RTPSender) Stop() error { r.mu.Lock() if stopped := r.hasStopped(); stopped { r.mu.Unlock() return nil } close(r.stopCalled) r.mu.Unlock() if !r.hasSent() { return nil } if err := r.ReplaceTrack(nil); err != nil { return err } errs := []error{} for _, trackEncoding := range r.trackEncodings { r.api.interceptor.UnbindLocalStream(&trackEncoding.streamInfo) errs = append(errs, trackEncoding.srtpStream.Close()) } return util.FlattenErrs(errs) } // Read reads incoming RTCP for this RTPSender func (r *RTPSender) Read(b []byte) (n int, a interceptor.Attributes, err error) { select { case <-r.sendCalled: return r.trackEncodings[0].rtcpInterceptor.Read(b, a) case <-r.stopCalled: return 0, nil, io.ErrClosedPipe } } // ReadRTCP is a convenience method that wraps Read and unmarshals for you. func (r *RTPSender) ReadRTCP() ([]rtcp.Packet, interceptor.Attributes, error) { b := make([]byte, r.api.settingEngine.getReceiveMTU()) i, attributes, err := r.Read(b) if err != nil { return nil, nil, err } pkts, err := rtcp.Unmarshal(b[:i]) if err != nil { return nil, nil, err } return pkts, attributes, nil } // ReadSimulcast reads incoming RTCP for this RTPSender for given rid func (r *RTPSender) ReadSimulcast(b []byte, rid string) (n int, a interceptor.Attributes, err error) { select { case <-r.sendCalled: for _, t := range r.trackEncodings { if t.track != nil && t.track.RID() == rid { return t.rtcpInterceptor.Read(b, a) } } return 0, nil, fmt.Errorf("%w: %s", errRTPSenderNoTrackForRID, rid) case <-r.stopCalled: return 0, nil, io.ErrClosedPipe } } // ReadSimulcastRTCP is a convenience method that wraps ReadSimulcast and unmarshal for you func (r *RTPSender) ReadSimulcastRTCP(rid string) ([]rtcp.Packet, interceptor.Attributes, error) { b := make([]byte, r.api.settingEngine.getReceiveMTU()) i, attributes, err := r.ReadSimulcast(b, rid) if err != nil { return nil, nil, err } pkts, err := rtcp.Unmarshal(b[:i]) return pkts, attributes, err } // SetReadDeadline sets the deadline for the Read operation. // Setting to zero means no deadline. func (r *RTPSender) SetReadDeadline(t time.Time) error { return r.trackEncodings[0].srtpStream.SetReadDeadline(t) } // SetReadDeadlineSimulcast sets the max amount of time the RTCP stream for a given rid will block before returning. 0 is forever. func (r *RTPSender) SetReadDeadlineSimulcast(deadline time.Time, rid string) error { r.mu.RLock() defer r.mu.RUnlock() for _, t := range r.trackEncodings { if t.track != nil && t.track.RID() == rid { return t.srtpStream.SetReadDeadline(deadline) } } return fmt.Errorf("%w: %s", errRTPSenderNoTrackForRID, rid) } // hasSent tells if data has been ever sent for this instance func (r *RTPSender) hasSent() bool { select { case <-r.sendCalled: return true default: return false } } // hasStopped tells if stop has been called func (r *RTPSender) hasStopped() bool { select { case <-r.stopCalled: return true default: return false } } webrtc-3.1.56/rtpsender_js.go000066400000000000000000000004471437620512100161330ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import "syscall/js" // RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer type RTPSender struct { // Pointer to the underlying JavaScript RTCRTPSender object. underlying js.Value } webrtc-3.1.56/rtpsender_test.go000066400000000000000000000271731437620512100165030ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "context" "errors" "io" "sync/atomic" "testing" "time" "github.com/pion/transport/v2/test" "github.com/pion/webrtc/v3/pkg/media" "github.com/stretchr/testify/assert" ) func Test_RTPSender_ReplaceTrack(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() s := SettingEngine{} s.DisableSRTPReplayProtection(true) m := &MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) sender, receiver, err := NewAPI(WithMediaEngine(m), WithSettingEngine(s)).newPair(Configuration{}) assert.NoError(t, err) trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264}, "video", "pion") assert.NoError(t, err) rtpSender, err := sender.AddTrack(trackA) assert.NoError(t, err) seenPacketA, seenPacketACancel := context.WithCancel(context.Background()) seenPacketB, seenPacketBCancel := context.WithCancel(context.Background()) var onTrackCount uint64 receiver.OnTrack(func(track *TrackRemote, _ *RTPReceiver) { assert.Equal(t, uint64(1), atomic.AddUint64(&onTrackCount, 1)) for { pkt, _, err := track.ReadRTP() if err != nil { assert.True(t, errors.Is(io.EOF, err)) return } switch { case pkt.Payload[len(pkt.Payload)-1] == 0xAA: assert.Equal(t, track.Codec().MimeType, MimeTypeVP8) seenPacketACancel() case pkt.Payload[len(pkt.Payload)-1] == 0xBB: assert.Equal(t, track.Codec().MimeType, MimeTypeH264) seenPacketBCancel() default: t.Fatalf("Unexpected RTP Data % 02x", pkt.Payload[len(pkt.Payload)-1]) } } }) assert.NoError(t, signalPair(sender, receiver)) // Block Until packet with 0xAA has been seen func() { for range time.Tick(time.Millisecond * 20) { select { case <-seenPacketA.Done(): return default: assert.NoError(t, trackA.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second})) } } }() assert.NoError(t, rtpSender.ReplaceTrack(trackB)) // Block Until packet with 0xBB has been seen func() { for range time.Tick(time.Millisecond * 20) { select { case <-seenPacketB.Done(): return default: assert.NoError(t, trackB.WriteSample(media.Sample{Data: []byte{0xBB}, Duration: time.Second})) } } }() closePairNow(t, sender, receiver) } func Test_RTPSender_GetParameters(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() offerer, answerer, err := newPair() assert.NoError(t, err) rtpTransceiver, err := offerer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) assert.NoError(t, signalPair(offerer, answerer)) parameters := rtpTransceiver.Sender().GetParameters() assert.NotEqual(t, 0, len(parameters.Codecs)) assert.Equal(t, 1, len(parameters.Encodings)) assert.Equal(t, rtpTransceiver.Sender().trackEncodings[0].ssrc, parameters.Encodings[0].SSRC) assert.Equal(t, "", parameters.Encodings[0].RID) closePairNow(t, offerer, answerer) } func Test_RTPSender_GetParameters_WithRID(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() offerer, answerer, err := newPair() assert.NoError(t, err) rtpTransceiver, err := offerer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) assert.NoError(t, signalPair(offerer, answerer)) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("moo")) assert.NoError(t, err) err = rtpTransceiver.setSendingTrack(track) assert.NoError(t, err) parameters := rtpTransceiver.Sender().GetParameters() assert.Equal(t, track.RID(), parameters.Encodings[0].RID) closePairNow(t, offerer, answerer) } func Test_RTPSender_SetReadDeadline(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() sender, receiver, wan := createVNetPair(t) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) rtpSender, err := sender.AddTrack(track) assert.NoError(t, err) peerConnectionsConnected := untilConnectionState(PeerConnectionStateConnected, sender, receiver) assert.NoError(t, signalPair(sender, receiver)) peerConnectionsConnected.Wait() assert.NoError(t, rtpSender.SetReadDeadline(time.Now().Add(1*time.Second))) _, _, err = rtpSender.ReadRTCP() assert.Error(t, err) assert.NoError(t, wan.Stop()) closePairNow(t, sender, receiver) } func Test_RTPSender_ReplaceTrack_InvalidTrackKindChange(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() sender, receiver, err := newPair() assert.NoError(t, err) trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "audio", "pion") assert.NoError(t, err) rtpSender, err := sender.AddTrack(trackA) assert.NoError(t, err) assert.NoError(t, signalPair(sender, receiver)) seenPacket, seenPacketCancel := context.WithCancel(context.Background()) receiver.OnTrack(func(_ *TrackRemote, _ *RTPReceiver) { seenPacketCancel() }) func() { for range time.Tick(time.Millisecond * 20) { select { case <-seenPacket.Done(): return default: assert.NoError(t, trackA.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second})) } } }() assert.True(t, errors.Is(rtpSender.ReplaceTrack(trackB), ErrRTPSenderNewTrackHasIncorrectKind)) closePairNow(t, sender, receiver) } func Test_RTPSender_ReplaceTrack_InvalidCodecChange(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() sender, receiver, err := newPair() assert.NoError(t, err) trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP9}, "video", "pion") assert.NoError(t, err) rtpSender, err := sender.AddTrack(trackA) assert.NoError(t, err) err = rtpSender.rtpTransceiver.SetCodecPreferences([]RTPCodecParameters{{ RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8}, PayloadType: 96, }}) assert.NoError(t, err) assert.NoError(t, signalPair(sender, receiver)) seenPacket, seenPacketCancel := context.WithCancel(context.Background()) receiver.OnTrack(func(_ *TrackRemote, _ *RTPReceiver) { seenPacketCancel() }) func() { for range time.Tick(time.Millisecond * 20) { select { case <-seenPacket.Done(): return default: assert.NoError(t, trackA.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second})) } } }() assert.True(t, errors.Is(rtpSender.ReplaceTrack(trackB), ErrUnsupportedCodec)) closePairNow(t, sender, receiver) } func Test_RTPSender_GetParameters_NilTrack(t *testing.T) { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) peerConnection, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) rtpSender, err := peerConnection.AddTrack(track) assert.NoError(t, err) assert.NoError(t, rtpSender.ReplaceTrack(nil)) rtpSender.GetParameters() assert.NoError(t, peerConnection.Close()) } func Test_RTPSender_Send(t *testing.T) { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) peerConnection, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) rtpSender, err := peerConnection.AddTrack(track) assert.NoError(t, err) parameter := rtpSender.GetParameters() err = rtpSender.Send(parameter) <-rtpSender.sendCalled assert.NoError(t, err) assert.NoError(t, peerConnection.Close()) } func Test_RTPSender_Send_Called_Once(t *testing.T) { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) peerConnection, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) rtpSender, err := peerConnection.AddTrack(track) assert.NoError(t, err) parameter := rtpSender.GetParameters() err = rtpSender.Send(parameter) <-rtpSender.sendCalled assert.NoError(t, err) err = rtpSender.Send(parameter) assert.Equal(t, errRTPSenderSendAlreadyCalled, err) assert.NoError(t, peerConnection.Close()) } func Test_RTPSender_Send_Track_Removed(t *testing.T) { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) peerConnection, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) rtpSender, err := peerConnection.AddTrack(track) assert.NoError(t, err) parameter := rtpSender.GetParameters() assert.NoError(t, peerConnection.RemoveTrack(rtpSender)) assert.Equal(t, errRTPSenderTrackRemoved, rtpSender.Send(parameter)) assert.NoError(t, peerConnection.Close()) } func Test_RTPSender_Add_Encoding(t *testing.T) { track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) peerConnection, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) rtpSender, err := peerConnection.AddTrack(track) assert.NoError(t, err) assert.Equal(t, errRTPSenderTrackNil, rtpSender.AddEncoding(nil)) track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) assert.Equal(t, errRTPSenderRidNil, rtpSender.AddEncoding(track1)) track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("h")) assert.NoError(t, err) assert.Equal(t, errRTPSenderNoBaseEncoding, rtpSender.AddEncoding(track1)) track, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("q")) assert.NoError(t, err) rtpSender, err = peerConnection.AddTrack(track) assert.NoError(t, err) track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video1", "pion", WithRTPStreamID("h")) assert.NoError(t, err) assert.Equal(t, errRTPSenderBaseEncodingMismatch, rtpSender.AddEncoding(track1)) track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1", WithRTPStreamID("h")) assert.NoError(t, err) assert.Equal(t, errRTPSenderBaseEncodingMismatch, rtpSender.AddEncoding(track1)) track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "video", "pion", WithRTPStreamID("h")) assert.NoError(t, err) assert.Equal(t, errRTPSenderBaseEncodingMismatch, rtpSender.AddEncoding(track1)) track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("q")) assert.NoError(t, err) assert.Equal(t, errRTPSenderRIDCollision, rtpSender.AddEncoding(track1)) track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("h")) assert.NoError(t, err) assert.NoError(t, rtpSender.AddEncoding(track1)) err = rtpSender.Send(rtpSender.GetParameters()) assert.NoError(t, err) track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("f")) assert.NoError(t, err) assert.Equal(t, errRTPSenderSendAlreadyCalled, rtpSender.AddEncoding(track1)) err = rtpSender.Stop() assert.NoError(t, err) assert.Equal(t, errRTPSenderStopped, rtpSender.AddEncoding(track1)) assert.NoError(t, peerConnection.Close()) } webrtc-3.1.56/rtpsendparameters.go000066400000000000000000000002531437620512100171670ustar00rootroot00000000000000package webrtc // RTPSendParameters contains the RTP stack settings used by receivers type RTPSendParameters struct { RTPParameters Encodings []RTPEncodingParameters } webrtc-3.1.56/rtptransceiver.go000066400000000000000000000202531437620512100165010ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "fmt" "sync" "sync/atomic" "github.com/pion/rtp" ) // RTPTransceiver represents a combination of an RTPSender and an RTPReceiver that share a common mid. type RTPTransceiver struct { mid atomic.Value // string sender atomic.Value // *RTPSender receiver atomic.Value // *RTPReceiver direction atomic.Value // RTPTransceiverDirection currentDirection atomic.Value // RTPTransceiverDirection codecs []RTPCodecParameters // User provided codecs via SetCodecPreferences stopped bool kind RTPCodecType api *API mu sync.RWMutex } func newRTPTransceiver( receiver *RTPReceiver, sender *RTPSender, direction RTPTransceiverDirection, kind RTPCodecType, api *API, ) *RTPTransceiver { t := &RTPTransceiver{kind: kind, api: api} t.setReceiver(receiver) t.setSender(sender) t.setDirection(direction) t.setCurrentDirection(RTPTransceiverDirection(Unknown)) return t } // SetCodecPreferences sets preferred list of supported codecs // if codecs is empty or nil we reset to default from MediaEngine func (t *RTPTransceiver) SetCodecPreferences(codecs []RTPCodecParameters) error { t.mu.Lock() defer t.mu.Unlock() for _, codec := range codecs { if _, matchType := codecParametersFuzzySearch(codec, t.api.mediaEngine.getCodecsByKind(t.kind)); matchType == codecMatchNone { return fmt.Errorf("%w %s", errRTPTransceiverCodecUnsupported, codec.MimeType) } } t.codecs = codecs return nil } // Codecs returns list of supported codecs func (t *RTPTransceiver) getCodecs() []RTPCodecParameters { t.mu.RLock() defer t.mu.RUnlock() mediaEngineCodecs := t.api.mediaEngine.getCodecsByKind(t.kind) if len(t.codecs) == 0 { return mediaEngineCodecs } filteredCodecs := []RTPCodecParameters{} for _, codec := range t.codecs { if c, matchType := codecParametersFuzzySearch(codec, mediaEngineCodecs); matchType != codecMatchNone { if codec.PayloadType == 0 { codec.PayloadType = c.PayloadType } filteredCodecs = append(filteredCodecs, codec) } } return filteredCodecs } // Sender returns the RTPTransceiver's RTPSender if it has one func (t *RTPTransceiver) Sender() *RTPSender { if v, ok := t.sender.Load().(*RTPSender); ok { return v } return nil } // SetSender sets the RTPSender and Track to current transceiver func (t *RTPTransceiver) SetSender(s *RTPSender, track TrackLocal) error { t.setSender(s) return t.setSendingTrack(track) } func (t *RTPTransceiver) setSender(s *RTPSender) { if s != nil { s.setRTPTransceiver(t) } if prevSender := t.Sender(); prevSender != nil { prevSender.setRTPTransceiver(nil) } t.sender.Store(s) } // Receiver returns the RTPTransceiver's RTPReceiver if it has one func (t *RTPTransceiver) Receiver() *RTPReceiver { if v, ok := t.receiver.Load().(*RTPReceiver); ok { return v } return nil } // SetMid sets the RTPTransceiver's mid. If it was already set, will return an error. func (t *RTPTransceiver) SetMid(mid string) error { if currentMid := t.Mid(); currentMid != "" { return fmt.Errorf("%w: %s to %s", errRTPTransceiverCannotChangeMid, currentMid, mid) } t.mid.Store(mid) return nil } // Mid gets the Transceiver's mid value. When not already set, this value will be set in CreateOffer or CreateAnswer. func (t *RTPTransceiver) Mid() string { if v, ok := t.mid.Load().(string); ok { return v } return "" } // Kind returns RTPTransceiver's kind. func (t *RTPTransceiver) Kind() RTPCodecType { return t.kind } // Direction returns the RTPTransceiver's current direction func (t *RTPTransceiver) Direction() RTPTransceiverDirection { if direction, ok := t.direction.Load().(RTPTransceiverDirection); ok { return direction } return RTPTransceiverDirection(0) } // Stop irreversibly stops the RTPTransceiver func (t *RTPTransceiver) Stop() error { if sender := t.Sender(); sender != nil { if err := sender.Stop(); err != nil { return err } } if receiver := t.Receiver(); receiver != nil { if err := receiver.Stop(); err != nil { return err } } t.setDirection(RTPTransceiverDirectionInactive) t.setCurrentDirection(RTPTransceiverDirectionInactive) return nil } func (t *RTPTransceiver) setReceiver(r *RTPReceiver) { if r != nil { r.setRTPTransceiver(t) } if prevReceiver := t.Receiver(); prevReceiver != nil { prevReceiver.setRTPTransceiver(nil) } t.receiver.Store(r) } func (t *RTPTransceiver) setDirection(d RTPTransceiverDirection) { t.direction.Store(d) } func (t *RTPTransceiver) setCurrentDirection(d RTPTransceiverDirection) { t.currentDirection.Store(d) } func (t *RTPTransceiver) getCurrentDirection() RTPTransceiverDirection { if v, ok := t.currentDirection.Load().(RTPTransceiverDirection); ok { return v } return RTPTransceiverDirection(Unknown) } func (t *RTPTransceiver) setSendingTrack(track TrackLocal) error { if err := t.Sender().ReplaceTrack(track); err != nil { return err } if track == nil { t.setSender(nil) } switch { case track != nil && t.Direction() == RTPTransceiverDirectionRecvonly: t.setDirection(RTPTransceiverDirectionSendrecv) case track != nil && t.Direction() == RTPTransceiverDirectionInactive: t.setDirection(RTPTransceiverDirectionSendonly) case track == nil && t.Direction() == RTPTransceiverDirectionSendrecv: t.setDirection(RTPTransceiverDirectionRecvonly) case track != nil && t.Direction() == RTPTransceiverDirectionSendonly: // Handle the case where a sendonly transceiver was added by a negotiation // initiated by remote peer. For example a remote peer added a transceiver // with direction recvonly. case track != nil && t.Direction() == RTPTransceiverDirectionSendrecv: // Similar to above, but for sendrecv transceiver. case track == nil && t.Direction() == RTPTransceiverDirectionSendonly: t.setDirection(RTPTransceiverDirectionInactive) default: return errRTPTransceiverSetSendingInvalidState } return nil } func findByMid(mid string, localTransceivers []*RTPTransceiver) (*RTPTransceiver, []*RTPTransceiver) { for i, t := range localTransceivers { if t.Mid() == mid { return t, append(localTransceivers[:i], localTransceivers[i+1:]...) } } return nil, localTransceivers } // Given a direction+type pluck a transceiver from the passed list // if no entry satisfies the requested type+direction return a inactive Transceiver func satisfyTypeAndDirection(remoteKind RTPCodecType, remoteDirection RTPTransceiverDirection, localTransceivers []*RTPTransceiver) (*RTPTransceiver, []*RTPTransceiver) { // Get direction order from most preferred to least getPreferredDirections := func() []RTPTransceiverDirection { switch remoteDirection { case RTPTransceiverDirectionSendrecv: return []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendrecv, RTPTransceiverDirectionSendonly} case RTPTransceiverDirectionSendonly: return []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendrecv} case RTPTransceiverDirectionRecvonly: return []RTPTransceiverDirection{RTPTransceiverDirectionSendonly, RTPTransceiverDirectionSendrecv} default: return []RTPTransceiverDirection{} } } for _, possibleDirection := range getPreferredDirections() { for i := range localTransceivers { t := localTransceivers[i] if t.Mid() == "" && t.kind == remoteKind && possibleDirection == t.Direction() { return t, append(localTransceivers[:i], localTransceivers[i+1:]...) } } } return nil, localTransceivers } // handleUnknownRTPPacket consumes a single RTP Packet and returns information that is helpful // for demuxing and handling an unknown SSRC (usually for Simulcast) func handleUnknownRTPPacket(buf []byte, midExtensionID, streamIDExtensionID, repairStreamIDExtensionID uint8, mid, rid, rsid *string) (payloadType PayloadType, err error) { rp := &rtp.Packet{} if err = rp.Unmarshal(buf); err != nil { return } if !rp.Header.Extension { return } payloadType = PayloadType(rp.PayloadType) if payload := rp.GetExtension(midExtensionID); payload != nil { *mid = string(payload) } if payload := rp.GetExtension(streamIDExtensionID); payload != nil { *rid = string(payload) } if payload := rp.GetExtension(repairStreamIDExtensionID); payload != nil { *rsid = string(payload) } return } webrtc-3.1.56/rtptransceiver_js.go000066400000000000000000000017501437620512100171760ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import ( "syscall/js" ) // RTPTransceiver represents a combination of an RTPSender and an RTPReceiver that share a common mid. type RTPTransceiver struct { // Pointer to the underlying JavaScript RTCRTPTransceiver object. underlying js.Value } // Direction returns the RTPTransceiver's current direction func (r *RTPTransceiver) Direction() RTPTransceiverDirection { return NewRTPTransceiverDirection(r.underlying.Get("direction").String()) } // Sender returns the RTPTransceiver's RTPSender if it has one func (r *RTPTransceiver) Sender() *RTPSender { underlying := r.underlying.Get("sender") if underlying.IsNull() { return nil } return &RTPSender{underlying: underlying} } // Receiver returns the RTPTransceiver's RTPReceiver if it has one func (r *RTPTransceiver) Receiver() *RTPReceiver { underlying := r.underlying.Get("receiver") if underlying.IsNull() { return nil } return &RTPReceiver{underlying: underlying} } webrtc-3.1.56/rtptransceiver_test.go000066400000000000000000000071131437620512100175400ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "strings" "testing" "github.com/stretchr/testify/assert" ) func Test_RTPTransceiver_SetCodecPreferences(t *testing.T) { me := &MediaEngine{} api := NewAPI(WithMediaEngine(me)) assert.NoError(t, me.RegisterDefaultCodecs()) me.pushCodecs(me.videoCodecs, RTPCodecTypeVideo) me.pushCodecs(me.audioCodecs, RTPCodecTypeAudio) tr := RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs} assert.EqualValues(t, me.videoCodecs, tr.getCodecs()) failTestCases := [][]RTPCodecParameters{ { { RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil}, PayloadType: 111, }, }, { { RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 96, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil}, PayloadType: 111, }, }, } for _, testCase := range failTestCases { assert.ErrorIs(t, tr.SetCodecPreferences(testCase), errRTPTransceiverCodecUnsupported) } successTestCases := [][]RTPCodecParameters{ { { RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 96, }, }, { { RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 96, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil}, PayloadType: 97, }, { RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", nil}, PayloadType: 98, }, { RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil}, PayloadType: 99, }, }, } for _, testCase := range successTestCases { assert.NoError(t, tr.SetCodecPreferences(testCase)) } assert.NoError(t, tr.SetCodecPreferences(nil)) assert.NotEqual(t, 0, len(tr.getCodecs())) assert.NoError(t, tr.SetCodecPreferences([]RTPCodecParameters{})) assert.NotEqual(t, 0, len(tr.getCodecs())) } // Assert that SetCodecPreferences properly filters codecs and PayloadTypes are respected func Test_RTPTransceiver_SetCodecPreferences_PayloadType(t *testing.T) { testCodec := RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{"video/testCodec", 90000, 0, "", nil}, PayloadType: 50, } m := &MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) offerPC, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{}) assert.NoError(t, err) assert.NoError(t, m.RegisterCodec(testCodec, RTPCodecTypeVideo)) answerPC, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = offerPC.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) answerTransceiver, err := answerPC.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) assert.NoError(t, answerTransceiver.SetCodecPreferences([]RTPCodecParameters{ testCodec, { RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 51, }, })) offer, err := offerPC.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, offerPC.SetLocalDescription(offer)) assert.NoError(t, answerPC.SetRemoteDescription(offer)) answer, err := answerPC.CreateAnswer(nil) assert.NoError(t, err) // VP8 with proper PayloadType assert.NotEqual(t, -1, strings.Index(answer.SDP, "a=rtpmap:51 VP8/90000")) // testCodec is ignored since offerer doesn't support assert.Equal(t, -1, strings.Index(answer.SDP, "testCodec")) closePairNow(t, offerPC, answerPC) } webrtc-3.1.56/rtptransceiverdirection.go000066400000000000000000000050771437620512100204110ustar00rootroot00000000000000package webrtc // RTPTransceiverDirection indicates the direction of the RTPTransceiver. type RTPTransceiverDirection int const ( // RTPTransceiverDirectionSendrecv indicates the RTPSender will offer // to send RTP and the RTPReceiver will offer to receive RTP. RTPTransceiverDirectionSendrecv RTPTransceiverDirection = iota + 1 // RTPTransceiverDirectionSendonly indicates the RTPSender will offer // to send RTP. RTPTransceiverDirectionSendonly // RTPTransceiverDirectionRecvonly indicates the RTPReceiver will // offer to receive RTP. RTPTransceiverDirectionRecvonly // RTPTransceiverDirectionInactive indicates the RTPSender won't offer // to send RTP and the RTPReceiver won't offer to receive RTP. RTPTransceiverDirectionInactive ) // This is done this way because of a linter. const ( rtpTransceiverDirectionSendrecvStr = "sendrecv" rtpTransceiverDirectionSendonlyStr = "sendonly" rtpTransceiverDirectionRecvonlyStr = "recvonly" rtpTransceiverDirectionInactiveStr = "inactive" ) // NewRTPTransceiverDirection defines a procedure for creating a new // RTPTransceiverDirection from a raw string naming the transceiver direction. func NewRTPTransceiverDirection(raw string) RTPTransceiverDirection { switch raw { case rtpTransceiverDirectionSendrecvStr: return RTPTransceiverDirectionSendrecv case rtpTransceiverDirectionSendonlyStr: return RTPTransceiverDirectionSendonly case rtpTransceiverDirectionRecvonlyStr: return RTPTransceiverDirectionRecvonly case rtpTransceiverDirectionInactiveStr: return RTPTransceiverDirectionInactive default: return RTPTransceiverDirection(Unknown) } } func (t RTPTransceiverDirection) String() string { switch t { case RTPTransceiverDirectionSendrecv: return rtpTransceiverDirectionSendrecvStr case RTPTransceiverDirectionSendonly: return rtpTransceiverDirectionSendonlyStr case RTPTransceiverDirectionRecvonly: return rtpTransceiverDirectionRecvonlyStr case RTPTransceiverDirectionInactive: return rtpTransceiverDirectionInactiveStr default: return ErrUnknownType.Error() } } // Revers indicate the opposite direction func (t RTPTransceiverDirection) Revers() RTPTransceiverDirection { switch t { case RTPTransceiverDirectionSendonly: return RTPTransceiverDirectionRecvonly case RTPTransceiverDirectionRecvonly: return RTPTransceiverDirectionSendonly default: return t } } func haveRTPTransceiverDirectionIntersection(haystack []RTPTransceiverDirection, needle []RTPTransceiverDirection) bool { for _, n := range needle { for _, h := range haystack { if n == h { return true } } } return false } webrtc-3.1.56/rtptransceiverdirection_test.go000066400000000000000000000023151437620512100214400ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewRTPTransceiverDirection(t *testing.T) { testCases := []struct { directionString string expectedDirection RTPTransceiverDirection }{ {unknownStr, RTPTransceiverDirection(Unknown)}, {"sendrecv", RTPTransceiverDirectionSendrecv}, {"sendonly", RTPTransceiverDirectionSendonly}, {"recvonly", RTPTransceiverDirectionRecvonly}, {"inactive", RTPTransceiverDirectionInactive}, } for i, testCase := range testCases { assert.Equal(t, NewRTPTransceiverDirection(testCase.directionString), testCase.expectedDirection, "testCase: %d %v", i, testCase, ) } } func TestRTPTransceiverDirection_String(t *testing.T) { testCases := []struct { direction RTPTransceiverDirection expectedString string }{ {RTPTransceiverDirection(Unknown), unknownStr}, {RTPTransceiverDirectionSendrecv, "sendrecv"}, {RTPTransceiverDirectionSendonly, "sendonly"}, {RTPTransceiverDirectionRecvonly, "recvonly"}, {RTPTransceiverDirectionInactive, "inactive"}, } for i, testCase := range testCases { assert.Equal(t, testCase.direction.String(), testCase.expectedString, "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/rtptransceiverinit.go000066400000000000000000000007641437620512100173720ustar00rootroot00000000000000package webrtc // RTPTransceiverInit dictionary is used when calling the WebRTC function addTransceiver() to provide configuration options for the new transceiver. type RTPTransceiverInit struct { Direction RTPTransceiverDirection SendEncodings []RTPEncodingParameters // Streams []*Track } // RtpTransceiverInit is a temporary mapping while we fix case sensitivity // Deprecated: Use RTPTransceiverInit instead type RtpTransceiverInit = RTPTransceiverInit //nolint: stylecheck,golint webrtc-3.1.56/sctpcapabilities.go000066400000000000000000000002451437620512100167500ustar00rootroot00000000000000package webrtc // SCTPCapabilities indicates the capabilities of the SCTPTransport. type SCTPCapabilities struct { MaxMessageSize uint32 `json:"maxMessageSize"` } webrtc-3.1.56/sctptransport.go000066400000000000000000000227601437620512100163610ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "errors" "io" "math" "sync" "time" "github.com/pion/datachannel" "github.com/pion/logging" "github.com/pion/sctp" "github.com/pion/webrtc/v3/pkg/rtcerr" ) const sctpMaxChannels = uint16(65535) // SCTPTransport provides details about the SCTP transport. type SCTPTransport struct { lock sync.RWMutex dtlsTransport *DTLSTransport // State represents the current state of the SCTP transport. state SCTPTransportState // SCTPTransportState doesn't have an enum to distinguish between New/Connecting // so we need a dedicated field isStarted bool // MaxMessageSize represents the maximum size of data that can be passed to // DataChannel's send() method. maxMessageSize float64 // MaxChannels represents the maximum amount of DataChannel's that can // be used simultaneously. maxChannels *uint16 // OnStateChange func() onErrorHandler func(error) sctpAssociation *sctp.Association onDataChannelHandler func(*DataChannel) onDataChannelOpenedHandler func(*DataChannel) // DataChannels dataChannels []*DataChannel dataChannelsOpened uint32 dataChannelsRequested uint32 dataChannelsAccepted uint32 api *API log logging.LeveledLogger } // NewSCTPTransport creates a new SCTPTransport. // This constructor is part of the ORTC API. It is not // meant to be used together with the basic WebRTC API. func (api *API) NewSCTPTransport(dtls *DTLSTransport) *SCTPTransport { res := &SCTPTransport{ dtlsTransport: dtls, state: SCTPTransportStateConnecting, api: api, log: api.settingEngine.LoggerFactory.NewLogger("ortc"), } res.updateMessageSize() res.updateMaxChannels() return res } // Transport returns the DTLSTransport instance the SCTPTransport is sending over. func (r *SCTPTransport) Transport() *DTLSTransport { r.lock.RLock() defer r.lock.RUnlock() return r.dtlsTransport } // GetCapabilities returns the SCTPCapabilities of the SCTPTransport. func (r *SCTPTransport) GetCapabilities() SCTPCapabilities { return SCTPCapabilities{ MaxMessageSize: 0, } } // Start the SCTPTransport. Since both local and remote parties must mutually // create an SCTPTransport, SCTP SO (Simultaneous Open) is used to establish // a connection over SCTP. func (r *SCTPTransport) Start(remoteCaps SCTPCapabilities) error { if r.isStarted { return nil } r.isStarted = true dtlsTransport := r.Transport() if dtlsTransport == nil || dtlsTransport.conn == nil { return errSCTPTransportDTLS } sctpAssociation, err := sctp.Client(sctp.Config{ NetConn: dtlsTransport.conn, MaxReceiveBufferSize: r.api.settingEngine.sctp.maxReceiveBufferSize, LoggerFactory: r.api.settingEngine.LoggerFactory, }) if err != nil { return err } r.lock.Lock() r.sctpAssociation = sctpAssociation r.state = SCTPTransportStateConnected dataChannels := append([]*DataChannel{}, r.dataChannels...) r.lock.Unlock() var openedDCCount uint32 for _, d := range dataChannels { if d.ReadyState() == DataChannelStateConnecting { err := d.open(r) if err != nil { r.log.Warnf("failed to open data channel: %s", err) continue } openedDCCount++ } } r.lock.Lock() r.dataChannelsOpened += openedDCCount r.lock.Unlock() go r.acceptDataChannels(sctpAssociation) return nil } // Stop stops the SCTPTransport func (r *SCTPTransport) Stop() error { r.lock.Lock() defer r.lock.Unlock() if r.sctpAssociation == nil { return nil } err := r.sctpAssociation.Close() if err != nil { return err } r.sctpAssociation = nil r.state = SCTPTransportStateClosed return nil } func (r *SCTPTransport) acceptDataChannels(a *sctp.Association) { r.lock.RLock() dataChannels := make([]*datachannel.DataChannel, 0, len(r.dataChannels)) for _, dc := range r.dataChannels { dc.mu.Lock() isNil := dc.dataChannel == nil dc.mu.Unlock() if isNil { continue } dataChannels = append(dataChannels, dc.dataChannel) } r.lock.RUnlock() ACCEPT: for { dc, err := datachannel.Accept(a, &datachannel.Config{ LoggerFactory: r.api.settingEngine.LoggerFactory, }, dataChannels...) if err != nil { if !errors.Is(err, io.EOF) { r.log.Errorf("Failed to accept data channel: %v", err) r.onError(err) } return } for _, ch := range dataChannels { if ch.StreamIdentifier() == dc.StreamIdentifier() { continue ACCEPT } } var ( maxRetransmits *uint16 maxPacketLifeTime *uint16 ) val := uint16(dc.Config.ReliabilityParameter) ordered := true switch dc.Config.ChannelType { case datachannel.ChannelTypeReliable: ordered = true case datachannel.ChannelTypeReliableUnordered: ordered = false case datachannel.ChannelTypePartialReliableRexmit: ordered = true maxRetransmits = &val case datachannel.ChannelTypePartialReliableRexmitUnordered: ordered = false maxRetransmits = &val case datachannel.ChannelTypePartialReliableTimed: ordered = true maxPacketLifeTime = &val case datachannel.ChannelTypePartialReliableTimedUnordered: ordered = false maxPacketLifeTime = &val default: } sid := dc.StreamIdentifier() rtcDC, err := r.api.newDataChannel(&DataChannelParameters{ ID: &sid, Label: dc.Config.Label, Protocol: dc.Config.Protocol, Negotiated: dc.Config.Negotiated, Ordered: ordered, MaxPacketLifeTime: maxPacketLifeTime, MaxRetransmits: maxRetransmits, }, r.api.settingEngine.LoggerFactory.NewLogger("ortc")) if err != nil { r.log.Errorf("Failed to accept data channel: %v", err) r.onError(err) return } <-r.onDataChannel(rtcDC) rtcDC.handleOpen(dc, true, dc.Config.Negotiated) r.lock.Lock() r.dataChannelsOpened++ handler := r.onDataChannelOpenedHandler r.lock.Unlock() if handler != nil { handler(rtcDC) } } } // OnError sets an event handler which is invoked when // the SCTP connection error occurs. func (r *SCTPTransport) OnError(f func(err error)) { r.lock.Lock() defer r.lock.Unlock() r.onErrorHandler = f } func (r *SCTPTransport) onError(err error) { r.lock.RLock() handler := r.onErrorHandler r.lock.RUnlock() if handler != nil { go handler(err) } } // OnDataChannel sets an event handler which is invoked when a data // channel message arrives from a remote peer. func (r *SCTPTransport) OnDataChannel(f func(*DataChannel)) { r.lock.Lock() defer r.lock.Unlock() r.onDataChannelHandler = f } // OnDataChannelOpened sets an event handler which is invoked when a data // channel is opened func (r *SCTPTransport) OnDataChannelOpened(f func(*DataChannel)) { r.lock.Lock() defer r.lock.Unlock() r.onDataChannelOpenedHandler = f } func (r *SCTPTransport) onDataChannel(dc *DataChannel) (done chan struct{}) { r.lock.Lock() r.dataChannels = append(r.dataChannels, dc) r.dataChannelsAccepted++ handler := r.onDataChannelHandler r.lock.Unlock() done = make(chan struct{}) if handler == nil || dc == nil { close(done) return } // Run this synchronously to allow setup done in onDataChannelFn() // to complete before datachannel event handlers might be called. go func() { handler(dc) close(done) }() return } func (r *SCTPTransport) updateMessageSize() { r.lock.Lock() defer r.lock.Unlock() var remoteMaxMessageSize float64 = 65536 // pion/webrtc#758 var canSendSize float64 = 65536 // pion/webrtc#758 r.maxMessageSize = r.calcMessageSize(remoteMaxMessageSize, canSendSize) } func (r *SCTPTransport) calcMessageSize(remoteMaxMessageSize, canSendSize float64) float64 { switch { case remoteMaxMessageSize == 0 && canSendSize == 0: return math.Inf(1) case remoteMaxMessageSize == 0: return canSendSize case canSendSize == 0: return remoteMaxMessageSize case canSendSize > remoteMaxMessageSize: return remoteMaxMessageSize default: return canSendSize } } func (r *SCTPTransport) updateMaxChannels() { val := sctpMaxChannels r.maxChannels = &val } // MaxChannels is the maximum number of RTCDataChannels that can be open simultaneously. func (r *SCTPTransport) MaxChannels() uint16 { r.lock.Lock() defer r.lock.Unlock() if r.maxChannels == nil { return sctpMaxChannels } return *r.maxChannels } // State returns the current state of the SCTPTransport func (r *SCTPTransport) State() SCTPTransportState { r.lock.RLock() defer r.lock.RUnlock() return r.state } func (r *SCTPTransport) collectStats(collector *statsReportCollector) { collector.Collecting() stats := TransportStats{ Timestamp: statsTimestampFrom(time.Now()), Type: StatsTypeTransport, ID: "sctpTransport", } association := r.association() if association != nil { stats.BytesSent = association.BytesSent() stats.BytesReceived = association.BytesReceived() } collector.Collect(stats.ID, stats) } func (r *SCTPTransport) generateAndSetDataChannelID(dtlsRole DTLSRole, idOut **uint16) error { var id uint16 if dtlsRole != DTLSRoleClient { id++ } max := r.MaxChannels() r.lock.Lock() defer r.lock.Unlock() // Create map of ids so we can compare without double-looping each time. idsMap := make(map[uint16]struct{}, len(r.dataChannels)) for _, dc := range r.dataChannels { if dc.ID() == nil { continue } idsMap[*dc.ID()] = struct{}{} } for ; id < max-1; id += 2 { if _, ok := idsMap[id]; ok { continue } *idOut = &id return nil } return &rtcerr.OperationError{Err: ErrMaxDataChannelID} } func (r *SCTPTransport) association() *sctp.Association { if r == nil { return nil } r.lock.RLock() association := r.sctpAssociation r.lock.RUnlock() return association } webrtc-3.1.56/sctptransport_js.go000066400000000000000000000010601437620512100170430ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc import "syscall/js" // SCTPTransport provides details about the SCTP transport. type SCTPTransport struct { // Pointer to the underlying JavaScript SCTPTransport object. underlying js.Value } // Transport returns the DTLSTransport instance the SCTPTransport is sending over. func (r *SCTPTransport) Transport() *DTLSTransport { underlying := r.underlying.Get("transport") if underlying.IsNull() || underlying.IsUndefined() { return nil } return &DTLSTransport{ underlying: underlying, } } webrtc-3.1.56/sctptransport_test.go000066400000000000000000000026041437620512100174130ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import "testing" func TestGenerateDataChannelID(t *testing.T) { sctpTransportWithChannels := func(ids []uint16) *SCTPTransport { ret := &SCTPTransport{dataChannels: []*DataChannel{}} for i := range ids { id := ids[i] ret.dataChannels = append(ret.dataChannels, &DataChannel{id: &id}) } return ret } testCases := []struct { role DTLSRole s *SCTPTransport result uint16 }{ {DTLSRoleClient, sctpTransportWithChannels([]uint16{}), 0}, {DTLSRoleClient, sctpTransportWithChannels([]uint16{1}), 0}, {DTLSRoleClient, sctpTransportWithChannels([]uint16{0}), 2}, {DTLSRoleClient, sctpTransportWithChannels([]uint16{0, 2}), 4}, {DTLSRoleClient, sctpTransportWithChannels([]uint16{0, 4}), 2}, {DTLSRoleServer, sctpTransportWithChannels([]uint16{}), 1}, {DTLSRoleServer, sctpTransportWithChannels([]uint16{0}), 1}, {DTLSRoleServer, sctpTransportWithChannels([]uint16{1}), 3}, {DTLSRoleServer, sctpTransportWithChannels([]uint16{1, 3}), 5}, {DTLSRoleServer, sctpTransportWithChannels([]uint16{1, 5}), 3}, } for _, testCase := range testCases { idPtr := new(uint16) err := testCase.s.generateAndSetDataChannelID(testCase.role, &idPtr) if err != nil { t.Errorf("failed to generate id: %v", err) return } if *idPtr != testCase.result { t.Errorf("Wrong id: %d expected %d", *idPtr, testCase.result) } } } webrtc-3.1.56/sctptransportstate.go000066400000000000000000000032071437620512100174150ustar00rootroot00000000000000package webrtc // SCTPTransportState indicates the state of the SCTP transport. type SCTPTransportState int const ( // SCTPTransportStateConnecting indicates the SCTPTransport is in the // process of negotiating an association. This is the initial state of the // SCTPTransportState when an SCTPTransport is created. SCTPTransportStateConnecting SCTPTransportState = iota + 1 // SCTPTransportStateConnected indicates the negotiation of an // association is completed. SCTPTransportStateConnected // SCTPTransportStateClosed indicates a SHUTDOWN or ABORT chunk is // received or when the SCTP association has been closed intentionally, // such as by closing the peer connection or applying a remote description // that rejects data or changes the SCTP port. SCTPTransportStateClosed ) // This is done this way because of a linter. const ( sctpTransportStateConnectingStr = "connecting" sctpTransportStateConnectedStr = "connected" sctpTransportStateClosedStr = "closed" ) func newSCTPTransportState(raw string) SCTPTransportState { switch raw { case sctpTransportStateConnectingStr: return SCTPTransportStateConnecting case sctpTransportStateConnectedStr: return SCTPTransportStateConnected case sctpTransportStateClosedStr: return SCTPTransportStateClosed default: return SCTPTransportState(Unknown) } } func (s SCTPTransportState) String() string { switch s { case SCTPTransportStateConnecting: return sctpTransportStateConnectingStr case SCTPTransportStateConnected: return sctpTransportStateConnectedStr case SCTPTransportStateClosed: return sctpTransportStateClosedStr default: return ErrUnknownType.Error() } } webrtc-3.1.56/sctptransportstate_test.go000066400000000000000000000021071437620512100204520ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewSCTPTransportState(t *testing.T) { testCases := []struct { transportStateString string expectedTransportState SCTPTransportState }{ {unknownStr, SCTPTransportState(Unknown)}, {"connecting", SCTPTransportStateConnecting}, {"connected", SCTPTransportStateConnected}, {"closed", SCTPTransportStateClosed}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedTransportState, newSCTPTransportState(testCase.transportStateString), "testCase: %d %v", i, testCase, ) } } func TestSCTPTransportState_String(t *testing.T) { testCases := []struct { transportState SCTPTransportState expectedString string }{ {SCTPTransportState(Unknown), unknownStr}, {SCTPTransportStateConnecting, "connecting"}, {SCTPTransportStateConnected, "connected"}, {SCTPTransportStateClosed, "closed"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.transportState.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/sdp.go000066400000000000000000000552141437620512100142210ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "errors" "fmt" "net/url" "regexp" "strconv" "strings" "sync/atomic" "github.com/pion/ice/v2" "github.com/pion/logging" "github.com/pion/sdp/v3" ) // trackDetails represents any media source that can be represented in a SDP // This isn't keyed by SSRC because it also needs to support rid based sources type trackDetails struct { mid string kind RTPCodecType streamID string id string ssrcs []SSRC repairSsrc *SSRC rids []string } func trackDetailsForSSRC(trackDetails []trackDetails, ssrc SSRC) *trackDetails { for i := range trackDetails { for j := range trackDetails[i].ssrcs { if trackDetails[i].ssrcs[j] == ssrc { return &trackDetails[i] } } } return nil } func trackDetailsForRID(trackDetails []trackDetails, rid string) *trackDetails { for i := range trackDetails { for j := range trackDetails[i].rids { if trackDetails[i].rids[j] == rid { return &trackDetails[i] } } } return nil } func filterTrackWithSSRC(incomingTracks []trackDetails, ssrc SSRC) []trackDetails { filtered := []trackDetails{} doesTrackHaveSSRC := func(t trackDetails) bool { for i := range t.ssrcs { if t.ssrcs[i] == ssrc { return true } } return false } for i := range incomingTracks { if !doesTrackHaveSSRC(incomingTracks[i]) { filtered = append(filtered, incomingTracks[i]) } } return filtered } // extract all trackDetails from an SDP. func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) (incomingTracks []trackDetails) { // nolint:gocognit for _, media := range s.MediaDescriptions { tracksInMediaSection := []trackDetails{} rtxRepairFlows := map[uint64]uint64{} // Plan B can have multiple tracks in a signle media section streamID := "" trackID := "" // If media section is recvonly or inactive skip if _, ok := media.Attribute(sdp.AttrKeyRecvOnly); ok { continue } else if _, ok := media.Attribute(sdp.AttrKeyInactive); ok { continue } midValue := getMidValue(media) if midValue == "" { continue } codecType := NewRTPCodecType(media.MediaName.Media) if codecType == 0 { continue } for _, attr := range media.Attributes { switch attr.Key { case sdp.AttrKeySSRCGroup: split := strings.Split(attr.Value, " ") if split[0] == sdp.SemanticTokenFlowIdentification { // Add rtx ssrcs to blacklist, to avoid adding them as tracks // Essentially lines like `a=ssrc-group:FID 2231627014 632943048` are processed by this section // as this declares that the second SSRC (632943048) is a rtx repair flow (RFC4588) for the first // (2231627014) as specified in RFC5576 if len(split) == 3 { baseSsrc, err := strconv.ParseUint(split[1], 10, 32) if err != nil { log.Warnf("Failed to parse SSRC: %v", err) continue } rtxRepairFlow, err := strconv.ParseUint(split[2], 10, 32) if err != nil { log.Warnf("Failed to parse SSRC: %v", err) continue } rtxRepairFlows[rtxRepairFlow] = baseSsrc tracksInMediaSection = filterTrackWithSSRC(tracksInMediaSection, SSRC(rtxRepairFlow)) // Remove if rtx was added as track before } } // Handle `a=msid: ` for Unified plan. The first value is the same as MediaStream.id // in the browser and can be used to figure out which tracks belong to the same stream. The browser should // figure this out automatically when an ontrack event is emitted on RTCPeerConnection. case sdp.AttrKeyMsid: split := strings.Split(attr.Value, " ") if len(split) == 2 { streamID = split[0] trackID = split[1] } case sdp.AttrKeySSRC: split := strings.Split(attr.Value, " ") ssrc, err := strconv.ParseUint(split[0], 10, 32) if err != nil { log.Warnf("Failed to parse SSRC: %v", err) continue } if _, ok := rtxRepairFlows[ssrc]; ok { continue // This ssrc is a RTX repair flow, ignore } if len(split) == 3 && strings.HasPrefix(split[1], "msid:") { streamID = split[1][len("msid:"):] trackID = split[2] } isNewTrack := true trackDetails := &trackDetails{} for i := range tracksInMediaSection { for j := range tracksInMediaSection[i].ssrcs { if tracksInMediaSection[i].ssrcs[j] == SSRC(ssrc) { trackDetails = &tracksInMediaSection[i] isNewTrack = false } } } trackDetails.mid = midValue trackDetails.kind = codecType trackDetails.streamID = streamID trackDetails.id = trackID trackDetails.ssrcs = []SSRC{SSRC(ssrc)} for r, baseSsrc := range rtxRepairFlows { if baseSsrc == ssrc { repairSsrc := SSRC(r) trackDetails.repairSsrc = &repairSsrc } } if isNewTrack { tracksInMediaSection = append(tracksInMediaSection, *trackDetails) } } } if rids := getRids(media); len(rids) != 0 && trackID != "" && streamID != "" { simulcastTrack := trackDetails{ mid: midValue, kind: codecType, streamID: streamID, id: trackID, rids: []string{}, } for rid := range rids { simulcastTrack.rids = append(simulcastTrack.rids, rid) } tracksInMediaSection = []trackDetails{simulcastTrack} } incomingTracks = append(incomingTracks, tracksInMediaSection...) } return incomingTracks } func trackDetailsToRTPReceiveParameters(t *trackDetails) RTPReceiveParameters { encodingSize := len(t.ssrcs) if len(t.rids) >= encodingSize { encodingSize = len(t.rids) } encodings := make([]RTPDecodingParameters, encodingSize) for i := range encodings { if len(t.rids) > i { encodings[i].RID = t.rids[i] } if len(t.ssrcs) > i { encodings[i].SSRC = t.ssrcs[i] } if t.repairSsrc != nil { encodings[i].RTX.SSRC = *t.repairSsrc } } return RTPReceiveParameters{Encodings: encodings} } func getRids(media *sdp.MediaDescription) map[string]string { rids := map[string]string{} for _, attr := range media.Attributes { if attr.Key == sdpAttributeRid { split := strings.Split(attr.Value, " ") rids[split[0]] = attr.Value } } return rids } func addCandidatesToMediaDescriptions(candidates []ICECandidate, m *sdp.MediaDescription, iceGatheringState ICEGatheringState) error { appendCandidateIfNew := func(c ice.Candidate, attributes []sdp.Attribute) { marshaled := c.Marshal() for _, a := range attributes { if marshaled == a.Value { return } } m.WithValueAttribute("candidate", marshaled) } for _, c := range candidates { candidate, err := c.toICE() if err != nil { return err } candidate.SetComponent(1) appendCandidateIfNew(candidate, m.Attributes) candidate.SetComponent(2) appendCandidateIfNew(candidate, m.Attributes) } if iceGatheringState != ICEGatheringStateComplete { return nil } for _, a := range m.Attributes { if a.Key == "end-of-candidates" { return nil } } m.WithPropertyAttribute("end-of-candidates") return nil } func addDataMediaSection(d *sdp.SessionDescription, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState) error { media := (&sdp.MediaDescription{ MediaName: sdp.MediaName{ Media: mediaSectionApplication, Port: sdp.RangedPort{Value: 9}, Protos: []string{"UDP", "DTLS", "SCTP"}, Formats: []string{"webrtc-datachannel"}, }, ConnectionInformation: &sdp.ConnectionInformation{ NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{ Address: "0.0.0.0", }, }, }). WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()). WithValueAttribute(sdp.AttrKeyMID, midValue). WithPropertyAttribute(RTPTransceiverDirectionSendrecv.String()). WithPropertyAttribute("sctp-port:5000"). WithICECredentials(iceParams.UsernameFragment, iceParams.Password) for _, f := range dtlsFingerprints { media = media.WithFingerprint(f.Algorithm, strings.ToUpper(f.Value)) } if shouldAddCandidates { if err := addCandidatesToMediaDescriptions(candidates, media, iceGatheringState); err != nil { return err } } d.WithMedia(media) return nil } func populateLocalCandidates(sessionDescription *SessionDescription, i *ICEGatherer, iceGatheringState ICEGatheringState) *SessionDescription { if sessionDescription == nil || i == nil { return sessionDescription } candidates, err := i.GetLocalCandidates() if err != nil { return sessionDescription } parsed := sessionDescription.parsed if len(parsed.MediaDescriptions) > 0 { m := parsed.MediaDescriptions[0] if err = addCandidatesToMediaDescriptions(candidates, m, iceGatheringState); err != nil { return sessionDescription } } sdp, err := parsed.Marshal() if err != nil { return sessionDescription } return &SessionDescription{ SDP: string(sdp), Type: sessionDescription.Type, parsed: parsed, } } func addSenderSDP( mediaSection mediaSection, isPlanB bool, media *sdp.MediaDescription, ) { for _, mt := range mediaSection.transceivers { sender := mt.Sender() if sender == nil { continue } track := sender.Track() if track == nil { continue } sendParameters := sender.GetParameters() for _, encoding := range sendParameters.Encodings { media = media.WithMediaSource(uint32(encoding.SSRC), track.StreamID() /* cname */, track.StreamID() /* streamLabel */, track.ID()) if !isPlanB { media = media.WithPropertyAttribute("msid:" + track.StreamID() + " " + track.ID()) } } if len(sendParameters.Encodings) > 1 { sendRids := make([]string, 0, len(sendParameters.Encodings)) for _, encoding := range sendParameters.Encodings { media.WithValueAttribute(sdpAttributeRid, encoding.RID+" send") sendRids = append(sendRids, encoding.RID) } // Simulcast media.WithValueAttribute("simulcast", "send "+strings.Join(sendRids, ";")) } if !isPlanB { break } } } func addTransceiverSDP( d *sdp.SessionDescription, isPlanB bool, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, mediaEngine *MediaEngine, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState, mediaSection mediaSection, ) (bool, error) { transceivers := mediaSection.transceivers if len(transceivers) < 1 { return false, errSDPZeroTransceivers } // Use the first transceiver to generate the section attributes t := transceivers[0] media := sdp.NewJSEPMediaDescription(t.kind.String(), []string{}). WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()). WithValueAttribute(sdp.AttrKeyMID, midValue). WithICECredentials(iceParams.UsernameFragment, iceParams.Password). WithPropertyAttribute(sdp.AttrKeyRTCPMux). WithPropertyAttribute(sdp.AttrKeyRTCPRsize) codecs := t.getCodecs() for _, codec := range codecs { name := strings.TrimPrefix(codec.MimeType, "audio/") name = strings.TrimPrefix(name, "video/") media.WithCodec(uint8(codec.PayloadType), name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine) for _, feedback := range codec.RTPCodecCapability.RTCPFeedback { media.WithValueAttribute("rtcp-fb", fmt.Sprintf("%d %s %s", codec.PayloadType, feedback.Type, feedback.Parameter)) } } if len(codecs) == 0 { // If we are sender and we have no codecs throw an error early if t.Sender() != nil { return false, ErrSenderWithNoCodecs } // Explicitly reject track if we don't have the codec // We need to include connection information even if we're rejecting a track, otherwise Firefox will fail to // parse the SDP with an error like: // SIPCC Failed to parse SDP: SDP Parse Error on line 50: c= connection line not specified for every media level, validation failed. // In addition this makes our SDP compliant with RFC 4566 Section 5.7: https://datatracker.ietf.org/doc/html/rfc4566#section-5.7 d.WithMedia(&sdp.MediaDescription{ MediaName: sdp.MediaName{ Media: t.kind.String(), Port: sdp.RangedPort{Value: 0}, Protos: []string{"UDP", "TLS", "RTP", "SAVPF"}, Formats: []string{"0"}, }, ConnectionInformation: &sdp.ConnectionInformation{ NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{ Address: "0.0.0.0", }, }, }) return false, nil } directions := []RTPTransceiverDirection{} if t.Sender() != nil { directions = append(directions, RTPTransceiverDirectionSendonly) } if t.Receiver() != nil { directions = append(directions, RTPTransceiverDirectionRecvonly) } parameters := mediaEngine.getRTPParametersByKind(t.kind, directions) for _, rtpExtension := range parameters.HeaderExtensions { extURL, err := url.Parse(rtpExtension.URI) if err != nil { return false, err } media.WithExtMap(sdp.ExtMap{Value: rtpExtension.ID, URI: extURL}) } if len(mediaSection.ridMap) > 0 { recvRids := make([]string, 0, len(mediaSection.ridMap)) for rid := range mediaSection.ridMap { media.WithValueAttribute(sdpAttributeRid, rid+" recv") recvRids = append(recvRids, rid) } // Simulcast media.WithValueAttribute("simulcast", "recv "+strings.Join(recvRids, ";")) } addSenderSDP(mediaSection, isPlanB, media) media = media.WithPropertyAttribute(t.Direction().String()) for _, fingerprint := range dtlsFingerprints { media = media.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value)) } if shouldAddCandidates { if err := addCandidatesToMediaDescriptions(candidates, media, iceGatheringState); err != nil { return false, err } } d.WithMedia(media) return true, nil } type mediaSection struct { id string transceivers []*RTPTransceiver data bool ridMap map[string]string } // populateSDP serializes a PeerConnections state into an SDP func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaDescriptionFingerprint bool, isICELite bool, isExtmapAllowMixed bool, mediaEngine *MediaEngine, connectionRole sdp.ConnectionRole, candidates []ICECandidate, iceParams ICEParameters, mediaSections []mediaSection, iceGatheringState ICEGatheringState) (*sdp.SessionDescription, error) { var err error mediaDtlsFingerprints := []DTLSFingerprint{} if mediaDescriptionFingerprint { mediaDtlsFingerprints = dtlsFingerprints } bundleValue := "BUNDLE" bundleCount := 0 appendBundle := func(midValue string) { bundleValue += " " + midValue bundleCount++ } for i, m := range mediaSections { if m.data && len(m.transceivers) != 0 { return nil, errSDPMediaSectionMediaDataChanInvalid } else if !isPlanB && len(m.transceivers) > 1 { return nil, errSDPMediaSectionMultipleTrackInvalid } shouldAddID := true shouldAddCandidates := i == 0 if m.data { if err = addDataMediaSection(d, shouldAddCandidates, mediaDtlsFingerprints, m.id, iceParams, candidates, connectionRole, iceGatheringState); err != nil { return nil, err } } else { shouldAddID, err = addTransceiverSDP(d, isPlanB, shouldAddCandidates, mediaDtlsFingerprints, mediaEngine, m.id, iceParams, candidates, connectionRole, iceGatheringState, m) if err != nil { return nil, err } } if shouldAddID { appendBundle(m.id) } } if !mediaDescriptionFingerprint { for _, fingerprint := range dtlsFingerprints { d.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value)) } } if isICELite { // RFC 5245 S15.3 d = d.WithValueAttribute(sdp.AttrKeyICELite, "") } if isExtmapAllowMixed { d = d.WithPropertyAttribute(sdp.AttrKeyExtMapAllowMixed) } return d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue), nil } func getMidValue(media *sdp.MediaDescription) string { for _, attr := range media.Attributes { if attr.Key == "mid" { return attr.Value } } return "" } // SessionDescription contains a MediaSection with Multiple SSRCs, it is Plan-B func descriptionIsPlanB(desc *SessionDescription, log logging.LeveledLogger) bool { if desc == nil || desc.parsed == nil { return false } // Store all MIDs that already contain a track midWithTrack := map[string]bool{} for _, trackDetail := range trackDetailsFromSDP(log, desc.parsed) { if _, ok := midWithTrack[trackDetail.mid]; ok { return true } midWithTrack[trackDetail.mid] = true } return false } // SessionDescription contains a MediaSection with name `audio`, `video` or `data` // If only one SSRC is set we can't know if it is Plan-B or Unified. If users have // set fallback mode assume it is Plan-B func descriptionPossiblyPlanB(desc *SessionDescription) bool { if desc == nil || desc.parsed == nil { return false } detectionRegex := regexp.MustCompile(`(?i)^(audio|video|data)$`) for _, media := range desc.parsed.MediaDescriptions { if len(detectionRegex.FindStringSubmatch(getMidValue(media))) == 2 { return true } } return false } func getPeerDirection(media *sdp.MediaDescription) RTPTransceiverDirection { for _, a := range media.Attributes { if direction := NewRTPTransceiverDirection(a.Key); direction != RTPTransceiverDirection(Unknown) { return direction } } return RTPTransceiverDirection(Unknown) } func extractFingerprint(desc *sdp.SessionDescription) (string, string, error) { fingerprints := []string{} if fingerprint, haveFingerprint := desc.Attribute("fingerprint"); haveFingerprint { fingerprints = append(fingerprints, fingerprint) } for _, m := range desc.MediaDescriptions { if fingerprint, haveFingerprint := m.Attribute("fingerprint"); haveFingerprint { fingerprints = append(fingerprints, fingerprint) } } if len(fingerprints) < 1 { return "", "", ErrSessionDescriptionNoFingerprint } for _, m := range fingerprints { if m != fingerprints[0] { return "", "", ErrSessionDescriptionConflictingFingerprints } } parts := strings.Split(fingerprints[0], " ") if len(parts) != 2 { return "", "", ErrSessionDescriptionInvalidFingerprint } return parts[1], parts[0], nil } func extractICEDetails(desc *sdp.SessionDescription, log logging.LeveledLogger) (string, string, []ICECandidate, error) { // nolint:gocognit candidates := []ICECandidate{} remotePwds := []string{} remoteUfrags := []string{} if ufrag, haveUfrag := desc.Attribute("ice-ufrag"); haveUfrag { remoteUfrags = append(remoteUfrags, ufrag) } if pwd, havePwd := desc.Attribute("ice-pwd"); havePwd { remotePwds = append(remotePwds, pwd) } for _, m := range desc.MediaDescriptions { if ufrag, haveUfrag := m.Attribute("ice-ufrag"); haveUfrag { remoteUfrags = append(remoteUfrags, ufrag) } if pwd, havePwd := m.Attribute("ice-pwd"); havePwd { remotePwds = append(remotePwds, pwd) } for _, a := range m.Attributes { if a.IsICECandidate() { c, err := ice.UnmarshalCandidate(a.Value) if err != nil { if errors.Is(err, ice.ErrUnknownCandidateTyp) || errors.Is(err, ice.ErrDetermineNetworkType) { log.Warnf("Discarding remote candidate: %s", err) continue } return "", "", nil, err } candidate, err := newICECandidateFromICE(c) if err != nil { return "", "", nil, err } candidates = append(candidates, candidate) } } } if len(remoteUfrags) == 0 { return "", "", nil, ErrSessionDescriptionMissingIceUfrag } else if len(remotePwds) == 0 { return "", "", nil, ErrSessionDescriptionMissingIcePwd } for _, m := range remoteUfrags { if m != remoteUfrags[0] { return "", "", nil, ErrSessionDescriptionConflictingIceUfrag } } for _, m := range remotePwds { if m != remotePwds[0] { return "", "", nil, ErrSessionDescriptionConflictingIcePwd } } return remoteUfrags[0], remotePwds[0], candidates, nil } func haveApplicationMediaSection(desc *sdp.SessionDescription) bool { for _, m := range desc.MediaDescriptions { if m.MediaName.Media == mediaSectionApplication { return true } } return false } func getByMid(searchMid string, desc *SessionDescription) *sdp.MediaDescription { for _, m := range desc.parsed.MediaDescriptions { if mid, ok := m.Attribute(sdp.AttrKeyMID); ok && mid == searchMid { return m } } return nil } // haveDataChannel return MediaDescription with MediaName equal application func haveDataChannel(desc *SessionDescription) *sdp.MediaDescription { for _, d := range desc.parsed.MediaDescriptions { if d.MediaName.Media == mediaSectionApplication { return d } } return nil } func codecsFromMediaDescription(m *sdp.MediaDescription) (out []RTPCodecParameters, err error) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{m}, } for _, payloadStr := range m.MediaName.Formats { payloadType, err := strconv.ParseUint(payloadStr, 10, 8) if err != nil { return nil, err } codec, err := s.GetCodecForPayloadType(uint8(payloadType)) if err != nil { if payloadType == 0 { continue } return nil, err } channels := uint16(0) val, err := strconv.ParseUint(codec.EncodingParameters, 10, 16) if err == nil { channels = uint16(val) } feedback := []RTCPFeedback{} for _, raw := range codec.RTCPFeedback { split := strings.Split(raw, " ") entry := RTCPFeedback{Type: split[0]} if len(split) == 2 { entry.Parameter = split[1] } feedback = append(feedback, entry) } out = append(out, RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{m.MediaName.Media + "/" + codec.Name, codec.ClockRate, channels, codec.Fmtp, feedback}, PayloadType: PayloadType(payloadType), }) } return out, nil } func rtpExtensionsFromMediaDescription(m *sdp.MediaDescription) (map[string]int, error) { out := map[string]int{} for _, a := range m.Attributes { if a.Key == sdp.AttrKeyExtMap { e := sdp.ExtMap{} if err := e.Unmarshal(a.String()); err != nil { return nil, err } out[e.URI.String()] = e.Value } } return out, nil } // updateSDPOrigin saves sdp.Origin in PeerConnection when creating 1st local SDP; // for subsequent calling, it updates Origin for SessionDescription from saved one // and increments session version by one. // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-25#section-5.2.2 func updateSDPOrigin(origin *sdp.Origin, d *sdp.SessionDescription) { if atomic.CompareAndSwapUint64(&origin.SessionVersion, 0, d.Origin.SessionVersion) { // store atomic.StoreUint64(&origin.SessionID, d.Origin.SessionID) } else { // load for { // awaiting for saving session id d.Origin.SessionID = atomic.LoadUint64(&origin.SessionID) if d.Origin.SessionID != 0 { break } } d.Origin.SessionVersion = atomic.AddUint64(&origin.SessionVersion, 1) } } func isIceLiteSet(desc *sdp.SessionDescription) bool { for _, a := range desc.Attributes { if strings.TrimSpace(a.Key) == sdp.AttrKeyICELite { return true } } return false } func isExtMapAllowMixedSet(desc *sdp.SessionDescription) bool { for _, a := range desc.Attributes { if strings.TrimSpace(a.Key) == sdp.AttrKeyExtMapAllowMixed { return true } } return false } webrtc-3.1.56/sdp_test.go000066400000000000000000000444121437620512100152560ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "strings" "testing" "github.com/pion/sdp/v3" "github.com/stretchr/testify/assert" ) func TestExtractFingerprint(t *testing.T) { t.Run("Good Session Fingerprint", func(t *testing.T) { s := &sdp.SessionDescription{ Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo bar"}}, } fingerprint, hash, err := extractFingerprint(s) assert.NoError(t, err) assert.Equal(t, fingerprint, "bar") assert.Equal(t, hash, "foo") }) t.Run("Good Media Fingerprint", func(t *testing.T) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{ {Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo bar"}}}, }, } fingerprint, hash, err := extractFingerprint(s) assert.NoError(t, err) assert.Equal(t, fingerprint, "bar") assert.Equal(t, hash, "foo") }) t.Run("No Fingerprint", func(t *testing.T) { s := &sdp.SessionDescription{} _, _, err := extractFingerprint(s) assert.Equal(t, ErrSessionDescriptionNoFingerprint, err) }) t.Run("Invalid Fingerprint", func(t *testing.T) { s := &sdp.SessionDescription{ Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo"}}, } _, _, err := extractFingerprint(s) assert.Equal(t, ErrSessionDescriptionInvalidFingerprint, err) }) t.Run("Conflicting Fingerprint", func(t *testing.T) { s := &sdp.SessionDescription{ Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo"}}, MediaDescriptions: []*sdp.MediaDescription{ {Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo blah"}}}, }, } _, _, err := extractFingerprint(s) assert.Equal(t, ErrSessionDescriptionConflictingFingerprints, err) }) } func TestExtractICEDetails(t *testing.T) { const defaultUfrag = "defaultPwd" const defaultPwd = "defaultUfrag" t.Run("Missing ice-pwd", func(t *testing.T) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{ {Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}}}, }, } _, _, _, err := extractICEDetails(s, nil) assert.Equal(t, err, ErrSessionDescriptionMissingIcePwd) }) t.Run("Missing ice-ufrag", func(t *testing.T) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{ {Attributes: []sdp.Attribute{{Key: "ice-pwd", Value: defaultPwd}}}, }, } _, _, _, err := extractICEDetails(s, nil) assert.Equal(t, err, ErrSessionDescriptionMissingIceUfrag) }) t.Run("ice details at session level", func(t *testing.T) { s := &sdp.SessionDescription{ Attributes: []sdp.Attribute{ {Key: "ice-ufrag", Value: defaultUfrag}, {Key: "ice-pwd", Value: defaultPwd}, }, MediaDescriptions: []*sdp.MediaDescription{}, } ufrag, pwd, _, err := extractICEDetails(s, nil) assert.Equal(t, ufrag, defaultUfrag) assert.Equal(t, pwd, defaultPwd) assert.NoError(t, err) }) t.Run("ice details at media level", func(t *testing.T) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{ { Attributes: []sdp.Attribute{ {Key: "ice-ufrag", Value: defaultUfrag}, {Key: "ice-pwd", Value: defaultPwd}, }, }, }, } ufrag, pwd, _, err := extractICEDetails(s, nil) assert.Equal(t, ufrag, defaultUfrag) assert.Equal(t, pwd, defaultPwd) assert.NoError(t, err) }) t.Run("Conflict ufrag", func(t *testing.T) { s := &sdp.SessionDescription{ Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: "invalidUfrag"}}, MediaDescriptions: []*sdp.MediaDescription{ {Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}, {Key: "ice-pwd", Value: defaultPwd}}}, }, } _, _, _, err := extractICEDetails(s, nil) assert.Equal(t, err, ErrSessionDescriptionConflictingIceUfrag) }) t.Run("Conflict pwd", func(t *testing.T) { s := &sdp.SessionDescription{ Attributes: []sdp.Attribute{{Key: "ice-pwd", Value: "invalidPwd"}}, MediaDescriptions: []*sdp.MediaDescription{ {Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}, {Key: "ice-pwd", Value: defaultPwd}}}, }, } _, _, _, err := extractICEDetails(s, nil) assert.Equal(t, err, ErrSessionDescriptionConflictingIcePwd) }) } func TestTrackDetailsFromSDP(t *testing.T) { t.Run("Tracks unknown, audio and video with RTX", func(t *testing.T) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{ { MediaName: sdp.MediaName{ Media: "foobar", }, Attributes: []sdp.Attribute{ {Key: "mid", Value: "0"}, {Key: "sendrecv"}, {Key: "ssrc", Value: "1000 msid:unknown_trk_label unknown_trk_guid"}, }, }, { MediaName: sdp.MediaName{ Media: "audio", }, Attributes: []sdp.Attribute{ {Key: "mid", Value: "1"}, {Key: "sendrecv"}, {Key: "ssrc", Value: "2000 msid:audio_trk_label audio_trk_guid"}, }, }, { MediaName: sdp.MediaName{ Media: "video", }, Attributes: []sdp.Attribute{ {Key: "mid", Value: "2"}, {Key: "sendrecv"}, {Key: "ssrc-group", Value: "FID 3000 4000"}, {Key: "ssrc", Value: "3000 msid:video_trk_label video_trk_guid"}, {Key: "ssrc", Value: "4000 msid:rtx_trk_label rtx_trck_guid"}, }, }, { MediaName: sdp.MediaName{ Media: "video", }, Attributes: []sdp.Attribute{ {Key: "mid", Value: "3"}, {Key: "sendonly"}, {Key: "msid", Value: "video_stream_id video_trk_id"}, {Key: "ssrc", Value: "5000"}, }, }, { MediaName: sdp.MediaName{ Media: "video", }, Attributes: []sdp.Attribute{ {Key: "sendonly"}, {Key: sdpAttributeRid, Value: "f send pt=97;max-width=1280;max-height=720"}, }, }, }, } tracks := trackDetailsFromSDP(nil, s) assert.Equal(t, 3, len(tracks)) if trackDetail := trackDetailsForSSRC(tracks, 1000); trackDetail != nil { assert.Fail(t, "got the unknown track ssrc:1000 which should have been skipped") } if track := trackDetailsForSSRC(tracks, 2000); track == nil { assert.Fail(t, "missing audio track with ssrc:2000") } else { assert.Equal(t, RTPCodecTypeAudio, track.kind) assert.Equal(t, SSRC(2000), track.ssrcs[0]) assert.Equal(t, "audio_trk_label", track.streamID) } if track := trackDetailsForSSRC(tracks, 3000); track == nil { assert.Fail(t, "missing video track with ssrc:3000") } else { assert.Equal(t, RTPCodecTypeVideo, track.kind) assert.Equal(t, SSRC(3000), track.ssrcs[0]) assert.Equal(t, "video_trk_label", track.streamID) } if track := trackDetailsForSSRC(tracks, 4000); track != nil { assert.Fail(t, "got the rtx track ssrc:3000 which should have been skipped") } if track := trackDetailsForSSRC(tracks, 5000); track == nil { assert.Fail(t, "missing video track with ssrc:5000") } else { assert.Equal(t, RTPCodecTypeVideo, track.kind) assert.Equal(t, SSRC(5000), track.ssrcs[0]) assert.Equal(t, "video_trk_id", track.id) assert.Equal(t, "video_stream_id", track.streamID) } }) t.Run("inactive and recvonly tracks ignored", func(t *testing.T) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{ { MediaName: sdp.MediaName{ Media: "video", }, Attributes: []sdp.Attribute{ {Key: "inactive"}, {Key: "ssrc", Value: "6000"}, }, }, { MediaName: sdp.MediaName{ Media: "video", }, Attributes: []sdp.Attribute{ {Key: "recvonly"}, {Key: "ssrc", Value: "7000"}, }, }, }, } assert.Equal(t, 0, len(trackDetailsFromSDP(nil, s))) }) } func TestHaveApplicationMediaSection(t *testing.T) { t.Run("Audio only", func(t *testing.T) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{ { MediaName: sdp.MediaName{ Media: "audio", }, Attributes: []sdp.Attribute{ {Key: "sendrecv"}, {Key: "ssrc", Value: "2000"}, }, }, }, } assert.False(t, haveApplicationMediaSection(s)) }) t.Run("Application", func(t *testing.T) { s := &sdp.SessionDescription{ MediaDescriptions: []*sdp.MediaDescription{ { MediaName: sdp.MediaName{ Media: mediaSectionApplication, }, }, }, } assert.True(t, haveApplicationMediaSection(s)) }) } func TestMediaDescriptionFingerprints(t *testing.T) { engine := &MediaEngine{} assert.NoError(t, engine.RegisterDefaultCodecs()) api := NewAPI(WithMediaEngine(engine)) sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.NoError(t, err) certificate, err := GenerateCertificate(sk) assert.NoError(t, err) media := []mediaSection{ { id: "video", transceivers: []*RTPTransceiver{{ kind: RTPCodecTypeVideo, api: api, codecs: engine.getCodecsByKind(RTPCodecTypeVideo), }}, }, { id: "audio", transceivers: []*RTPTransceiver{{ kind: RTPCodecTypeAudio, api: api, codecs: engine.getCodecsByKind(RTPCodecTypeAudio), }}, }, { id: "application", data: true, }, } for i := 0; i < 2; i++ { media[i].transceivers[0].setSender(&RTPSender{}) media[i].transceivers[0].setDirection(RTPTransceiverDirectionSendonly) } fingerprintTest := func(SDPMediaDescriptionFingerprints bool, expectedFingerprintCount int) func(t *testing.T) { return func(t *testing.T) { s := &sdp.SessionDescription{} dtlsFingerprints, err := certificate.GetFingerprints() assert.NoError(t, err) s, err = populateSDP(s, false, dtlsFingerprints, SDPMediaDescriptionFingerprints, false, true, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew) assert.NoError(t, err) sdparray, err := s.Marshal() assert.NoError(t, err) assert.Equal(t, strings.Count(string(sdparray), "sha-256"), expectedFingerprintCount) } } t.Run("Per-Media Description Fingerprints", fingerprintTest(true, 3)) t.Run("Per-Session Description Fingerprints", fingerprintTest(false, 1)) } func TestPopulateSDP(t *testing.T) { t.Run("rid", func(t *testing.T) { se := SettingEngine{} me := &MediaEngine{} assert.NoError(t, me.RegisterDefaultCodecs()) api := NewAPI(WithMediaEngine(me)) tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs} tr.setDirection(RTPTransceiverDirectionRecvonly) ridMap := map[string]string{ "ridkey": "some", } mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}, ridMap: ridMap}} d := &sdp.SessionDescription{} offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete) assert.Nil(t, err) // Test contains rid map keys var found bool for _, desc := range offerSdp.MediaDescriptions { if desc.MediaName.Media != "video" { continue } for _, a := range desc.Attributes { if a.Key == sdpAttributeRid { if strings.Contains(a.Value, "ridkey") { found = true break } } } } assert.Equal(t, true, found, "Rid key should be present") }) t.Run("SetCodecPreferences", func(t *testing.T) { se := SettingEngine{} me := &MediaEngine{} assert.NoError(t, me.RegisterDefaultCodecs()) api := NewAPI(WithMediaEngine(me)) me.pushCodecs(me.videoCodecs, RTPCodecTypeVideo) me.pushCodecs(me.audioCodecs, RTPCodecTypeAudio) tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs} tr.setDirection(RTPTransceiverDirectionRecvonly) codecErr := tr.SetCodecPreferences([]RTPCodecParameters{ { RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil}, PayloadType: 96, }, }) assert.NoError(t, codecErr) mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}} d := &sdp.SessionDescription{} offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete) assert.Nil(t, err) // Test codecs foundVP8 := false for _, desc := range offerSdp.MediaDescriptions { if desc.MediaName.Media != "video" { continue } for _, a := range desc.Attributes { if strings.Contains(a.Key, "rtpmap") { if a.Value == "98 VP9/90000" { t.Fatal("vp9 should not be present in sdp") } else if a.Value == "96 VP8/90000" { foundVP8 = true } } } } assert.Equal(t, true, foundVP8, "vp8 should be present in sdp") }) t.Run("ice-lite", func(t *testing.T) { se := SettingEngine{} se.SetLite(true) offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete) assert.Nil(t, err) var found bool // ice-lite is an session-level attribute for _, a := range offerSdp.Attributes { if a.Key == sdp.AttrKeyICELite { // ice-lite does not have value (e.g. ":") and it should be an empty string if a.Value == "" { found = true break } } } assert.Equal(t, true, found, "ICELite key should be present") }) t.Run("rejected track", func(t *testing.T) { se := SettingEngine{} me := &MediaEngine{} registerCodecErr := me.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, PayloadType: 96, }, RTPCodecTypeVideo) assert.NoError(t, registerCodecErr) api := NewAPI(WithMediaEngine(me)) videoTransceiver := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs} audioTransceiver := &RTPTransceiver{kind: RTPCodecTypeAudio, api: api, codecs: []RTPCodecParameters{}} mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{videoTransceiver}}, {id: "audio", transceivers: []*RTPTransceiver{audioTransceiver}}} d := &sdp.SessionDescription{} offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete) assert.NoError(t, err) // Test codecs foundRejectedTrack := false for _, desc := range offerSdp.MediaDescriptions { if desc.MediaName.Media != "audio" { continue } assert.True(t, desc.ConnectionInformation != nil, "connection information must be provided for rejected tracks") assert.Equal(t, desc.MediaName.Formats, []string{"0"}, "rejected tracks have 0 for Formats") assert.Equal(t, desc.MediaName.Port, sdp.RangedPort{Value: 0}, "rejected tracks have 0 for Port") foundRejectedTrack = true } assert.Equal(t, true, foundRejectedTrack, "rejected track wasn't present") }) t.Run("allow mixed extmap", func(t *testing.T) { se := SettingEngine{} offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete) assert.Nil(t, err) var found bool // session-level attribute for _, a := range offerSdp.Attributes { if a.Key == sdp.AttrKeyExtMapAllowMixed { if a.Value == "" { found = true break } } } assert.Equal(t, true, found, "AllowMixedExtMap key should be present") offerSdp, err = populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, false, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete) assert.Nil(t, err) found = false // session-level attribute for _, a := range offerSdp.Attributes { if a.Key == sdp.AttrKeyExtMapAllowMixed { if a.Value == "" { found = true break } } } assert.Equal(t, false, found, "AllowMixedExtMap key should not be present") }) } func TestGetRIDs(t *testing.T) { m := []*sdp.MediaDescription{ { MediaName: sdp.MediaName{ Media: "video", }, Attributes: []sdp.Attribute{ {Key: "sendonly"}, {Key: sdpAttributeRid, Value: "f send pt=97;max-width=1280;max-height=720"}, }, }, } rids := getRids(m[0]) assert.NotEmpty(t, rids, "Rid mapping should be present") if _, ok := rids["f"]; !ok { assert.Fail(t, "rid values should contain 'f'") } } func TestCodecsFromMediaDescription(t *testing.T) { t.Run("Codec Only", func(t *testing.T) { codecs, err := codecsFromMediaDescription(&sdp.MediaDescription{ MediaName: sdp.MediaName{ Media: "audio", Formats: []string{"111"}, }, Attributes: []sdp.Attribute{ {Key: "rtpmap", Value: "111 opus/48000/2"}, }, }) assert.Equal(t, codecs, []RTPCodecParameters{ { RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "", []RTCPFeedback{}}, PayloadType: 111, }, }) assert.NoError(t, err) }) t.Run("Codec with fmtp/rtcp-fb", func(t *testing.T) { codecs, err := codecsFromMediaDescription(&sdp.MediaDescription{ MediaName: sdp.MediaName{ Media: "audio", Formats: []string{"111"}, }, Attributes: []sdp.Attribute{ {Key: "rtpmap", Value: "111 opus/48000/2"}, {Key: "fmtp", Value: "111 minptime=10;useinbandfec=1"}, {Key: "rtcp-fb", Value: "111 goog-remb"}, {Key: "rtcp-fb", Value: "111 ccm fir"}, }, }) assert.Equal(t, codecs, []RTPCodecParameters{ { RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}}}, PayloadType: 111, }, }) assert.NoError(t, err) }) } func TestRtpExtensionsFromMediaDescription(t *testing.T) { extensions, err := rtpExtensionsFromMediaDescription(&sdp.MediaDescription{ MediaName: sdp.MediaName{ Media: "audio", Formats: []string{"111"}, }, Attributes: []sdp.Attribute{ {Key: "extmap", Value: "1 " + sdp.ABSSendTimeURI}, {Key: "extmap", Value: "3 " + sdp.SDESMidURI}, }, }) assert.NoError(t, err) assert.Equal(t, extensions[sdp.ABSSendTimeURI], 1) assert.Equal(t, extensions[sdp.SDESMidURI], 3) } webrtc-3.1.56/sdpsemantics.go000066400000000000000000000035751437620512100161330ustar00rootroot00000000000000package webrtc import ( "encoding/json" ) // SDPSemantics determines which style of SDP offers and answers // can be used type SDPSemantics int const ( // SDPSemanticsUnifiedPlan uses unified-plan offers and answers // (the default in Chrome since M72) // https://tools.ietf.org/html/draft-roach-mmusic-unified-plan-00 SDPSemanticsUnifiedPlan SDPSemantics = iota // SDPSemanticsPlanB uses plan-b offers and answers // NB: This format should be considered deprecated // https://tools.ietf.org/html/draft-uberti-rtcweb-plan-00 SDPSemanticsPlanB // SDPSemanticsUnifiedPlanWithFallback prefers unified-plan // offers and answers, but will respond to a plan-b offer // with a plan-b answer SDPSemanticsUnifiedPlanWithFallback ) const ( sdpSemanticsUnifiedPlanWithFallback = "unified-plan-with-fallback" sdpSemanticsUnifiedPlan = "unified-plan" sdpSemanticsPlanB = "plan-b" ) func newSDPSemantics(raw string) SDPSemantics { switch raw { case sdpSemanticsUnifiedPlan: return SDPSemanticsUnifiedPlan case sdpSemanticsPlanB: return SDPSemanticsPlanB case sdpSemanticsUnifiedPlanWithFallback: return SDPSemanticsUnifiedPlanWithFallback default: return SDPSemantics(Unknown) } } func (s SDPSemantics) String() string { switch s { case SDPSemanticsUnifiedPlanWithFallback: return sdpSemanticsUnifiedPlanWithFallback case SDPSemanticsUnifiedPlan: return sdpSemanticsUnifiedPlan case SDPSemanticsPlanB: return sdpSemanticsPlanB default: return ErrUnknownType.Error() } } // UnmarshalJSON parses the JSON-encoded data and stores the result func (s *SDPSemantics) UnmarshalJSON(b []byte) error { var val string if err := json.Unmarshal(b, &val); err != nil { return err } *s = newSDPSemantics(val) return nil } // MarshalJSON returns the JSON encoding func (s SDPSemantics) MarshalJSON() ([]byte, error) { return json.Marshal(s.String()) } webrtc-3.1.56/sdpsemantics_test.go000066400000000000000000000265071437620512100171720ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "encoding/json" "errors" "strings" "testing" "time" "github.com/pion/sdp/v3" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) func TestSDPSemantics_String(t *testing.T) { testCases := []struct { value SDPSemantics expectedString string }{ {SDPSemanticsUnifiedPlanWithFallback, "unified-plan-with-fallback"}, {SDPSemanticsPlanB, "plan-b"}, {SDPSemanticsUnifiedPlan, "unified-plan"}, } assert.Equal(t, unknownStr, SDPSemantics(42).String(), ) for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.value.String(), "testCase: %d %v", i, testCase, ) assert.Equal(t, testCase.value, newSDPSemantics(testCase.expectedString), "testCase: %d %v", i, testCase, ) } } func TestSDPSemantics_JSON(t *testing.T) { testCases := []struct { value SDPSemantics JSON []byte }{ {SDPSemanticsUnifiedPlanWithFallback, []byte("\"unified-plan-with-fallback\"")}, {SDPSemanticsPlanB, []byte("\"plan-b\"")}, {SDPSemanticsUnifiedPlan, []byte("\"unified-plan\"")}, } for i, testCase := range testCases { res, err := json.Marshal(testCase.value) assert.NoError(t, err) assert.Equal(t, testCase.JSON, res, "testCase: %d %v", i, testCase, ) var v SDPSemantics err = json.Unmarshal(testCase.JSON, &v) assert.NoError(t, err) assert.Equal(t, v, testCase.value) } } // The following tests are for non-standard SDP semantics // (i.e. not unified-unified) func getMdNames(sdp *sdp.SessionDescription) []string { mdNames := make([]string, 0, len(sdp.MediaDescriptions)) for _, media := range sdp.MediaDescriptions { mdNames = append(mdNames, media.MediaName.Media) } return mdNames } func extractSsrcList(md *sdp.MediaDescription) []string { ssrcMap := map[string]struct{}{} for _, attr := range md.Attributes { if attr.Key == sdp.AttrKeySSRC { ssrc := strings.Fields(attr.Value)[0] ssrcMap[ssrc] = struct{}{} } } ssrcList := make([]string, 0, len(ssrcMap)) for ssrc := range ssrcMap { ssrcList = append(ssrcList, ssrc) } return ssrcList } func TestSDPSemantics_PlanBOfferTransceivers(t *testing.T) { report := test.CheckRoutines(t) defer report() lim := test.TimeOut(time.Second * 30) defer lim.Stop() opc, err := NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsPlanB, }) assert.NoError(t, err) _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendrecv, }) assert.NoError(t, err) _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendrecv, }) assert.NoError(t, err) _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendrecv, }) assert.NoError(t, err) _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{ Direction: RTPTransceiverDirectionSendrecv, }) assert.NoError(t, err) offer, err := opc.CreateOffer(nil) assert.NoError(t, err) mdNames := getMdNames(offer.parsed) assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"}) // Verify that each section has 2 SSRCs (one for each transceiver) for _, section := range []string{"video", "audio"} { for _, media := range offer.parsed.MediaDescriptions { if media.MediaName.Media == section { assert.Len(t, extractSsrcList(media), 2) } } } apc, err := NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsPlanB, }) assert.NoError(t, err) assert.NoError(t, apc.SetRemoteDescription(offer)) answer, err := apc.CreateAnswer(nil) assert.NoError(t, err) mdNames = getMdNames(answer.parsed) assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"}) closePairNow(t, apc, opc) } func TestSDPSemantics_PlanBAnswerSenders(t *testing.T) { report := test.CheckRoutines(t) defer report() lim := test.TimeOut(time.Second * 30) defer lim.Stop() opc, err := NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsPlanB, }) assert.NoError(t, err) _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ Direction: RTPTransceiverDirectionRecvonly, }) assert.NoError(t, err) _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{ Direction: RTPTransceiverDirectionRecvonly, }) assert.NoError(t, err) offer, err := opc.CreateOffer(nil) assert.NoError(t, err) assert.ObjectsAreEqual(getMdNames(offer.parsed), []string{"video", "audio", "data"}) apc, err := NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsPlanB, }) assert.NoError(t, err) video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1") assert.NoError(t, err) _, err = apc.AddTrack(video1) assert.NoError(t, err) video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2") assert.NoError(t, err) _, err = apc.AddTrack(video2) assert.NoError(t, err) audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "3", "3") assert.NoError(t, err) _, err = apc.AddTrack(audio1) assert.NoError(t, err) audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "4", "4") assert.NoError(t, err) _, err = apc.AddTrack(audio2) assert.NoError(t, err) assert.NoError(t, apc.SetRemoteDescription(offer)) answer, err := apc.CreateAnswer(nil) assert.NoError(t, err) assert.ObjectsAreEqual(getMdNames(answer.parsed), []string{"video", "audio", "data"}) // Verify that each section has 2 SSRCs (one for each sender) for _, section := range []string{"video", "audio"} { for _, media := range answer.parsed.MediaDescriptions { if media.MediaName.Media == section { assert.Lenf(t, extractSsrcList(media), 2, "%q should have 2 SSRCs in Plan-B mode", section) } } } closePairNow(t, apc, opc) } func TestSDPSemantics_UnifiedPlanWithFallback(t *testing.T) { report := test.CheckRoutines(t) defer report() lim := test.TimeOut(time.Second * 30) defer lim.Stop() opc, err := NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsPlanB, }) assert.NoError(t, err) _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{ Direction: RTPTransceiverDirectionRecvonly, }) assert.NoError(t, err) _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{ Direction: RTPTransceiverDirectionRecvonly, }) assert.NoError(t, err) offer, err := opc.CreateOffer(nil) assert.NoError(t, err) assert.ObjectsAreEqual(getMdNames(offer.parsed), []string{"video", "audio", "data"}) apc, err := NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsUnifiedPlanWithFallback, }) assert.NoError(t, err) video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1") assert.NoError(t, err) _, err = apc.AddTrack(video1) assert.NoError(t, err) video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2") assert.NoError(t, err) _, err = apc.AddTrack(video2) assert.NoError(t, err) audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "3", "3") assert.NoError(t, err) _, err = apc.AddTrack(audio1) assert.NoError(t, err) audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "4", "4") assert.NoError(t, err) _, err = apc.AddTrack(audio2) assert.NoError(t, err) assert.NoError(t, apc.SetRemoteDescription(offer)) answer, err := apc.CreateAnswer(nil) assert.NoError(t, err) assert.ObjectsAreEqual(getMdNames(answer.parsed), []string{"video", "audio", "data"}) extractSsrcList := func(md *sdp.MediaDescription) []string { ssrcMap := map[string]struct{}{} for _, attr := range md.Attributes { if attr.Key == sdp.AttrKeySSRC { ssrc := strings.Fields(attr.Value)[0] ssrcMap[ssrc] = struct{}{} } } ssrcList := make([]string, 0, len(ssrcMap)) for ssrc := range ssrcMap { ssrcList = append(ssrcList, ssrc) } return ssrcList } // Verify that each section has 2 SSRCs (one for each sender) for _, section := range []string{"video", "audio"} { for _, media := range answer.parsed.MediaDescriptions { if media.MediaName.Media == section { assert.Lenf(t, extractSsrcList(media), 2, "%q should have 2 SSRCs in Plan-B fallback mode", section) } } } closePairNow(t, apc, opc) } // Assert that we can catch Remote SessionDescription that don't match our Semantics func TestSDPSemantics_SetRemoteDescription_Mismatch(t *testing.T) { planBOffer := "v=0\r\no=- 4648475892259889561 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video audio\r\na=ice-ufrag:1hhfzwf0ijpzm\r\na=ice-pwd:jm5puo2ab1op3vs59ca53bdk7s\r\na=fingerprint:sha-256 40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:video\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\na=ssrc:1505338584 cname:10000000b5810aac\r\na=ssrc:1 cname:trackB\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:audio\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=ssrc:697641945 cname:10000000b5810aac\r\n" unifiedPlanOffer := "v=0\r\no=- 4648475892259889561 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=ice-ufrag:1hhfzwf0ijpzm\r\na=ice-pwd:jm5puo2ab1op3vs59ca53bdk7s\r\na=fingerprint:sha-256 40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:0\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\na=ssrc:1505338584 cname:10000000b5810aac\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:1\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=ssrc:697641945 cname:10000000b5810aac\r\n" report := test.CheckRoutines(t) defer report() lim := test.TimeOut(time.Second * 30) defer lim.Stop() t.Run("PlanB", func(t *testing.T) { pc, err := NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsUnifiedPlan, }) assert.NoError(t, err) err = pc.SetRemoteDescription(SessionDescription{SDP: planBOffer, Type: SDPTypeOffer}) assert.NoError(t, err) _, err = pc.CreateAnswer(nil) assert.True(t, errors.Is(err, ErrIncorrectSDPSemantics)) assert.NoError(t, pc.Close()) }) t.Run("UnifiedPlan", func(t *testing.T) { pc, err := NewPeerConnection(Configuration{ SDPSemantics: SDPSemanticsPlanB, }) assert.NoError(t, err) err = pc.SetRemoteDescription(SessionDescription{SDP: unifiedPlanOffer, Type: SDPTypeOffer}) assert.NoError(t, err) _, err = pc.CreateAnswer(nil) assert.True(t, errors.Is(err, ErrIncorrectSDPSemantics)) assert.NoError(t, pc.Close()) }) } webrtc-3.1.56/sdptype.go000066400000000000000000000050131437620512100151130ustar00rootroot00000000000000package webrtc import ( "encoding/json" "strings" ) // SDPType describes the type of an SessionDescription. type SDPType int const ( // SDPTypeOffer indicates that a description MUST be treated as an SDP // offer. SDPTypeOffer SDPType = iota + 1 // SDPTypePranswer indicates that a description MUST be treated as an // SDP answer, but not a final answer. A description used as an SDP // pranswer may be applied as a response to an SDP offer, or an update to // a previously sent SDP pranswer. SDPTypePranswer // SDPTypeAnswer indicates that a description MUST be treated as an SDP // final answer, and the offer-answer exchange MUST be considered complete. // A description used as an SDP answer may be applied as a response to an // SDP offer or as an update to a previously sent SDP pranswer. SDPTypeAnswer // SDPTypeRollback indicates that a description MUST be treated as // canceling the current SDP negotiation and moving the SDP offer and // answer back to what it was in the previous stable state. Note the // local or remote SDP descriptions in the previous stable state could be // null if there has not yet been a successful offer-answer negotiation. SDPTypeRollback ) // This is done this way because of a linter. const ( sdpTypeOfferStr = "offer" sdpTypePranswerStr = "pranswer" sdpTypeAnswerStr = "answer" sdpTypeRollbackStr = "rollback" ) // NewSDPType creates an SDPType from a string func NewSDPType(raw string) SDPType { switch raw { case sdpTypeOfferStr: return SDPTypeOffer case sdpTypePranswerStr: return SDPTypePranswer case sdpTypeAnswerStr: return SDPTypeAnswer case sdpTypeRollbackStr: return SDPTypeRollback default: return SDPType(Unknown) } } func (t SDPType) String() string { switch t { case SDPTypeOffer: return sdpTypeOfferStr case SDPTypePranswer: return sdpTypePranswerStr case SDPTypeAnswer: return sdpTypeAnswerStr case SDPTypeRollback: return sdpTypeRollbackStr default: return ErrUnknownType.Error() } } // MarshalJSON enables JSON marshaling of a SDPType func (t SDPType) MarshalJSON() ([]byte, error) { return json.Marshal(t.String()) } // UnmarshalJSON enables JSON unmarshaling of a SDPType func (t *SDPType) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { return err } switch strings.ToLower(s) { default: return ErrUnknownType case "offer": *t = SDPTypeOffer case "pranswer": *t = SDPTypePranswer case "answer": *t = SDPTypeAnswer case "rollback": *t = SDPTypeRollback } return nil } webrtc-3.1.56/sdptype_test.go000066400000000000000000000016771437620512100161660ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewSDPType(t *testing.T) { testCases := []struct { sdpTypeString string expectedSDPType SDPType }{ {unknownStr, SDPType(Unknown)}, {"offer", SDPTypeOffer}, {"pranswer", SDPTypePranswer}, {"answer", SDPTypeAnswer}, {"rollback", SDPTypeRollback}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedSDPType, NewSDPType(testCase.sdpTypeString), "testCase: %d %v", i, testCase, ) } } func TestSDPType_String(t *testing.T) { testCases := []struct { sdpType SDPType expectedString string }{ {SDPType(Unknown), unknownStr}, {SDPTypeOffer, "offer"}, {SDPTypePranswer, "pranswer"}, {SDPTypeAnswer, "answer"}, {SDPTypeRollback, "rollback"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.sdpType.String(), "testCase: %d %v", i, testCase, ) } } webrtc-3.1.56/sessiondescription.go000066400000000000000000000010531437620512100173520ustar00rootroot00000000000000package webrtc import ( "github.com/pion/sdp/v3" ) // SessionDescription is used to expose local and remote session descriptions. type SessionDescription struct { Type SDPType `json:"type"` SDP string `json:"sdp"` // This will never be initialized by callers, internal use only parsed *sdp.SessionDescription } // Unmarshal is a helper to deserialize the sdp func (sd *SessionDescription) Unmarshal() (*sdp.SessionDescription, error) { sd.parsed = &sdp.SessionDescription{} err := sd.parsed.Unmarshal([]byte(sd.SDP)) return sd.parsed, err } webrtc-3.1.56/sessiondescription_test.go000066400000000000000000000040701437620512100204130ustar00rootroot00000000000000package webrtc import ( "encoding/json" "reflect" "testing" "github.com/stretchr/testify/assert" ) func TestSessionDescription_JSON(t *testing.T) { testCases := []struct { desc SessionDescription expectedString string unmarshalErr error }{ {SessionDescription{Type: SDPTypeOffer, SDP: "sdp"}, `{"type":"offer","sdp":"sdp"}`, nil}, {SessionDescription{Type: SDPTypePranswer, SDP: "sdp"}, `{"type":"pranswer","sdp":"sdp"}`, nil}, {SessionDescription{Type: SDPTypeAnswer, SDP: "sdp"}, `{"type":"answer","sdp":"sdp"}`, nil}, {SessionDescription{Type: SDPTypeRollback, SDP: "sdp"}, `{"type":"rollback","sdp":"sdp"}`, nil}, {SessionDescription{Type: SDPType(Unknown), SDP: "sdp"}, `{"type":"unknown","sdp":"sdp"}`, ErrUnknownType}, } for i, testCase := range testCases { descData, err := json.Marshal(testCase.desc) assert.Nil(t, err, "testCase: %d %v marshal err: %v", i, testCase, err, ) assert.Equal(t, string(descData), testCase.expectedString, "testCase: %d %v", i, testCase, ) var desc SessionDescription err = json.Unmarshal(descData, &desc) if testCase.unmarshalErr != nil { assert.Equal(t, err, testCase.unmarshalErr, "testCase: %d %v", i, testCase, ) continue } assert.Nil(t, err, "testCase: %d %v unmarshal err: %v", i, testCase, err, ) assert.Equal(t, desc, testCase.desc, "testCase: %d %v", i, testCase, ) } } func TestSessionDescription_Unmarshal(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) offer, err := pc.CreateOffer(nil) assert.NoError(t, err) desc := SessionDescription{ Type: offer.Type, SDP: offer.SDP, } assert.Nil(t, desc.parsed) parsed1, err := desc.Unmarshal() assert.NotNil(t, parsed1) assert.NotNil(t, desc.parsed) assert.NoError(t, err) parsed2, err2 := desc.Unmarshal() assert.NotNil(t, parsed2) assert.NoError(t, err2) assert.NoError(t, pc.Close()) // check if the two parsed results _really_ match, could be affected by internal caching assert.True(t, reflect.DeepEqual(parsed1, parsed2)) } webrtc-3.1.56/settingengine.go000066400000000000000000000326341437620512100162770ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "io" "net" "time" "github.com/pion/dtls/v2" "github.com/pion/ice/v2" "github.com/pion/logging" "github.com/pion/transport/v2" "github.com/pion/transport/v2/packetio" "github.com/pion/transport/v2/vnet" "golang.org/x/net/proxy" ) // SettingEngine allows influencing behavior in ways that are not // supported by the WebRTC API. This allows us to support additional // use-cases without deviating from the WebRTC API elsewhere. type SettingEngine struct { ephemeralUDP struct { PortMin uint16 PortMax uint16 } detach struct { DataChannels bool } timeout struct { ICEDisconnectedTimeout *time.Duration ICEFailedTimeout *time.Duration ICEKeepaliveInterval *time.Duration ICEHostAcceptanceMinWait *time.Duration ICESrflxAcceptanceMinWait *time.Duration ICEPrflxAcceptanceMinWait *time.Duration ICERelayAcceptanceMinWait *time.Duration } candidates struct { ICELite bool ICENetworkTypes []NetworkType InterfaceFilter func(string) bool IPFilter func(net.IP) bool NAT1To1IPs []string NAT1To1IPCandidateType ICECandidateType MulticastDNSMode ice.MulticastDNSMode MulticastDNSHostName string UsernameFragment string Password string IncludeLoopbackCandidate bool } replayProtection struct { DTLS *uint SRTP *uint SRTCP *uint } dtls struct { retransmissionInterval time.Duration } sctp struct { maxReceiveBufferSize uint32 } sdpMediaLevelFingerprints bool answeringDTLSRole DTLSRole disableCertificateFingerprintVerification bool disableSRTPReplayProtection bool disableSRTCPReplayProtection bool net transport.Net BufferFactory func(packetType packetio.BufferPacketType, ssrc uint32) io.ReadWriteCloser LoggerFactory logging.LoggerFactory iceTCPMux ice.TCPMux iceUDPMux ice.UDPMux iceProxyDialer proxy.Dialer disableMediaEngineCopy bool srtpProtectionProfiles []dtls.SRTPProtectionProfile receiveMTU uint } // getReceiveMTU returns the configured MTU. If SettingEngine's MTU is configured to 0 it returns the default func (e *SettingEngine) getReceiveMTU() uint { if e.receiveMTU != 0 { return e.receiveMTU } return receiveMTU } // DetachDataChannels enables detaching data channels. When enabled // data channels have to be detached in the OnOpen callback using the // DataChannel.Detach method. func (e *SettingEngine) DetachDataChannels() { e.detach.DataChannels = true } // SetSRTPProtectionProfiles allows the user to override the default SRTP Protection Profiles // The default srtp protection profiles are provided by the function `defaultSrtpProtectionProfiles` func (e *SettingEngine) SetSRTPProtectionProfiles(profiles ...dtls.SRTPProtectionProfile) { e.srtpProtectionProfiles = profiles } // SetICETimeouts sets the behavior around ICE Timeouts // // disconnectedTimeout: // // Duration without network activity before an Agent is considered disconnected. Default is 5 Seconds // // failedTimeout: // // Duration without network activity before an Agent is considered failed after disconnected. Default is 25 Seconds // // keepAliveInterval: // // How often the ICE Agent sends extra traffic if there is no activity, if media is flowing no traffic will be sent. Default is 2 seconds func (e *SettingEngine) SetICETimeouts(disconnectedTimeout, failedTimeout, keepAliveInterval time.Duration) { e.timeout.ICEDisconnectedTimeout = &disconnectedTimeout e.timeout.ICEFailedTimeout = &failedTimeout e.timeout.ICEKeepaliveInterval = &keepAliveInterval } // SetHostAcceptanceMinWait sets the ICEHostAcceptanceMinWait func (e *SettingEngine) SetHostAcceptanceMinWait(t time.Duration) { e.timeout.ICEHostAcceptanceMinWait = &t } // SetSrflxAcceptanceMinWait sets the ICESrflxAcceptanceMinWait func (e *SettingEngine) SetSrflxAcceptanceMinWait(t time.Duration) { e.timeout.ICESrflxAcceptanceMinWait = &t } // SetPrflxAcceptanceMinWait sets the ICEPrflxAcceptanceMinWait func (e *SettingEngine) SetPrflxAcceptanceMinWait(t time.Duration) { e.timeout.ICEPrflxAcceptanceMinWait = &t } // SetRelayAcceptanceMinWait sets the ICERelayAcceptanceMinWait func (e *SettingEngine) SetRelayAcceptanceMinWait(t time.Duration) { e.timeout.ICERelayAcceptanceMinWait = &t } // SetEphemeralUDPPortRange limits the pool of ephemeral ports that // ICE UDP connections can allocate from. This affects both host candidates, // and the local address of server reflexive candidates. func (e *SettingEngine) SetEphemeralUDPPortRange(portMin, portMax uint16) error { if portMax < portMin { return ice.ErrPort } e.ephemeralUDP.PortMin = portMin e.ephemeralUDP.PortMax = portMax return nil } // SetLite configures whether or not the ice agent should be a lite agent func (e *SettingEngine) SetLite(lite bool) { e.candidates.ICELite = lite } // SetNetworkTypes configures what types of candidate networks are supported // during local and server reflexive gathering. func (e *SettingEngine) SetNetworkTypes(candidateTypes []NetworkType) { e.candidates.ICENetworkTypes = candidateTypes } // SetInterfaceFilter sets the filtering functions when gathering ICE candidates // This can be used to exclude certain network interfaces from ICE. Which may be // useful if you know a certain interface will never succeed, or if you wish to reduce // the amount of information you wish to expose to the remote peer func (e *SettingEngine) SetInterfaceFilter(filter func(string) bool) { e.candidates.InterfaceFilter = filter } // SetIPFilter sets the filtering functions when gathering ICE candidates // This can be used to exclude certain ip from ICE. Which may be // useful if you know a certain ip will never succeed, or if you wish to reduce // the amount of information you wish to expose to the remote peer func (e *SettingEngine) SetIPFilter(filter func(net.IP) bool) { e.candidates.IPFilter = filter } // SetNAT1To1IPs sets a list of external IP addresses of 1:1 (D)NAT // and a candidate type for which the external IP address is used. // This is useful when you host a server using Pion on an AWS EC2 instance // which has a private address, behind a 1:1 DNAT with a public IP (e.g. // Elastic IP). In this case, you can give the public IP address so that // Pion will use the public IP address in its candidate instead of the private // IP address. The second argument, candidateType, is used to tell Pion which // type of candidate should use the given public IP address. // Two types of candidates are supported: // // ICECandidateTypeHost: // // The public IP address will be used for the host candidate in the SDP. // // ICECandidateTypeSrflx: // // A server reflexive candidate with the given public IP address will be added to the SDP. // // Please note that if you choose ICECandidateTypeHost, then the private IP address // won't be advertised with the peer. Also, this option cannot be used along with mDNS. // // If you choose ICECandidateTypeSrflx, it simply adds a server reflexive candidate // with the public IP. The host candidate is still available along with mDNS // capabilities unaffected. Also, you cannot give STUN server URL at the same time. // It will result in an error otherwise. func (e *SettingEngine) SetNAT1To1IPs(ips []string, candidateType ICECandidateType) { e.candidates.NAT1To1IPs = ips e.candidates.NAT1To1IPCandidateType = candidateType } // SetIncludeLoopbackCandidate enable pion to gather loopback candidates, it is useful // for some VM have public IP mapped to loopback interface func (e *SettingEngine) SetIncludeLoopbackCandidate(include bool) { e.candidates.IncludeLoopbackCandidate = include } // SetAnsweringDTLSRole sets the DTLS role that is selected when offering // The DTLS role controls if the WebRTC Client as a client or server. This // may be useful when interacting with non-compliant clients or debugging issues. // // DTLSRoleActive: // // Act as DTLS Client, send the ClientHello and starts the handshake // // DTLSRolePassive: // // Act as DTLS Server, wait for ClientHello func (e *SettingEngine) SetAnsweringDTLSRole(role DTLSRole) error { if role != DTLSRoleClient && role != DTLSRoleServer { return errSettingEngineSetAnsweringDTLSRole } e.answeringDTLSRole = role return nil } // SetVNet sets the VNet instance that is passed to pion/ice // // VNet is a virtual network layer for Pion, allowing users to simulate // different topologies, latency, loss and jitter. This can be useful for // learning WebRTC concepts or testing your application in a lab environment // Deprecated: Please use SetNet() func (e *SettingEngine) SetVNet(vnet *vnet.Net) { e.SetNet(vnet) } // SetNet sets the Net instance that is passed to pion/ice // // Net is an network interface layer for Pion, allowing users to replace // Pions network stack with a custom implementation. func (e *SettingEngine) SetNet(net transport.Net) { e.net = net } // SetICEMulticastDNSMode controls if pion/ice queries and generates mDNS ICE Candidates func (e *SettingEngine) SetICEMulticastDNSMode(multicastDNSMode ice.MulticastDNSMode) { e.candidates.MulticastDNSMode = multicastDNSMode } // SetMulticastDNSHostName sets a static HostName to be used by pion/ice instead of generating one on startup // // This should only be used for a single PeerConnection. Having multiple PeerConnections with the same HostName will cause // undefined behavior func (e *SettingEngine) SetMulticastDNSHostName(hostName string) { e.candidates.MulticastDNSHostName = hostName } // SetICECredentials sets a staic uFrag/uPwd to be used by pion/ice // // This is useful if you want to do signalless WebRTC session, or having a reproducible environment with static credentials func (e *SettingEngine) SetICECredentials(usernameFragment, password string) { e.candidates.UsernameFragment = usernameFragment e.candidates.Password = password } // DisableCertificateFingerprintVerification disables fingerprint verification after DTLS Handshake has finished func (e *SettingEngine) DisableCertificateFingerprintVerification(isDisabled bool) { e.disableCertificateFingerprintVerification = isDisabled } // SetDTLSReplayProtectionWindow sets a replay attack protection window size of DTLS connection. func (e *SettingEngine) SetDTLSReplayProtectionWindow(n uint) { e.replayProtection.DTLS = &n } // SetSRTPReplayProtectionWindow sets a replay attack protection window size of SRTP session. func (e *SettingEngine) SetSRTPReplayProtectionWindow(n uint) { e.disableSRTPReplayProtection = false e.replayProtection.SRTP = &n } // SetSRTCPReplayProtectionWindow sets a replay attack protection window size of SRTCP session. func (e *SettingEngine) SetSRTCPReplayProtectionWindow(n uint) { e.disableSRTCPReplayProtection = false e.replayProtection.SRTCP = &n } // DisableSRTPReplayProtection disables SRTP replay protection. func (e *SettingEngine) DisableSRTPReplayProtection(isDisabled bool) { e.disableSRTPReplayProtection = isDisabled } // DisableSRTCPReplayProtection disables SRTCP replay protection. func (e *SettingEngine) DisableSRTCPReplayProtection(isDisabled bool) { e.disableSRTCPReplayProtection = isDisabled } // SetSDPMediaLevelFingerprints configures the logic for DTLS Fingerprint insertion // If true, fingerprints will be inserted in the sdp at the fingerprint // level, instead of the session level. This helps with compatibility with // some webrtc implementations. func (e *SettingEngine) SetSDPMediaLevelFingerprints(sdpMediaLevelFingerprints bool) { e.sdpMediaLevelFingerprints = sdpMediaLevelFingerprints } // SetICETCPMux enables ICE-TCP when set to a non-nil value. Make sure that // NetworkTypeTCP4 or NetworkTypeTCP6 is enabled as well. func (e *SettingEngine) SetICETCPMux(tcpMux ice.TCPMux) { e.iceTCPMux = tcpMux } // SetICEUDPMux allows ICE traffic to come through a single UDP port, drastically // simplifying deployments where ports will need to be opened/forwarded. // UDPMux should be started prior to creating PeerConnections. func (e *SettingEngine) SetICEUDPMux(udpMux ice.UDPMux) { e.iceUDPMux = udpMux } // SetICEProxyDialer sets the proxy dialer interface based on golang.org/x/net/proxy. func (e *SettingEngine) SetICEProxyDialer(d proxy.Dialer) { e.iceProxyDialer = d } // DisableMediaEngineCopy stops the MediaEngine from being copied. This allows a user to modify // the MediaEngine after the PeerConnection has been constructed. This is useful if you wish to // modify codecs after signaling. Make sure not to share MediaEngines between PeerConnections. func (e *SettingEngine) DisableMediaEngineCopy(isDisabled bool) { e.disableMediaEngineCopy = isDisabled } // SetReceiveMTU sets the size of read buffer that copies incoming packets. This is optional. // Leave this 0 for the default receiveMTU func (e *SettingEngine) SetReceiveMTU(receiveMTU uint) { e.receiveMTU = receiveMTU } // SetDTLSRetransmissionInterval sets the retranmission interval for DTLS. func (e *SettingEngine) SetDTLSRetransmissionInterval(interval time.Duration) { e.dtls.retransmissionInterval = interval } // SetSCTPMaxReceiveBufferSize sets the maximum receive buffer size. // Leave this 0 for the default maxReceiveBufferSize. func (e *SettingEngine) SetSCTPMaxReceiveBufferSize(maxReceiveBufferSize uint32) { e.sctp.maxReceiveBufferSize = maxReceiveBufferSize } webrtc-3.1.56/settingengine_js.go000066400000000000000000000010741437620512100167650ustar00rootroot00000000000000//go:build js && wasm // +build js,wasm package webrtc // SettingEngine allows influencing behavior in ways that are not // supported by the WebRTC API. This allows us to support additional // use-cases without deviating from the WebRTC API elsewhere. type SettingEngine struct { detach struct { DataChannels bool } } // DetachDataChannels enables detaching data channels. When enabled // data channels have to be detached in the OnOpen callback using the // DataChannel.Detach method. func (e *SettingEngine) DetachDataChannels() { e.detach.DataChannels = true } webrtc-3.1.56/settingengine_test.go000066400000000000000000000153131437620512100173310ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "net" "testing" "time" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) func TestSetEphemeralUDPPortRange(t *testing.T) { s := SettingEngine{} if s.ephemeralUDP.PortMin != 0 || s.ephemeralUDP.PortMax != 0 { t.Fatalf("SettingEngine defaults aren't as expected.") } // set bad ephemeral ports if err := s.SetEphemeralUDPPortRange(3000, 2999); err == nil { t.Fatalf("Setting engine should fail bad ephemeral ports.") } if err := s.SetEphemeralUDPPortRange(3000, 4000); err != nil { t.Fatalf("Setting engine failed valid port range: %s", err) } if s.ephemeralUDP.PortMin != 3000 || s.ephemeralUDP.PortMax != 4000 { t.Fatalf("Setting engine ports do not reflect expected range") } } func TestSetConnectionTimeout(t *testing.T) { s := SettingEngine{} var nilDuration *time.Duration assert.Equal(t, s.timeout.ICEDisconnectedTimeout, nilDuration) assert.Equal(t, s.timeout.ICEFailedTimeout, nilDuration) assert.Equal(t, s.timeout.ICEKeepaliveInterval, nilDuration) s.SetICETimeouts(1*time.Second, 2*time.Second, 3*time.Second) assert.Equal(t, *s.timeout.ICEDisconnectedTimeout, 1*time.Second) assert.Equal(t, *s.timeout.ICEFailedTimeout, 2*time.Second) assert.Equal(t, *s.timeout.ICEKeepaliveInterval, 3*time.Second) } func TestDetachDataChannels(t *testing.T) { s := SettingEngine{} if s.detach.DataChannels { t.Fatalf("SettingEngine defaults aren't as expected.") } s.DetachDataChannels() if !s.detach.DataChannels { t.Fatalf("Failed to enable detached data channels.") } } func TestSetNAT1To1IPs(t *testing.T) { s := SettingEngine{} if s.candidates.NAT1To1IPs != nil { t.Errorf("Invalid default value") } if s.candidates.NAT1To1IPCandidateType != 0 { t.Errorf("Invalid default value") } ips := []string{"1.2.3.4"} typ := ICECandidateTypeHost s.SetNAT1To1IPs(ips, typ) if len(s.candidates.NAT1To1IPs) != 1 || s.candidates.NAT1To1IPs[0] != "1.2.3.4" { t.Fatalf("Failed to set NAT1To1IPs") } if s.candidates.NAT1To1IPCandidateType != typ { t.Fatalf("Failed to set NAT1To1IPCandidateType") } } func TestSetAnsweringDTLSRole(t *testing.T) { s := SettingEngine{} assert.Error(t, s.SetAnsweringDTLSRole(DTLSRoleAuto), "SetAnsweringDTLSRole can only be called with DTLSRoleClient or DTLSRoleServer") assert.Error(t, s.SetAnsweringDTLSRole(DTLSRole(0)), "SetAnsweringDTLSRole can only be called with DTLSRoleClient or DTLSRoleServer") } func TestSetReplayProtection(t *testing.T) { s := SettingEngine{} if s.replayProtection.DTLS != nil || s.replayProtection.SRTP != nil || s.replayProtection.SRTCP != nil { t.Fatalf("SettingEngine defaults aren't as expected.") } s.SetDTLSReplayProtectionWindow(128) s.SetSRTPReplayProtectionWindow(64) s.SetSRTCPReplayProtectionWindow(32) if s.replayProtection.DTLS == nil || *s.replayProtection.DTLS != 128 { t.Errorf("Failed to set DTLS replay protection window") } if s.replayProtection.SRTP == nil || *s.replayProtection.SRTP != 64 { t.Errorf("Failed to set SRTP replay protection window") } if s.replayProtection.SRTCP == nil || *s.replayProtection.SRTCP != 32 { t.Errorf("Failed to set SRTCP replay protection window") } } func TestSettingEngine_SetICETCP(t *testing.T) { report := test.CheckRoutines(t) defer report() listener, err := net.ListenTCP("tcp", &net.TCPAddr{}) if err != nil { panic(err) } defer func() { _ = listener.Close() }() tcpMux := NewICETCPMux(nil, listener, 8) defer func() { _ = tcpMux.Close() }() settingEngine := SettingEngine{} settingEngine.SetICETCPMux(tcpMux) assert.Equal(t, tcpMux, settingEngine.iceTCPMux) } func TestSettingEngine_SetDisableMediaEngineCopy(t *testing.T) { t.Run("Copy", func(t *testing.T) { m := &MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) api := NewAPI(WithMediaEngine(m)) offerer, answerer, err := api.newPair(Configuration{}) assert.NoError(t, err) _, err = offerer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) assert.NoError(t, signalPair(offerer, answerer)) // Assert that the MediaEngine the user created isn't modified assert.False(t, m.negotiatedVideo) assert.Empty(t, m.negotiatedVideoCodecs) // Assert that the internal MediaEngine is modified assert.True(t, offerer.api.mediaEngine.negotiatedVideo) assert.NotEmpty(t, offerer.api.mediaEngine.negotiatedVideoCodecs) closePairNow(t, offerer, answerer) newOfferer, newAnswerer, err := api.newPair(Configuration{}) assert.NoError(t, err) // Assert that the first internal MediaEngine hasn't been cleared assert.True(t, offerer.api.mediaEngine.negotiatedVideo) assert.NotEmpty(t, offerer.api.mediaEngine.negotiatedVideoCodecs) // Assert that the new internal MediaEngine isn't modified assert.False(t, newOfferer.api.mediaEngine.negotiatedVideo) assert.Empty(t, newAnswerer.api.mediaEngine.negotiatedVideoCodecs) closePairNow(t, newOfferer, newAnswerer) }) t.Run("No Copy", func(t *testing.T) { m := &MediaEngine{} assert.NoError(t, m.RegisterDefaultCodecs()) s := SettingEngine{} s.DisableMediaEngineCopy(true) api := NewAPI(WithMediaEngine(m), WithSettingEngine(s)) offerer, answerer, err := api.newPair(Configuration{}) assert.NoError(t, err) _, err = offerer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) assert.NoError(t, signalPair(offerer, answerer)) // Assert that the user MediaEngine was modified, so no copy happened assert.True(t, m.negotiatedVideo) assert.NotEmpty(t, m.negotiatedVideoCodecs) closePairNow(t, offerer, answerer) offerer, answerer, err = api.newPair(Configuration{}) assert.NoError(t, err) // Assert that the new internal MediaEngine was modified, so no copy happened assert.True(t, offerer.api.mediaEngine.negotiatedVideo) assert.NotEmpty(t, offerer.api.mediaEngine.negotiatedVideoCodecs) closePairNow(t, offerer, answerer) }) } func TestSetDTLSRetransmissionInterval(t *testing.T) { s := SettingEngine{} if s.dtls.retransmissionInterval != 0 { t.Fatalf("SettingEngine defaults aren't as expected.") } s.SetDTLSRetransmissionInterval(100 * time.Millisecond) if s.dtls.retransmissionInterval == 0 || s.dtls.retransmissionInterval != 100*time.Millisecond { t.Errorf("Failed to set DTLS retransmission interval") } s.SetDTLSRetransmissionInterval(1 * time.Second) if s.dtls.retransmissionInterval == 0 || s.dtls.retransmissionInterval != 1*time.Second { t.Errorf("Failed to set DTLS retransmission interval") } } func TestSetSCTPMaxReceiverBufferSize(t *testing.T) { s := SettingEngine{} assert.Equal(t, uint32(0), s.sctp.maxReceiveBufferSize) expSize := uint32(4 * 1024 * 1024) s.SetSCTPMaxReceiveBufferSize(expSize) assert.Equal(t, expSize, s.sctp.maxReceiveBufferSize) } webrtc-3.1.56/signalingstate.go000066400000000000000000000130341437620512100164410ustar00rootroot00000000000000package webrtc import ( "fmt" "sync/atomic" "github.com/pion/webrtc/v3/pkg/rtcerr" ) type stateChangeOp int const ( stateChangeOpSetLocal stateChangeOp = iota + 1 stateChangeOpSetRemote ) func (op stateChangeOp) String() string { switch op { case stateChangeOpSetLocal: return "SetLocal" case stateChangeOpSetRemote: return "SetRemote" default: return "Unknown State Change Operation" } } // SignalingState indicates the signaling state of the offer/answer process. type SignalingState int32 const ( // SignalingStateStable indicates there is no offer/answer exchange in // progress. This is also the initial state, in which case the local and // remote descriptions are nil. SignalingStateStable SignalingState = iota + 1 // SignalingStateHaveLocalOffer indicates that a local description, of // type "offer", has been successfully applied. SignalingStateHaveLocalOffer // SignalingStateHaveRemoteOffer indicates that a remote description, of // type "offer", has been successfully applied. SignalingStateHaveRemoteOffer // SignalingStateHaveLocalPranswer indicates that a remote description // of type "offer" has been successfully applied and a local description // of type "pranswer" has been successfully applied. SignalingStateHaveLocalPranswer // SignalingStateHaveRemotePranswer indicates that a local description // of type "offer" has been successfully applied and a remote description // of type "pranswer" has been successfully applied. SignalingStateHaveRemotePranswer // SignalingStateClosed indicates The PeerConnection has been closed. SignalingStateClosed ) // This is done this way because of a linter. const ( signalingStateStableStr = "stable" signalingStateHaveLocalOfferStr = "have-local-offer" signalingStateHaveRemoteOfferStr = "have-remote-offer" signalingStateHaveLocalPranswerStr = "have-local-pranswer" signalingStateHaveRemotePranswerStr = "have-remote-pranswer" signalingStateClosedStr = "closed" ) func newSignalingState(raw string) SignalingState { switch raw { case signalingStateStableStr: return SignalingStateStable case signalingStateHaveLocalOfferStr: return SignalingStateHaveLocalOffer case signalingStateHaveRemoteOfferStr: return SignalingStateHaveRemoteOffer case signalingStateHaveLocalPranswerStr: return SignalingStateHaveLocalPranswer case signalingStateHaveRemotePranswerStr: return SignalingStateHaveRemotePranswer case signalingStateClosedStr: return SignalingStateClosed default: return SignalingState(Unknown) } } func (t SignalingState) String() string { switch t { case SignalingStateStable: return signalingStateStableStr case SignalingStateHaveLocalOffer: return signalingStateHaveLocalOfferStr case SignalingStateHaveRemoteOffer: return signalingStateHaveRemoteOfferStr case SignalingStateHaveLocalPranswer: return signalingStateHaveLocalPranswerStr case SignalingStateHaveRemotePranswer: return signalingStateHaveRemotePranswerStr case SignalingStateClosed: return signalingStateClosedStr default: return ErrUnknownType.Error() } } // Get thread safe read value func (t *SignalingState) Get() SignalingState { return SignalingState(atomic.LoadInt32((*int32)(t))) } // Set thread safe write value func (t *SignalingState) Set(state SignalingState) { atomic.StoreInt32((*int32)(t), int32(state)) } func checkNextSignalingState(cur, next SignalingState, op stateChangeOp, sdpType SDPType) (SignalingState, error) { // nolint:gocognit // Special case for rollbacks if sdpType == SDPTypeRollback && cur == SignalingStateStable { return cur, &rtcerr.InvalidModificationError{ Err: errSignalingStateCannotRollback, } } // 4.3.1 valid state transitions switch cur { // nolint:exhaustive case SignalingStateStable: switch op { case stateChangeOpSetLocal: // stable->SetLocal(offer)->have-local-offer if sdpType == SDPTypeOffer && next == SignalingStateHaveLocalOffer { return next, nil } case stateChangeOpSetRemote: // stable->SetRemote(offer)->have-remote-offer if sdpType == SDPTypeOffer && next == SignalingStateHaveRemoteOffer { return next, nil } } case SignalingStateHaveLocalOffer: if op == stateChangeOpSetRemote { switch sdpType { // nolint:exhaustive // have-local-offer->SetRemote(answer)->stable case SDPTypeAnswer: if next == SignalingStateStable { return next, nil } // have-local-offer->SetRemote(pranswer)->have-remote-pranswer case SDPTypePranswer: if next == SignalingStateHaveRemotePranswer { return next, nil } } } case SignalingStateHaveRemotePranswer: if op == stateChangeOpSetRemote && sdpType == SDPTypeAnswer { // have-remote-pranswer->SetRemote(answer)->stable if next == SignalingStateStable { return next, nil } } case SignalingStateHaveRemoteOffer: if op == stateChangeOpSetLocal { switch sdpType { // nolint:exhaustive // have-remote-offer->SetLocal(answer)->stable case SDPTypeAnswer: if next == SignalingStateStable { return next, nil } // have-remote-offer->SetLocal(pranswer)->have-local-pranswer case SDPTypePranswer: if next == SignalingStateHaveLocalPranswer { return next, nil } } } case SignalingStateHaveLocalPranswer: if op == stateChangeOpSetLocal && sdpType == SDPTypeAnswer { // have-local-pranswer->SetLocal(answer)->stable if next == SignalingStateStable { return next, nil } } } return cur, &rtcerr.InvalidModificationError{ Err: fmt.Errorf("%w: %s->%s(%s)->%s", errSignalingStateProposedTransitionInvalid, cur, op, sdpType, next), } } webrtc-3.1.56/signalingstate_test.go000066400000000000000000000074171437620512100175100ustar00rootroot00000000000000package webrtc import ( "testing" "github.com/pion/webrtc/v3/pkg/rtcerr" "github.com/stretchr/testify/assert" ) func TestNewSignalingState(t *testing.T) { testCases := []struct { stateString string expectedState SignalingState }{ {unknownStr, SignalingState(Unknown)}, {"stable", SignalingStateStable}, {"have-local-offer", SignalingStateHaveLocalOffer}, {"have-remote-offer", SignalingStateHaveRemoteOffer}, {"have-local-pranswer", SignalingStateHaveLocalPranswer}, {"have-remote-pranswer", SignalingStateHaveRemotePranswer}, {"closed", SignalingStateClosed}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedState, newSignalingState(testCase.stateString), "testCase: %d %v", i, testCase, ) } } func TestSignalingState_String(t *testing.T) { testCases := []struct { state SignalingState expectedString string }{ {SignalingState(Unknown), unknownStr}, {SignalingStateStable, "stable"}, {SignalingStateHaveLocalOffer, "have-local-offer"}, {SignalingStateHaveRemoteOffer, "have-remote-offer"}, {SignalingStateHaveLocalPranswer, "have-local-pranswer"}, {SignalingStateHaveRemotePranswer, "have-remote-pranswer"}, {SignalingStateClosed, "closed"}, } for i, testCase := range testCases { assert.Equal(t, testCase.expectedString, testCase.state.String(), "testCase: %d %v", i, testCase, ) } } func TestSignalingState_Transitions(t *testing.T) { testCases := []struct { desc string current SignalingState next SignalingState op stateChangeOp sdpType SDPType expectedErr error }{ { "stable->SetLocal(offer)->have-local-offer", SignalingStateStable, SignalingStateHaveLocalOffer, stateChangeOpSetLocal, SDPTypeOffer, nil, }, { "stable->SetRemote(offer)->have-remote-offer", SignalingStateStable, SignalingStateHaveRemoteOffer, stateChangeOpSetRemote, SDPTypeOffer, nil, }, { "have-local-offer->SetRemote(answer)->stable", SignalingStateHaveLocalOffer, SignalingStateStable, stateChangeOpSetRemote, SDPTypeAnswer, nil, }, { "have-local-offer->SetRemote(pranswer)->have-remote-pranswer", SignalingStateHaveLocalOffer, SignalingStateHaveRemotePranswer, stateChangeOpSetRemote, SDPTypePranswer, nil, }, { "have-remote-pranswer->SetRemote(answer)->stable", SignalingStateHaveRemotePranswer, SignalingStateStable, stateChangeOpSetRemote, SDPTypeAnswer, nil, }, { "have-remote-offer->SetLocal(answer)->stable", SignalingStateHaveRemoteOffer, SignalingStateStable, stateChangeOpSetLocal, SDPTypeAnswer, nil, }, { "have-remote-offer->SetLocal(pranswer)->have-local-pranswer", SignalingStateHaveRemoteOffer, SignalingStateHaveLocalPranswer, stateChangeOpSetLocal, SDPTypePranswer, nil, }, { "have-local-pranswer->SetLocal(answer)->stable", SignalingStateHaveLocalPranswer, SignalingStateStable, stateChangeOpSetLocal, SDPTypeAnswer, nil, }, { "(invalid) stable->SetRemote(pranswer)->have-remote-pranswer", SignalingStateStable, SignalingStateHaveRemotePranswer, stateChangeOpSetRemote, SDPTypePranswer, &rtcerr.InvalidModificationError{}, }, { "(invalid) stable->SetRemote(rollback)->have-local-offer", SignalingStateStable, SignalingStateHaveLocalOffer, stateChangeOpSetRemote, SDPTypeRollback, &rtcerr.InvalidModificationError{}, }, } for i, tc := range testCases { next, err := checkNextSignalingState(tc.current, tc.next, tc.op, tc.sdpType) if tc.expectedErr != nil { assert.Error(t, err, "testCase: %d %s", i, tc.desc) } else { assert.NoError(t, err, "testCase: %d %s", i, tc.desc) assert.Equal(t, tc.next, next, "testCase: %d %s", i, tc.desc, ) } } } webrtc-3.1.56/srtp_writer_future.go000066400000000000000000000053531437620512100174100ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "io" "sync" "sync/atomic" "time" "github.com/pion/rtp" "github.com/pion/srtp/v2" ) // srtpWriterFuture blocks Read/Write calls until // the SRTP Session is available type srtpWriterFuture struct { ssrc SSRC rtpSender *RTPSender rtcpReadStream atomic.Value // *srtp.ReadStreamSRTCP rtpWriteStream atomic.Value // *srtp.WriteStreamSRTP mu sync.Mutex closed bool } func (s *srtpWriterFuture) init(returnWhenNoSRTP bool) error { if returnWhenNoSRTP { select { case <-s.rtpSender.stopCalled: return io.ErrClosedPipe case <-s.rtpSender.transport.srtpReady: default: return nil } } else { select { case <-s.rtpSender.stopCalled: return io.ErrClosedPipe case <-s.rtpSender.transport.srtpReady: } } s.mu.Lock() defer s.mu.Unlock() if s.closed { return io.ErrClosedPipe } srtcpSession, err := s.rtpSender.transport.getSRTCPSession() if err != nil { return err } rtcpReadStream, err := srtcpSession.OpenReadStream(uint32(s.ssrc)) if err != nil { return err } srtpSession, err := s.rtpSender.transport.getSRTPSession() if err != nil { return err } rtpWriteStream, err := srtpSession.OpenWriteStream() if err != nil { return err } s.rtcpReadStream.Store(rtcpReadStream) s.rtpWriteStream.Store(rtpWriteStream) return nil } func (s *srtpWriterFuture) Close() error { s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil } s.closed = true if value, ok := s.rtcpReadStream.Load().(*srtp.ReadStreamSRTCP); ok { return value.Close() } return nil } func (s *srtpWriterFuture) Read(b []byte) (n int, err error) { if value, ok := s.rtcpReadStream.Load().(*srtp.ReadStreamSRTCP); ok { return value.Read(b) } if err := s.init(false); err != nil || s.rtcpReadStream.Load() == nil { return 0, err } return s.Read(b) } func (s *srtpWriterFuture) SetReadDeadline(t time.Time) error { if value, ok := s.rtcpReadStream.Load().(*srtp.ReadStreamSRTCP); ok { return value.SetReadDeadline(t) } if err := s.init(false); err != nil || s.rtcpReadStream.Load() == nil { return err } return s.SetReadDeadline(t) } func (s *srtpWriterFuture) WriteRTP(header *rtp.Header, payload []byte) (int, error) { if value, ok := s.rtpWriteStream.Load().(*srtp.WriteStreamSRTP); ok { return value.WriteRTP(header, payload) } if err := s.init(true); err != nil || s.rtpWriteStream.Load() == nil { return 0, err } return s.WriteRTP(header, payload) } func (s *srtpWriterFuture) Write(b []byte) (int, error) { if value, ok := s.rtpWriteStream.Load().(*srtp.WriteStreamSRTP); ok { return value.Write(b) } if err := s.init(true); err != nil || s.rtpWriteStream.Load() == nil { return 0, err } return s.Write(b) } webrtc-3.1.56/stats.go000066400000000000000000002045141437620512100145700ustar00rootroot00000000000000package webrtc import ( "fmt" "sync" "time" "github.com/pion/ice/v2" ) // A Stats object contains a set of statistics copies out of a monitored component // of the WebRTC stack at a specific time. type Stats interface{} // StatsType indicates the type of the object that a Stats object represents. type StatsType string const ( // StatsTypeCodec is used by CodecStats. StatsTypeCodec StatsType = "codec" // StatsTypeInboundRTP is used by InboundRTPStreamStats. StatsTypeInboundRTP StatsType = "inbound-rtp" // StatsTypeOutboundRTP is used by OutboundRTPStreamStats. StatsTypeOutboundRTP StatsType = "outbound-rtp" // StatsTypeRemoteInboundRTP is used by RemoteInboundRTPStreamStats. StatsTypeRemoteInboundRTP StatsType = "remote-inbound-rtp" // StatsTypeRemoteOutboundRTP is used by RemoteOutboundRTPStreamStats. StatsTypeRemoteOutboundRTP StatsType = "remote-outbound-rtp" // StatsTypeCSRC is used by RTPContributingSourceStats. StatsTypeCSRC StatsType = "csrc" // StatsTypePeerConnection used by PeerConnectionStats. StatsTypePeerConnection StatsType = "peer-connection" // StatsTypeDataChannel is used by DataChannelStats. StatsTypeDataChannel StatsType = "data-channel" // StatsTypeStream is used by MediaStreamStats. StatsTypeStream StatsType = "stream" // StatsTypeTrack is used by SenderVideoTrackAttachmentStats and SenderAudioTrackAttachmentStats. StatsTypeTrack StatsType = "track" // StatsTypeSender is used by by the AudioSenderStats or VideoSenderStats depending on kind. StatsTypeSender StatsType = "sender" // StatsTypeReceiver is used by the AudioReceiverStats or VideoReceiverStats depending on kind. StatsTypeReceiver StatsType = "receiver" // StatsTypeTransport is used by TransportStats. StatsTypeTransport StatsType = "transport" // StatsTypeCandidatePair is used by ICECandidatePairStats. StatsTypeCandidatePair StatsType = "candidate-pair" // StatsTypeLocalCandidate is used by ICECandidateStats for the local candidate. StatsTypeLocalCandidate StatsType = "local-candidate" // StatsTypeRemoteCandidate is used by ICECandidateStats for the remote candidate. StatsTypeRemoteCandidate StatsType = "remote-candidate" // StatsTypeCertificate is used by CertificateStats. StatsTypeCertificate StatsType = "certificate" ) // StatsTimestamp is a timestamp represented by the floating point number of // milliseconds since the epoch. type StatsTimestamp float64 // Time returns the time.Time represented by this timestamp. func (s StatsTimestamp) Time() time.Time { millis := float64(s) nanos := int64(millis * float64(time.Millisecond)) return time.Unix(0, nanos).UTC() } func statsTimestampFrom(t time.Time) StatsTimestamp { return StatsTimestamp(t.UnixNano() / int64(time.Millisecond)) } func statsTimestampNow() StatsTimestamp { return statsTimestampFrom(time.Now()) } // StatsReport collects Stats objects indexed by their ID. type StatsReport map[string]Stats type statsReportCollector struct { collectingGroup sync.WaitGroup report StatsReport mux sync.Mutex } func newStatsReportCollector() *statsReportCollector { return &statsReportCollector{report: make(StatsReport)} } func (src *statsReportCollector) Collecting() { src.collectingGroup.Add(1) } func (src *statsReportCollector) Collect(id string, stats Stats) { src.mux.Lock() defer src.mux.Unlock() src.report[id] = stats src.collectingGroup.Done() } func (src *statsReportCollector) Done() { src.collectingGroup.Done() } func (src *statsReportCollector) Ready() StatsReport { src.collectingGroup.Wait() src.mux.Lock() defer src.mux.Unlock() return src.report } // CodecType specifies whether a CodecStats objects represents a media format // that is being encoded or decoded type CodecType string const ( // CodecTypeEncode means the attached CodecStats represents a media format that // is being encoded, or that the implementation is prepared to encode. CodecTypeEncode CodecType = "encode" // CodecTypeDecode means the attached CodecStats represents a media format // that the implementation is prepared to decode. CodecTypeDecode CodecType = "decode" ) // CodecStats contains statistics for a codec that is currently being used by RTP streams // being sent or received by this PeerConnection object. type CodecStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // PayloadType as used in RTP encoding or decoding PayloadType PayloadType `json:"payloadType"` // CodecType of this CodecStats CodecType CodecType `json:"codecType"` // TransportID is the unique identifier of the transport on which this codec is // being used, which can be used to look up the corresponding TransportStats object. TransportID string `json:"transportId"` // MimeType is the codec MIME media type/subtype. e.g., video/vp8 or equivalent. MimeType string `json:"mimeType"` // ClockRate represents the media sampling rate. ClockRate uint32 `json:"clockRate"` // Channels is 2 for stereo, missing for most other cases. Channels uint8 `json:"channels"` // SDPFmtpLine is the a=fmtp line in the SDP corresponding to the codec, // i.e., after the colon following the PT. SDPFmtpLine string `json:"sdpFmtpLine"` // Implementation identifies the implementation used. This is useful for diagnosing // interoperability issues. Implementation string `json:"implementation"` } // InboundRTPStreamStats contains statistics for an inbound RTP stream that is // currently received with this PeerConnection object. type InboundRTPStreamStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // SSRC is the 32-bit unsigned integer value used to identify the source of the // stream of RTP packets that this stats object concerns. SSRC SSRC `json:"ssrc"` // Kind is either "audio" or "video" Kind string `json:"kind"` // It is a unique identifier that is associated to the object that was inspected // to produce the TransportStats associated with this RTP stream. TransportID string `json:"transportId"` // CodecID is a unique identifier that is associated to the object that was inspected // to produce the CodecStats associated with this RTP stream. CodecID string `json:"codecId"` // FIRCount counts the total number of Full Intra Request (FIR) packets received // by the sender. This metric is only valid for video and is sent by receiver. FIRCount uint32 `json:"firCount"` // PLICount counts the total number of Picture Loss Indication (PLI) packets // received by the sender. This metric is only valid for video and is sent by receiver. PLICount uint32 `json:"pliCount"` // NACKCount counts the total number of Negative ACKnowledgement (NACK) packets // received by the sender and is sent by receiver. NACKCount uint32 `json:"nackCount"` // SLICount counts the total number of Slice Loss Indication (SLI) packets received // by the sender. This metric is only valid for video and is sent by receiver. SLICount uint32 `json:"sliCount"` // QPSum is the sum of the QP values of frames passed. The count of frames is // in FramesDecoded for inbound stream stats, and in FramesEncoded for outbound stream stats. QPSum uint64 `json:"qpSum"` // PacketsReceived is the total number of RTP packets received for this SSRC. PacketsReceived uint32 `json:"packetsReceived"` // PacketsLost is the total number of RTP packets lost for this SSRC. Note that // because of how this is estimated, it can be negative if more packets are received than sent. PacketsLost int32 `json:"packetsLost"` // Jitter is the packet jitter measured in seconds for this SSRC Jitter float64 `json:"jitter"` // PacketsDiscarded is the cumulative number of RTP packets discarded by the jitter // buffer due to late or early-arrival, i.e., these packets are not played out. // RTP packets discarded due to packet duplication are not reported in this metric. PacketsDiscarded uint32 `json:"packetsDiscarded"` // PacketsRepaired is the cumulative number of lost RTP packets repaired after applying // an error-resilience mechanism. It is measured for the primary source RTP packets // and only counted for RTP packets that have no further chance of repair. PacketsRepaired uint32 `json:"packetsRepaired"` // BurstPacketsLost is the cumulative number of RTP packets lost during loss bursts. BurstPacketsLost uint32 `json:"burstPacketsLost"` // BurstPacketsDiscarded is the cumulative number of RTP packets discarded during discard bursts. BurstPacketsDiscarded uint32 `json:"burstPacketsDiscarded"` // BurstLossCount is the cumulative number of bursts of lost RTP packets. BurstLossCount uint32 `json:"burstLossCount"` // BurstDiscardCount is the cumulative number of bursts of discarded RTP packets. BurstDiscardCount uint32 `json:"burstDiscardCount"` // BurstLossRate is the fraction of RTP packets lost during bursts to the // total number of RTP packets expected in the bursts. BurstLossRate float64 `json:"burstLossRate"` // BurstDiscardRate is the fraction of RTP packets discarded during bursts to // the total number of RTP packets expected in bursts. BurstDiscardRate float64 `json:"burstDiscardRate"` // GapLossRate is the fraction of RTP packets lost during the gap periods. GapLossRate float64 `json:"gapLossRate"` // GapDiscardRate is the fraction of RTP packets discarded during the gap periods. GapDiscardRate float64 `json:"gapDiscardRate"` // TrackID is the identifier of the stats object representing the receiving track, // a ReceiverAudioTrackAttachmentStats or ReceiverVideoTrackAttachmentStats. TrackID string `json:"trackId"` // ReceiverID is the stats ID used to look up the AudioReceiverStats or VideoReceiverStats // object receiving this stream. ReceiverID string `json:"receiverId"` // RemoteID is used for looking up the remote RemoteOutboundRTPStreamStats object // for the same SSRC. RemoteID string `json:"remoteId"` // FramesDecoded represents the total number of frames correctly decoded for this SSRC, // i.e., frames that would be displayed if no frames are dropped. Only valid for video. FramesDecoded uint32 `json:"framesDecoded"` // LastPacketReceivedTimestamp represents the timestamp at which the last packet was // received for this SSRC. This differs from Timestamp, which represents the time // at which the statistics were generated by the local endpoint. LastPacketReceivedTimestamp StatsTimestamp `json:"lastPacketReceivedTimestamp"` // AverageRTCPInterval is the average RTCP interval between two consecutive compound RTCP packets. // This is calculated by the sending endpoint when sending compound RTCP reports. // Compound packets must contain at least a RTCP RR or SR packet and an SDES packet // with the CNAME item. AverageRTCPInterval float64 `json:"averageRtcpInterval"` // FECPacketsReceived is the total number of RTP FEC packets received for this SSRC. // This counter can also be incremented when receiving FEC packets in-band with media packets (e.g., with Opus). FECPacketsReceived uint32 `json:"fecPacketsReceived"` // BytesReceived is the total number of bytes received for this SSRC. BytesReceived uint64 `json:"bytesReceived"` // PacketsFailedDecryption is the cumulative number of RTP packets that failed // to be decrypted. These packets are not counted by PacketsDiscarded. PacketsFailedDecryption uint32 `json:"packetsFailedDecryption"` // PacketsDuplicated is the cumulative number of packets discarded because they // are duplicated. Duplicate packets are not counted in PacketsDiscarded. // // Duplicated packets have the same RTP sequence number and content as a previously // received packet. If multiple duplicates of a packet are received, all of them are counted. // An improved estimate of lost packets can be calculated by adding PacketsDuplicated to PacketsLost. PacketsDuplicated uint32 `json:"packetsDuplicated"` // PerDSCPPacketsReceived is the total number of packets received for this SSRC, // per Differentiated Services code point (DSCP) [RFC2474]. DSCPs are identified // as decimal integers in string form. Note that due to network remapping and bleaching, // these numbers are not expected to match the numbers seen on sending. Not all // OSes make this information available. PerDSCPPacketsReceived map[string]uint32 `json:"perDscpPacketsReceived"` } // QualityLimitationReason lists the reason for limiting the resolution and/or framerate. // Only valid for video. type QualityLimitationReason string const ( // QualityLimitationReasonNone means the resolution and/or framerate is not limited. QualityLimitationReasonNone QualityLimitationReason = "none" // QualityLimitationReasonCPU means the resolution and/or framerate is primarily limited due to CPU load. QualityLimitationReasonCPU QualityLimitationReason = "cpu" // QualityLimitationReasonBandwidth means the resolution and/or framerate is primarily limited due to congestion cues during bandwidth estimation. Typical, congestion control algorithms use inter-arrival time, round-trip time, packet or other congestion cues to perform bandwidth estimation. QualityLimitationReasonBandwidth QualityLimitationReason = "bandwidth" // QualityLimitationReasonOther means the resolution and/or framerate is primarily limited for a reason other than the above. QualityLimitationReasonOther QualityLimitationReason = "other" ) // OutboundRTPStreamStats contains statistics for an outbound RTP stream that is // currently sent with this PeerConnection object. type OutboundRTPStreamStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // SSRC is the 32-bit unsigned integer value used to identify the source of the // stream of RTP packets that this stats object concerns. SSRC SSRC `json:"ssrc"` // Kind is either "audio" or "video" Kind string `json:"kind"` // It is a unique identifier that is associated to the object that was inspected // to produce the TransportStats associated with this RTP stream. TransportID string `json:"transportId"` // CodecID is a unique identifier that is associated to the object that was inspected // to produce the CodecStats associated with this RTP stream. CodecID string `json:"codecId"` // FIRCount counts the total number of Full Intra Request (FIR) packets received // by the sender. This metric is only valid for video and is sent by receiver. FIRCount uint32 `json:"firCount"` // PLICount counts the total number of Picture Loss Indication (PLI) packets // received by the sender. This metric is only valid for video and is sent by receiver. PLICount uint32 `json:"pliCount"` // NACKCount counts the total number of Negative ACKnowledgement (NACK) packets // received by the sender and is sent by receiver. NACKCount uint32 `json:"nackCount"` // SLICount counts the total number of Slice Loss Indication (SLI) packets received // by the sender. This metric is only valid for video and is sent by receiver. SLICount uint32 `json:"sliCount"` // QPSum is the sum of the QP values of frames passed. The count of frames is // in FramesDecoded for inbound stream stats, and in FramesEncoded for outbound stream stats. QPSum uint64 `json:"qpSum"` // PacketsSent is the total number of RTP packets sent for this SSRC. PacketsSent uint32 `json:"packetsSent"` // PacketsDiscardedOnSend is the total number of RTP packets for this SSRC that // have been discarded due to socket errors, i.e. a socket error occurred when handing // the packets to the socket. This might happen due to various reasons, including // full buffer or no available memory. PacketsDiscardedOnSend uint32 `json:"packetsDiscardedOnSend"` // FECPacketsSent is the total number of RTP FEC packets sent for this SSRC. // This counter can also be incremented when sending FEC packets in-band with // media packets (e.g., with Opus). FECPacketsSent uint32 `json:"fecPacketsSent"` // BytesSent is the total number of bytes sent for this SSRC. BytesSent uint64 `json:"bytesSent"` // BytesDiscardedOnSend is the total number of bytes for this SSRC that have // been discarded due to socket errors, i.e. a socket error occurred when handing // the packets containing the bytes to the socket. This might happen due to various // reasons, including full buffer or no available memory. BytesDiscardedOnSend uint64 `json:"bytesDiscardedOnSend"` // TrackID is the identifier of the stats object representing the current track // attachment to the sender of this stream, a SenderAudioTrackAttachmentStats // or SenderVideoTrackAttachmentStats. TrackID string `json:"trackId"` // SenderID is the stats ID used to look up the AudioSenderStats or VideoSenderStats // object sending this stream. SenderID string `json:"senderId"` // RemoteID is used for looking up the remote RemoteInboundRTPStreamStats object // for the same SSRC. RemoteID string `json:"remoteId"` // LastPacketSentTimestamp represents the timestamp at which the last packet was // sent for this SSRC. This differs from timestamp, which represents the time at // which the statistics were generated by the local endpoint. LastPacketSentTimestamp StatsTimestamp `json:"lastPacketSentTimestamp"` // TargetBitrate is the current target bitrate configured for this particular SSRC // and is the Transport Independent Application Specific (TIAS) bitrate [RFC3890]. // Typically, the target bitrate is a configuration parameter provided to the codec's // encoder and does not count the size of the IP or other transport layers like TCP or UDP. // It is measured in bits per second and the bitrate is calculated over a 1 second window. TargetBitrate float64 `json:"targetBitrate"` // FramesEncoded represents the total number of frames successfully encoded for this RTP media stream. // Only valid for video. FramesEncoded uint32 `json:"framesEncoded"` // TotalEncodeTime is the total number of seconds that has been spent encoding the // framesEncoded frames of this stream. The average encode time can be calculated by // dividing this value with FramesEncoded. The time it takes to encode one frame is the // time passed between feeding the encoder a frame and the encoder returning encoded data // for that frame. This does not include any additional time it may take to packetize the resulting data. TotalEncodeTime float64 `json:"totalEncodeTime"` // AverageRTCPInterval is the average RTCP interval between two consecutive compound RTCP // packets. This is calculated by the sending endpoint when sending compound RTCP reports. // Compound packets must contain at least a RTCP RR or SR packet and an SDES packet with the CNAME item. AverageRTCPInterval float64 `json:"averageRtcpInterval"` // QualityLimitationReason is the current reason for limiting the resolution and/or framerate, // or "none" if not limited. Only valid for video. QualityLimitationReason QualityLimitationReason `json:"qualityLimitationReason"` // QualityLimitationDurations is record of the total time, in seconds, that this // stream has spent in each quality limitation state. The record includes a mapping // for all QualityLimitationReason types, including "none". Only valid for video. QualityLimitationDurations map[string]float64 `json:"qualityLimitationDurations"` // PerDSCPPacketsSent is the total number of packets sent for this SSRC, per DSCP. // DSCPs are identified as decimal integers in string form. PerDSCPPacketsSent map[string]uint32 `json:"perDscpPacketsSent"` } // RemoteInboundRTPStreamStats contains statistics for the remote endpoint's inbound // RTP stream corresponding to an outbound stream that is currently sent with this // PeerConnection object. It is measured at the remote endpoint and reported in an RTCP // Receiver Report (RR) or RTCP Extended Report (XR). type RemoteInboundRTPStreamStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // SSRC is the 32-bit unsigned integer value used to identify the source of the // stream of RTP packets that this stats object concerns. SSRC SSRC `json:"ssrc"` // Kind is either "audio" or "video" Kind string `json:"kind"` // It is a unique identifier that is associated to the object that was inspected // to produce the TransportStats associated with this RTP stream. TransportID string `json:"transportId"` // CodecID is a unique identifier that is associated to the object that was inspected // to produce the CodecStats associated with this RTP stream. CodecID string `json:"codecId"` // FIRCount counts the total number of Full Intra Request (FIR) packets received // by the sender. This metric is only valid for video and is sent by receiver. FIRCount uint32 `json:"firCount"` // PLICount counts the total number of Picture Loss Indication (PLI) packets // received by the sender. This metric is only valid for video and is sent by receiver. PLICount uint32 `json:"pliCount"` // NACKCount counts the total number of Negative ACKnowledgement (NACK) packets // received by the sender and is sent by receiver. NACKCount uint32 `json:"nackCount"` // SLICount counts the total number of Slice Loss Indication (SLI) packets received // by the sender. This metric is only valid for video and is sent by receiver. SLICount uint32 `json:"sliCount"` // QPSum is the sum of the QP values of frames passed. The count of frames is // in FramesDecoded for inbound stream stats, and in FramesEncoded for outbound stream stats. QPSum uint64 `json:"qpSum"` // PacketsReceived is the total number of RTP packets received for this SSRC. PacketsReceived uint32 `json:"packetsReceived"` // PacketsLost is the total number of RTP packets lost for this SSRC. Note that // because of how this is estimated, it can be negative if more packets are received than sent. PacketsLost int32 `json:"packetsLost"` // Jitter is the packet jitter measured in seconds for this SSRC Jitter float64 `json:"jitter"` // PacketsDiscarded is the cumulative number of RTP packets discarded by the jitter // buffer due to late or early-arrival, i.e., these packets are not played out. // RTP packets discarded due to packet duplication are not reported in this metric. PacketsDiscarded uint32 `json:"packetsDiscarded"` // PacketsRepaired is the cumulative number of lost RTP packets repaired after applying // an error-resilience mechanism. It is measured for the primary source RTP packets // and only counted for RTP packets that have no further chance of repair. PacketsRepaired uint32 `json:"packetsRepaired"` // BurstPacketsLost is the cumulative number of RTP packets lost during loss bursts. BurstPacketsLost uint32 `json:"burstPacketsLost"` // BurstPacketsDiscarded is the cumulative number of RTP packets discarded during discard bursts. BurstPacketsDiscarded uint32 `json:"burstPacketsDiscarded"` // BurstLossCount is the cumulative number of bursts of lost RTP packets. BurstLossCount uint32 `json:"burstLossCount"` // BurstDiscardCount is the cumulative number of bursts of discarded RTP packets. BurstDiscardCount uint32 `json:"burstDiscardCount"` // BurstLossRate is the fraction of RTP packets lost during bursts to the // total number of RTP packets expected in the bursts. BurstLossRate float64 `json:"burstLossRate"` // BurstDiscardRate is the fraction of RTP packets discarded during bursts to // the total number of RTP packets expected in bursts. BurstDiscardRate float64 `json:"burstDiscardRate"` // GapLossRate is the fraction of RTP packets lost during the gap periods. GapLossRate float64 `json:"gapLossRate"` // GapDiscardRate is the fraction of RTP packets discarded during the gap periods. GapDiscardRate float64 `json:"gapDiscardRate"` // LocalID is used for looking up the local OutboundRTPStreamStats object for the same SSRC. LocalID string `json:"localId"` // RoundTripTime is the estimated round trip time for this SSRC based on the // RTCP timestamps in the RTCP Receiver Report (RR) and measured in seconds. RoundTripTime float64 `json:"roundTripTime"` // FractionLost is the the fraction packet loss reported for this SSRC. FractionLost float64 `json:"fractionLost"` } // RemoteOutboundRTPStreamStats contains statistics for the remote endpoint's outbound // RTP stream corresponding to an inbound stream that is currently received with this // PeerConnection object. It is measured at the remote endpoint and reported in an // RTCP Sender Report (SR). type RemoteOutboundRTPStreamStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // SSRC is the 32-bit unsigned integer value used to identify the source of the // stream of RTP packets that this stats object concerns. SSRC SSRC `json:"ssrc"` // Kind is either "audio" or "video" Kind string `json:"kind"` // It is a unique identifier that is associated to the object that was inspected // to produce the TransportStats associated with this RTP stream. TransportID string `json:"transportId"` // CodecID is a unique identifier that is associated to the object that was inspected // to produce the CodecStats associated with this RTP stream. CodecID string `json:"codecId"` // FIRCount counts the total number of Full Intra Request (FIR) packets received // by the sender. This metric is only valid for video and is sent by receiver. FIRCount uint32 `json:"firCount"` // PLICount counts the total number of Picture Loss Indication (PLI) packets // received by the sender. This metric is only valid for video and is sent by receiver. PLICount uint32 `json:"pliCount"` // NACKCount counts the total number of Negative ACKnowledgement (NACK) packets // received by the sender and is sent by receiver. NACKCount uint32 `json:"nackCount"` // SLICount counts the total number of Slice Loss Indication (SLI) packets received // by the sender. This metric is only valid for video and is sent by receiver. SLICount uint32 `json:"sliCount"` // QPSum is the sum of the QP values of frames passed. The count of frames is // in FramesDecoded for inbound stream stats, and in FramesEncoded for outbound stream stats. QPSum uint64 `json:"qpSum"` // PacketsSent is the total number of RTP packets sent for this SSRC. PacketsSent uint32 `json:"packetsSent"` // PacketsDiscardedOnSend is the total number of RTP packets for this SSRC that // have been discarded due to socket errors, i.e. a socket error occurred when handing // the packets to the socket. This might happen due to various reasons, including // full buffer or no available memory. PacketsDiscardedOnSend uint32 `json:"packetsDiscardedOnSend"` // FECPacketsSent is the total number of RTP FEC packets sent for this SSRC. // This counter can also be incremented when sending FEC packets in-band with // media packets (e.g., with Opus). FECPacketsSent uint32 `json:"fecPacketsSent"` // BytesSent is the total number of bytes sent for this SSRC. BytesSent uint64 `json:"bytesSent"` // BytesDiscardedOnSend is the total number of bytes for this SSRC that have // been discarded due to socket errors, i.e. a socket error occurred when handing // the packets containing the bytes to the socket. This might happen due to various // reasons, including full buffer or no available memory. BytesDiscardedOnSend uint64 `json:"bytesDiscardedOnSend"` // LocalID is used for looking up the local InboundRTPStreamStats object for the same SSRC. LocalID string `json:"localId"` // RemoteTimestamp represents the remote timestamp at which these statistics were // sent by the remote endpoint. This differs from timestamp, which represents the // time at which the statistics were generated or received by the local endpoint. // The RemoteTimestamp, if present, is derived from the NTP timestamp in an RTCP // Sender Report (SR) packet, which reflects the remote endpoint's clock. // That clock may not be synchronized with the local clock. RemoteTimestamp StatsTimestamp `json:"remoteTimestamp"` } // RTPContributingSourceStats contains statistics for a contributing source (CSRC) that contributed // to an inbound RTP stream. type RTPContributingSourceStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // ContributorSSRC is the SSRC identifier of the contributing source represented // by this stats object. It is a 32-bit unsigned integer that appears in the CSRC // list of any packets the relevant source contributed to. ContributorSSRC SSRC `json:"contributorSsrc"` // InboundRTPStreamID is the ID of the InboundRTPStreamStats object representing // the inbound RTP stream that this contributing source is contributing to. InboundRTPStreamID string `json:"inboundRtpStreamId"` // PacketsContributedTo is the total number of RTP packets that this contributing // source contributed to. This value is incremented each time a packet is counted // by InboundRTPStreamStats.packetsReceived, and the packet's CSRC list contains // the SSRC identifier of this contributing source, ContributorSSRC. PacketsContributedTo uint32 `json:"packetsContributedTo"` // AudioLevel is present if the last received RTP packet that this source contributed // to contained an [RFC6465] mixer-to-client audio level header extension. The value // of audioLevel is between 0..1 (linear), where 1.0 represents 0 dBov, 0 represents // silence, and 0.5 represents approximately 6 dBSPL change in the sound pressure level from 0 dBov. AudioLevel float64 `json:"audioLevel"` } // PeerConnectionStats contains statistics related to the PeerConnection object. type PeerConnectionStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // DataChannelsOpened represents the number of unique DataChannels that have // entered the "open" state during their lifetime. DataChannelsOpened uint32 `json:"dataChannelsOpened"` // DataChannelsClosed represents the number of unique DataChannels that have // left the "open" state during their lifetime (due to being closed by either // end or the underlying transport being closed). DataChannels that transition // from "connecting" to "closing" or "closed" without ever being "open" // are not counted in this number. DataChannelsClosed uint32 `json:"dataChannelsClosed"` // DataChannelsRequested Represents the number of unique DataChannels returned // from a successful createDataChannel() call on the PeerConnection. If the // underlying data transport is not established, these may be in the "connecting" state. DataChannelsRequested uint32 `json:"dataChannelsRequested"` // DataChannelsAccepted represents the number of unique DataChannels signaled // in a "datachannel" event on the PeerConnection. DataChannelsAccepted uint32 `json:"dataChannelsAccepted"` } // DataChannelStats contains statistics related to each DataChannel ID. type DataChannelStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // Label is the "label" value of the DataChannel object. Label string `json:"label"` // Protocol is the "protocol" value of the DataChannel object. Protocol string `json:"protocol"` // DataChannelIdentifier is the "id" attribute of the DataChannel object. DataChannelIdentifier int32 `json:"dataChannelIdentifier"` // TransportID the ID of the TransportStats object for transport used to carry this datachannel. TransportID string `json:"transportId"` // State is the "readyState" value of the DataChannel object. State DataChannelState `json:"state"` // MessagesSent represents the total number of API "message" events sent. MessagesSent uint32 `json:"messagesSent"` // BytesSent represents the total number of payload bytes sent on this // datachannel not including headers or padding. BytesSent uint64 `json:"bytesSent"` // MessagesReceived represents the total number of API "message" events received. MessagesReceived uint32 `json:"messagesReceived"` // BytesReceived represents the total number of bytes received on this // datachannel not including headers or padding. BytesReceived uint64 `json:"bytesReceived"` } // MediaStreamStats contains statistics related to a specific MediaStream. type MediaStreamStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // StreamIdentifier is the "id" property of the MediaStream StreamIdentifier string `json:"streamIdentifier"` // TrackIDs is a list of the identifiers of the stats object representing the // stream's tracks, either ReceiverAudioTrackAttachmentStats or ReceiverVideoTrackAttachmentStats. TrackIDs []string `json:"trackIds"` } // AudioSenderStats represents the stats about one audio sender of a PeerConnection // object for which one calls GetStats. // // It appears in the stats as soon as the RTPSender is added by either AddTrack // or AddTransceiver, or by media negotiation. type AudioSenderStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // TrackIdentifier represents the id property of the track. TrackIdentifier string `json:"trackIdentifier"` // RemoteSource is true if the source is remote, for instance if it is sourced // from another host via a PeerConnection. False otherwise. Only applicable for 'track' stats. RemoteSource bool `json:"remoteSource"` // Ended reflects the "ended" state of the track. Ended bool `json:"ended"` // Kind is either "audio" or "video". This reflects the "kind" attribute of the MediaStreamTrack. Kind string `json:"kind"` // AudioLevel represents the output audio level of the track. // // The value is a value between 0..1 (linear), where 1.0 represents 0 dBov, // 0 represents silence, and 0.5 represents approximately 6 dBSPL change in // the sound pressure level from 0 dBov. // // If the track is sourced from an Receiver, does no audio processing, has a // constant level, and has a volume setting of 1.0, the audio level is expected // to be the same as the audio level of the source SSRC, while if the volume setting // is 0.5, the AudioLevel is expected to be half that value. // // For outgoing audio tracks, the AudioLevel is the level of the audio being sent. AudioLevel float64 `json:"audioLevel"` // TotalAudioEnergy is the total energy of all the audio samples sent/received // for this object, calculated by duration * Math.pow(energy/maxEnergy, 2) for // each audio sample seen. TotalAudioEnergy float64 `json:"totalAudioEnergy"` // VoiceActivityFlag represents whether the last RTP packet sent or played out // by this track contained voice activity or not based on the presence of the // V bit in the extension header, as defined in [RFC6464]. // // This value indicates the voice activity in the latest RTP packet played out // from a given SSRC, and is defined in RTPSynchronizationSource.voiceActivityFlag. VoiceActivityFlag bool `json:"voiceActivityFlag"` // TotalSamplesDuration represents the total duration in seconds of all samples // that have sent or received (and thus counted by TotalSamplesSent or TotalSamplesReceived). // Can be used with TotalAudioEnergy to compute an average audio level over different intervals. TotalSamplesDuration float64 `json:"totalSamplesDuration"` // EchoReturnLoss is only present while the sender is sending a track sourced from // a microphone where echo cancellation is applied. Calculated in decibels. EchoReturnLoss float64 `json:"echoReturnLoss"` // EchoReturnLossEnhancement is only present while the sender is sending a track // sourced from a microphone where echo cancellation is applied. Calculated in decibels. EchoReturnLossEnhancement float64 `json:"echoReturnLossEnhancement"` // TotalSamplesSent is the total number of samples that have been sent by this sender. TotalSamplesSent uint64 `json:"totalSamplesSent"` } // SenderAudioTrackAttachmentStats object represents the stats about one attachment // of an audio MediaStreamTrack to the PeerConnection object for which one calls GetStats. // // It appears in the stats as soon as it is attached (via AddTrack, via AddTransceiver, // via ReplaceTrack on an RTPSender object). // // If an audio track is attached twice (via AddTransceiver or ReplaceTrack), there // will be two SenderAudioTrackAttachmentStats objects, one for each attachment. // They will have the same "TrackIdentifier" attribute, but different "ID" attributes. // // If the track is detached from the PeerConnection (via removeTrack or via replaceTrack), // it continues to appear, but with the "ObjectDeleted" member set to true. type SenderAudioTrackAttachmentStats AudioSenderStats // VideoSenderStats represents the stats about one video sender of a PeerConnection // object for which one calls GetStats. // // It appears in the stats as soon as the sender is added by either AddTrack or // AddTransceiver, or by media negotiation. type VideoSenderStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // FramesCaptured represents the total number of frames captured, before encoding, // for this RTPSender (or for this MediaStreamTrack, if type is "track"). For example, // if type is "sender" and this sender's track represents a camera, then this is the // number of frames produced by the camera for this track while being sent by this sender, // combined with the number of frames produced by all tracks previously attached to this // sender while being sent by this sender. Framerates can vary due to hardware limitations // or environmental factors such as lighting conditions. FramesCaptured uint32 `json:"framesCaptured"` // FramesSent represents the total number of frames sent by this RTPSender // (or for this MediaStreamTrack, if type is "track"). FramesSent uint32 `json:"framesSent"` // HugeFramesSent represents the total number of huge frames sent by this RTPSender // (or for this MediaStreamTrack, if type is "track"). Huge frames, by definition, // are frames that have an encoded size at least 2.5 times the average size of the frames. // The average size of the frames is defined as the target bitrate per second divided // by the target fps at the time the frame was encoded. These are usually complex // to encode frames with a lot of changes in the picture. This can be used to estimate, // e.g slide changes in the streamed presentation. If a huge frame is also a key frame, // then both counters HugeFramesSent and KeyFramesSent are incremented. HugeFramesSent uint32 `json:"hugeFramesSent"` // KeyFramesSent represents the total number of key frames sent by this RTPSender // (or for this MediaStreamTrack, if type is "track"), such as Infra-frames in // VP8 [RFC6386] or I-frames in H.264 [RFC6184]. This is a subset of FramesSent. // FramesSent - KeyFramesSent gives you the number of delta frames sent. KeyFramesSent uint32 `json:"keyFramesSent"` } // SenderVideoTrackAttachmentStats represents the stats about one attachment of a // video MediaStreamTrack to the PeerConnection object for which one calls GetStats. // // It appears in the stats as soon as it is attached (via AddTrack, via AddTransceiver, // via ReplaceTrack on an RTPSender object). // // If a video track is attached twice (via AddTransceiver or ReplaceTrack), there // will be two SenderVideoTrackAttachmentStats objects, one for each attachment. // They will have the same "TrackIdentifier" attribute, but different "ID" attributes. // // If the track is detached from the PeerConnection (via RemoveTrack or via ReplaceTrack), // it continues to appear, but with the "ObjectDeleted" member set to true. type SenderVideoTrackAttachmentStats VideoSenderStats // AudioReceiverStats contains audio metrics related to a specific receiver. type AudioReceiverStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // AudioLevel represents the output audio level of the track. // // The value is a value between 0..1 (linear), where 1.0 represents 0 dBov, // 0 represents silence, and 0.5 represents approximately 6 dBSPL change in // the sound pressure level from 0 dBov. // // If the track is sourced from an Receiver, does no audio processing, has a // constant level, and has a volume setting of 1.0, the audio level is expected // to be the same as the audio level of the source SSRC, while if the volume setting // is 0.5, the AudioLevel is expected to be half that value. // // For outgoing audio tracks, the AudioLevel is the level of the audio being sent. AudioLevel float64 `json:"audioLevel"` // TotalAudioEnergy is the total energy of all the audio samples sent/received // for this object, calculated by duration * Math.pow(energy/maxEnergy, 2) for // each audio sample seen. TotalAudioEnergy float64 `json:"totalAudioEnergy"` // VoiceActivityFlag represents whether the last RTP packet sent or played out // by this track contained voice activity or not based on the presence of the // V bit in the extension header, as defined in [RFC6464]. // // This value indicates the voice activity in the latest RTP packet played out // from a given SSRC, and is defined in RTPSynchronizationSource.voiceActivityFlag. VoiceActivityFlag bool `json:"voiceActivityFlag"` // TotalSamplesDuration represents the total duration in seconds of all samples // that have sent or received (and thus counted by TotalSamplesSent or TotalSamplesReceived). // Can be used with TotalAudioEnergy to compute an average audio level over different intervals. TotalSamplesDuration float64 `json:"totalSamplesDuration"` // EstimatedPlayoutTimestamp is the estimated playout time of this receiver's // track. The playout time is the NTP timestamp of the last playable sample that // has a known timestamp (from an RTCP SR packet mapping RTP timestamps to NTP // timestamps), extrapolated with the time elapsed since it was ready to be played out. // This is the "current time" of the track in NTP clock time of the sender and // can be present even if there is no audio currently playing. // // This can be useful for estimating how much audio and video is out of // sync for two tracks from the same source: // AudioTrackStats.EstimatedPlayoutTimestamp - VideoTrackStats.EstimatedPlayoutTimestamp EstimatedPlayoutTimestamp StatsTimestamp `json:"estimatedPlayoutTimestamp"` // JitterBufferDelay is the sum of the time, in seconds, each sample takes from // the time it is received and to the time it exits the jitter buffer. // This increases upon samples exiting, having completed their time in the buffer // (incrementing JitterBufferEmittedCount). The average jitter buffer delay can // be calculated by dividing the JitterBufferDelay with the JitterBufferEmittedCount. JitterBufferDelay float64 `json:"jitterBufferDelay"` // JitterBufferEmittedCount is the total number of samples that have come out // of the jitter buffer (increasing JitterBufferDelay). JitterBufferEmittedCount uint64 `json:"jitterBufferEmittedCount"` // TotalSamplesReceived is the total number of samples that have been received // by this receiver. This includes ConcealedSamples. TotalSamplesReceived uint64 `json:"totalSamplesReceived"` // ConcealedSamples is the total number of samples that are concealed samples. // A concealed sample is a sample that is based on data that was synthesized // to conceal packet loss and does not represent incoming data. ConcealedSamples uint64 `json:"concealedSamples"` // ConcealmentEvents is the number of concealment events. This counter increases // every time a concealed sample is synthesized after a non-concealed sample. // That is, multiple consecutive concealed samples will increase the concealedSamples // count multiple times but is a single concealment event. ConcealmentEvents uint64 `json:"concealmentEvents"` } // VideoReceiverStats contains video metrics related to a specific receiver. type VideoReceiverStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // FrameWidth represents the width of the last processed frame for this track. // Before the first frame is processed this attribute is missing. FrameWidth uint32 `json:"frameWidth"` // FrameHeight represents the height of the last processed frame for this track. // Before the first frame is processed this attribute is missing. FrameHeight uint32 `json:"frameHeight"` // FramesPerSecond represents the nominal FPS value before the degradation preference // is applied. It is the number of complete frames in the last second. For sending // tracks it is the current captured FPS and for the receiving tracks it is the // current decoding framerate. FramesPerSecond float64 `json:"framesPerSecond"` // EstimatedPlayoutTimestamp is the estimated playout time of this receiver's // track. The playout time is the NTP timestamp of the last playable sample that // has a known timestamp (from an RTCP SR packet mapping RTP timestamps to NTP // timestamps), extrapolated with the time elapsed since it was ready to be played out. // This is the "current time" of the track in NTP clock time of the sender and // can be present even if there is no audio currently playing. // // This can be useful for estimating how much audio and video is out of // sync for two tracks from the same source: // AudioTrackStats.EstimatedPlayoutTimestamp - VideoTrackStats.EstimatedPlayoutTimestamp EstimatedPlayoutTimestamp StatsTimestamp `json:"estimatedPlayoutTimestamp"` // JitterBufferDelay is the sum of the time, in seconds, each sample takes from // the time it is received and to the time it exits the jitter buffer. // This increases upon samples exiting, having completed their time in the buffer // (incrementing JitterBufferEmittedCount). The average jitter buffer delay can // be calculated by dividing the JitterBufferDelay with the JitterBufferEmittedCount. JitterBufferDelay float64 `json:"jitterBufferDelay"` // JitterBufferEmittedCount is the total number of samples that have come out // of the jitter buffer (increasing JitterBufferDelay). JitterBufferEmittedCount uint64 `json:"jitterBufferEmittedCount"` // FramesReceived Represents the total number of complete frames received for // this receiver. This metric is incremented when the complete frame is received. FramesReceived uint32 `json:"framesReceived"` // KeyFramesReceived represents the total number of complete key frames received // for this MediaStreamTrack, such as Infra-frames in VP8 [RFC6386] or I-frames // in H.264 [RFC6184]. This is a subset of framesReceived. `framesReceived - keyFramesReceived` // gives you the number of delta frames received. This metric is incremented when // the complete key frame is received. It is not incremented if a partial key // frames is received and sent for decoding, i.e., the frame could not be recovered // via retransmission or FEC. KeyFramesReceived uint32 `json:"keyFramesReceived"` // FramesDecoded represents the total number of frames correctly decoded for this // SSRC, i.e., frames that would be displayed if no frames are dropped. FramesDecoded uint32 `json:"framesDecoded"` // FramesDropped is the total number of frames dropped predecode or dropped // because the frame missed its display deadline for this receiver's track. FramesDropped uint32 `json:"framesDropped"` // The cumulative number of partial frames lost. This metric is incremented when // the frame is sent to the decoder. If the partial frame is received and recovered // via retransmission or FEC before decoding, the FramesReceived counter is incremented. PartialFramesLost uint32 `json:"partialFramesLost"` // FullFramesLost is the cumulative number of full frames lost. FullFramesLost uint32 `json:"fullFramesLost"` } // TransportStats contains transport statistics related to the PeerConnection object. type TransportStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // PacketsSent represents the total number of packets sent over this transport. PacketsSent uint32 `json:"packetsSent"` // PacketsReceived represents the total number of packets received on this transport. PacketsReceived uint32 `json:"packetsReceived"` // BytesSent represents the total number of payload bytes sent on this PeerConnection // not including headers or padding. BytesSent uint64 `json:"bytesSent"` // BytesReceived represents the total number of bytes received on this PeerConnection // not including headers or padding. BytesReceived uint64 `json:"bytesReceived"` // RTCPTransportStatsID is the ID of the transport that gives stats for the RTCP // component If RTP and RTCP are not multiplexed and this record has only // the RTP component stats. RTCPTransportStatsID string `json:"rtcpTransportStatsId"` // ICERole is set to the current value of the "role" attribute of the underlying // DTLSTransport's "transport". ICERole ICERole `json:"iceRole"` // DTLSState is set to the current value of the "state" attribute of the underlying DTLSTransport. DTLSState DTLSTransportState `json:"dtlsState"` // SelectedCandidatePairID is a unique identifier that is associated to the object // that was inspected to produce the ICECandidatePairStats associated with this transport. SelectedCandidatePairID string `json:"selectedCandidatePairId"` // LocalCertificateID is the ID of the CertificateStats for the local certificate. // Present only if DTLS is negotiated. LocalCertificateID string `json:"localCertificateId"` // LocalCertificateID is the ID of the CertificateStats for the remote certificate. // Present only if DTLS is negotiated. RemoteCertificateID string `json:"remoteCertificateId"` // DTLSCipher is the descriptive name of the cipher suite used for the DTLS transport, // as defined in the "Description" column of the IANA cipher suite registry. DTLSCipher string `json:"dtlsCipher"` // SRTPCipher is the descriptive name of the protection profile used for the SRTP // transport, as defined in the "Profile" column of the IANA DTLS-SRTP protection // profile registry. SRTPCipher string `json:"srtpCipher"` } // StatsICECandidatePairState is the state of an ICE candidate pair used in the // ICECandidatePairStats object. type StatsICECandidatePairState string func toStatsICECandidatePairState(state ice.CandidatePairState) (StatsICECandidatePairState, error) { switch state { case ice.CandidatePairStateWaiting: return StatsICECandidatePairStateWaiting, nil case ice.CandidatePairStateInProgress: return StatsICECandidatePairStateInProgress, nil case ice.CandidatePairStateFailed: return StatsICECandidatePairStateFailed, nil case ice.CandidatePairStateSucceeded: return StatsICECandidatePairStateSucceeded, nil default: // NOTE: this should never happen[tm] err := fmt.Errorf("%w: %s", errStatsICECandidateStateInvalid, state.String()) return StatsICECandidatePairState("Unknown"), err } } const ( // StatsICECandidatePairStateFrozen means a check for this pair hasn't been // performed, and it can't yet be performed until some other check succeeds, // allowing this pair to unfreeze and move into the Waiting state. StatsICECandidatePairStateFrozen StatsICECandidatePairState = "frozen" // StatsICECandidatePairStateWaiting means a check has not been performed for // this pair, and can be performed as soon as it is the highest-priority Waiting // pair on the check list. StatsICECandidatePairStateWaiting StatsICECandidatePairState = "waiting" // StatsICECandidatePairStateInProgress means a check has been sent for this pair, // but the transaction is in progress. StatsICECandidatePairStateInProgress StatsICECandidatePairState = "in-progress" // StatsICECandidatePairStateFailed means a check for this pair was already done // and failed, either never producing any response or producing an unrecoverable // failure response. StatsICECandidatePairStateFailed StatsICECandidatePairState = "failed" // StatsICECandidatePairStateSucceeded means a check for this pair was already // done and produced a successful result. StatsICECandidatePairStateSucceeded StatsICECandidatePairState = "succeeded" ) // ICECandidatePairStats contains ICE candidate pair statistics related // to the ICETransport objects. type ICECandidatePairStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // TransportID is a unique identifier that is associated to the object that // was inspected to produce the TransportStats associated with this candidate pair. TransportID string `json:"transportId"` // LocalCandidateID is a unique identifier that is associated to the object // that was inspected to produce the ICECandidateStats for the local candidate // associated with this candidate pair. LocalCandidateID string `json:"localCandidateId"` // RemoteCandidateID is a unique identifier that is associated to the object // that was inspected to produce the ICECandidateStats for the remote candidate // associated with this candidate pair. RemoteCandidateID string `json:"remoteCandidateId"` // State represents the state of the checklist for the local and remote // candidates in a pair. State StatsICECandidatePairState `json:"state"` // Nominated is true when this valid pair that should be used for media // if it is the highest-priority one amongst those whose nominated flag is set Nominated bool `json:"nominated"` // PacketsSent represents the total number of packets sent on this candidate pair. PacketsSent uint32 `json:"packetsSent"` // PacketsReceived represents the total number of packets received on this candidate pair. PacketsReceived uint32 `json:"packetsReceived"` // BytesSent represents the total number of payload bytes sent on this candidate pair // not including headers or padding. BytesSent uint64 `json:"bytesSent"` // BytesReceived represents the total number of payload bytes received on this candidate pair // not including headers or padding. BytesReceived uint64 `json:"bytesReceived"` // LastPacketSentTimestamp represents the timestamp at which the last packet was // sent on this particular candidate pair, excluding STUN packets. LastPacketSentTimestamp StatsTimestamp `json:"lastPacketSentTimestamp"` // LastPacketReceivedTimestamp represents the timestamp at which the last packet // was received on this particular candidate pair, excluding STUN packets. LastPacketReceivedTimestamp StatsTimestamp `json:"lastPacketReceivedTimestamp"` // FirstRequestTimestamp represents the timestamp at which the first STUN request // was sent on this particular candidate pair. FirstRequestTimestamp StatsTimestamp `json:"firstRequestTimestamp"` // LastRequestTimestamp represents the timestamp at which the last STUN request // was sent on this particular candidate pair. The average interval between two // consecutive connectivity checks sent can be calculated with // (LastRequestTimestamp - FirstRequestTimestamp) / RequestsSent. LastRequestTimestamp StatsTimestamp `json:"lastRequestTimestamp"` // LastResponseTimestamp represents the timestamp at which the last STUN response // was received on this particular candidate pair. LastResponseTimestamp StatsTimestamp `json:"lastResponseTimestamp"` // TotalRoundTripTime represents the sum of all round trip time measurements // in seconds since the beginning of the session, based on STUN connectivity // check responses (ResponsesReceived), including those that reply to requests // that are sent in order to verify consent. The average round trip time can // be computed from TotalRoundTripTime by dividing it by ResponsesReceived. TotalRoundTripTime float64 `json:"totalRoundTripTime"` // CurrentRoundTripTime represents the latest round trip time measured in seconds, // computed from both STUN connectivity checks, including those that are sent // for consent verification. CurrentRoundTripTime float64 `json:"currentRoundTripTime"` // AvailableOutgoingBitrate is calculated by the underlying congestion control // by combining the available bitrate for all the outgoing RTP streams using // this candidate pair. The bitrate measurement does not count the size of the // IP or other transport layers like TCP or UDP. It is similar to the TIAS defined // in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated // over a 1 second window. AvailableOutgoingBitrate float64 `json:"availableOutgoingBitrate"` // AvailableIncomingBitrate is calculated by the underlying congestion control // by combining the available bitrate for all the incoming RTP streams using // this candidate pair. The bitrate measurement does not count the size of the // IP or other transport layers like TCP or UDP. It is similar to the TIAS defined // in RFC 3890, i.e., it is measured in bits per second and the bitrate is // calculated over a 1 second window. AvailableIncomingBitrate float64 `json:"availableIncomingBitrate"` // CircuitBreakerTriggerCount represents the number of times the circuit breaker // is triggered for this particular 5-tuple, ceasing transmission. CircuitBreakerTriggerCount uint32 `json:"circuitBreakerTriggerCount"` // RequestsReceived represents the total number of connectivity check requests // received (including retransmissions). It is impossible for the receiver to // tell whether the request was sent in order to check connectivity or check // consent, so all connectivity checks requests are counted here. RequestsReceived uint64 `json:"requestsReceived"` // RequestsSent represents the total number of connectivity check requests // sent (not including retransmissions). RequestsSent uint64 `json:"requestsSent"` // ResponsesReceived represents the total number of connectivity check responses received. ResponsesReceived uint64 `json:"responsesReceived"` // ResponsesSent represents the total number of connectivity check responses sent. // Since we cannot distinguish connectivity check requests and consent requests, // all responses are counted. ResponsesSent uint64 `json:"responsesSent"` // RetransmissionsReceived represents the total number of connectivity check // request retransmissions received. RetransmissionsReceived uint64 `json:"retransmissionsReceived"` // RetransmissionsSent represents the total number of connectivity check // request retransmissions sent. RetransmissionsSent uint64 `json:"retransmissionsSent"` // ConsentRequestsSent represents the total number of consent requests sent. ConsentRequestsSent uint64 `json:"consentRequestsSent"` // ConsentExpiredTimestamp represents the timestamp at which the latest valid // STUN binding response expired. ConsentExpiredTimestamp StatsTimestamp `json:"consentExpiredTimestamp"` } // ICECandidateStats contains ICE candidate statistics related to the ICETransport objects. type ICECandidateStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // TransportID is a unique identifier that is associated to the object that // was inspected to produce the TransportStats associated with this candidate. TransportID string `json:"transportId"` // NetworkType represents the type of network interface used by the base of a // local candidate (the address the ICE agent sends from). Only present for // local candidates; it's not possible to know what type of network interface // a remote candidate is using. // // Note: // This stat only tells you about the network interface used by the first "hop"; // it's possible that a connection will be bottlenecked by another type of network. // For example, when using Wi-Fi tethering, the networkType of the relevant candidate // would be "wifi", even when the next hop is over a cellular connection. NetworkType NetworkType `json:"networkType"` // IP is the IP address of the candidate, allowing for IPv4 addresses and // IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed. IP string `json:"ip"` // Port is the port number of the candidate. Port int32 `json:"port"` // Protocol is one of udp and tcp. Protocol string `json:"protocol"` // CandidateType is the "Type" field of the ICECandidate. CandidateType ICECandidateType `json:"candidateType"` // Priority is the "Priority" field of the ICECandidate. Priority int32 `json:"priority"` // URL is the URL of the TURN or STUN server indicated in the that translated // this IP address. It is the URL address surfaced in an PeerConnectionICEEvent. URL string `json:"url"` // RelayProtocol is the protocol used by the endpoint to communicate with the // TURN server. This is only present for local candidates. Valid values for // the TURN URL protocol is one of udp, tcp, or tls. RelayProtocol string `json:"relayProtocol"` // Deleted is true if the candidate has been deleted/freed. For host candidates, // this means that any network resources (typically a socket) associated with the // candidate have been released. For TURN candidates, this means the TURN allocation // is no longer active. // // Only defined for local candidates. For remote candidates, this property is not applicable. Deleted bool `json:"deleted"` } // CertificateStats contains information about a certificate used by an ICETransport. type CertificateStats struct { // Timestamp is the timestamp associated with this object. Timestamp StatsTimestamp `json:"timestamp"` // Type is the object's StatsType Type StatsType `json:"type"` // ID is a unique id that is associated with the component inspected to produce // this Stats object. Two Stats objects will have the same ID if they were produced // by inspecting the same underlying object. ID string `json:"id"` // Fingerprint is the fingerprint of the certificate. Fingerprint string `json:"fingerprint"` // FingerprintAlgorithm is the hash function used to compute the certificate fingerprint. For instance, "sha-256". FingerprintAlgorithm string `json:"fingerprintAlgorithm"` // Base64Certificate is the DER-encoded base-64 representation of the certificate. Base64Certificate string `json:"base64Certificate"` // IssuerCertificateID refers to the stats object that contains the next certificate // in the certificate chain. If the current certificate is at the end of the chain // (i.e. a self-signed certificate), this will not be set. IssuerCertificateID string `json:"issuerCertificateId"` } webrtc-3.1.56/stats_go.go000066400000000000000000000046721437620512100152600ustar00rootroot00000000000000//go:build !js // +build !js package webrtc // GetConnectionStats is a helper method to return the associated stats for a given PeerConnection func (r StatsReport) GetConnectionStats(conn *PeerConnection) (PeerConnectionStats, bool) { statsID := conn.getStatsID() stats, ok := r[statsID] if !ok { return PeerConnectionStats{}, false } pcStats, ok := stats.(PeerConnectionStats) if !ok { return PeerConnectionStats{}, false } return pcStats, true } // GetDataChannelStats is a helper method to return the associated stats for a given DataChannel func (r StatsReport) GetDataChannelStats(dc *DataChannel) (DataChannelStats, bool) { statsID := dc.getStatsID() stats, ok := r[statsID] if !ok { return DataChannelStats{}, false } dcStats, ok := stats.(DataChannelStats) if !ok { return DataChannelStats{}, false } return dcStats, true } // GetICECandidateStats is a helper method to return the associated stats for a given ICECandidate func (r StatsReport) GetICECandidateStats(c *ICECandidate) (ICECandidateStats, bool) { statsID := c.statsID stats, ok := r[statsID] if !ok { return ICECandidateStats{}, false } candidateStats, ok := stats.(ICECandidateStats) if !ok { return ICECandidateStats{}, false } return candidateStats, true } // GetICECandidatePairStats is a helper method to return the associated stats for a given ICECandidatePair func (r StatsReport) GetICECandidatePairStats(c *ICECandidatePair) (ICECandidatePairStats, bool) { statsID := c.statsID stats, ok := r[statsID] if !ok { return ICECandidatePairStats{}, false } candidateStats, ok := stats.(ICECandidatePairStats) if !ok { return ICECandidatePairStats{}, false } return candidateStats, true } // GetCertificateStats is a helper method to return the associated stats for a given Certificate func (r StatsReport) GetCertificateStats(c *Certificate) (CertificateStats, bool) { statsID := c.statsID stats, ok := r[statsID] if !ok { return CertificateStats{}, false } certificateStats, ok := stats.(CertificateStats) if !ok { return CertificateStats{}, false } return certificateStats, true } // GetCodecStats is a helper method to return the associated stats for a given Codec func (r StatsReport) GetCodecStats(c *RTPCodecParameters) (CodecStats, bool) { statsID := c.statsID stats, ok := r[statsID] if !ok { return CodecStats{}, false } codecStats, ok := stats.(CodecStats) if !ok { return CodecStats{}, false } return codecStats, true } webrtc-3.1.56/stats_go_test.go000066400000000000000000000262061437620512100163140ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "encoding/json" "fmt" "sync" "testing" "time" "github.com/pion/ice/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var errReceiveOfferTimeout = fmt.Errorf("timed out waiting to receive offer") func TestStatsTimestampTime(t *testing.T) { for _, test := range []struct { Timestamp StatsTimestamp WantTime time.Time }{ { Timestamp: 0, WantTime: time.Unix(0, 0), }, { Timestamp: 1, WantTime: time.Unix(0, 1e6), }, { Timestamp: 0.001, WantTime: time.Unix(0, 1e3), }, } { if got, want := test.Timestamp.Time(), test.WantTime.UTC(); got != want { t.Fatalf("StatsTimestamp(%v).Time() = %v, want %v", test.Timestamp, got, want) } } } func TestStatsMarshal(t *testing.T) { for _, test := range []Stats{ AudioReceiverStats{}, AudioSenderStats{}, CertificateStats{}, CodecStats{}, DataChannelStats{}, ICECandidatePairStats{}, ICECandidateStats{}, InboundRTPStreamStats{}, MediaStreamStats{}, OutboundRTPStreamStats{}, PeerConnectionStats{}, RemoteInboundRTPStreamStats{}, RemoteOutboundRTPStreamStats{}, RTPContributingSourceStats{}, SenderAudioTrackAttachmentStats{}, SenderAudioTrackAttachmentStats{}, SenderVideoTrackAttachmentStats{}, TransportStats{}, VideoReceiverStats{}, VideoReceiverStats{}, VideoSenderStats{}, } { _, err := json.Marshal(test) if err != nil { t.Fatal(err) } } } func waitWithTimeout(t *testing.T, wg *sync.WaitGroup) { // Wait for all of the event handlers to be triggered. done := make(chan struct{}) go func() { wg.Wait() done <- struct{}{} }() timeout := time.After(5 * time.Second) select { case <-done: break case <-timeout: t.Fatal("timed out waiting for waitgroup") } } func getConnectionStats(t *testing.T, report StatsReport, pc *PeerConnection) PeerConnectionStats { stats, ok := report.GetConnectionStats(pc) assert.True(t, ok) assert.Equal(t, stats.Type, StatsTypePeerConnection) return stats } func getDataChannelStats(t *testing.T, report StatsReport, dc *DataChannel) DataChannelStats { stats, ok := report.GetDataChannelStats(dc) assert.True(t, ok) assert.Equal(t, stats.Type, StatsTypeDataChannel) return stats } func getCodecStats(t *testing.T, report StatsReport, c *RTPCodecParameters) CodecStats { stats, ok := report.GetCodecStats(c) assert.True(t, ok) assert.Equal(t, stats.Type, StatsTypeCodec) return stats } func getTransportStats(t *testing.T, report StatsReport, statsID string) TransportStats { stats, ok := report[statsID] assert.True(t, ok) transportStats, ok := stats.(TransportStats) assert.True(t, ok) assert.Equal(t, transportStats.Type, StatsTypeTransport) return transportStats } func getCertificateStats(t *testing.T, report StatsReport, certificate *Certificate) CertificateStats { certificateStats, ok := report.GetCertificateStats(certificate) assert.True(t, ok) assert.Equal(t, certificateStats.Type, StatsTypeCertificate) return certificateStats } func findLocalCandidateStats(report StatsReport) []ICECandidateStats { result := []ICECandidateStats{} for _, s := range report { stats, ok := s.(ICECandidateStats) if ok && stats.Type == StatsTypeLocalCandidate { result = append(result, stats) } } return result } func findRemoteCandidateStats(report StatsReport) []ICECandidateStats { result := []ICECandidateStats{} for _, s := range report { stats, ok := s.(ICECandidateStats) if ok && stats.Type == StatsTypeRemoteCandidate { result = append(result, stats) } } return result } func findCandidatePairStats(t *testing.T, report StatsReport) []ICECandidatePairStats { result := []ICECandidatePairStats{} for _, s := range report { stats, ok := s.(ICECandidatePairStats) if ok { assert.Equal(t, StatsTypeCandidatePair, stats.Type) result = append(result, stats) } } return result } func signalPairForStats(pcOffer *PeerConnection, pcAnswer *PeerConnection) error { offerChan := make(chan SessionDescription) pcOffer.OnICECandidate(func(candidate *ICECandidate) { if candidate == nil { offerChan <- *pcOffer.PendingLocalDescription() } }) offer, err := pcOffer.CreateOffer(nil) if err != nil { return err } if err := pcOffer.SetLocalDescription(offer); err != nil { return err } timeout := time.After(3 * time.Second) select { case <-timeout: return errReceiveOfferTimeout case offer := <-offerChan: if err := pcAnswer.SetRemoteDescription(offer); err != nil { return err } answer, err := pcAnswer.CreateAnswer(nil) if err != nil { return err } if err = pcAnswer.SetLocalDescription(answer); err != nil { return err } err = pcOffer.SetRemoteDescription(answer) if err != nil { return err } return nil } } func TestStatsConvertState(t *testing.T) { testCases := []struct { ice ice.CandidatePairState stats StatsICECandidatePairState }{ { ice.CandidatePairStateWaiting, StatsICECandidatePairStateWaiting, }, { ice.CandidatePairStateInProgress, StatsICECandidatePairStateInProgress, }, { ice.CandidatePairStateFailed, StatsICECandidatePairStateFailed, }, { ice.CandidatePairStateSucceeded, StatsICECandidatePairStateSucceeded, }, } s, err := toStatsICECandidatePairState(ice.CandidatePairState(42)) assert.Error(t, err) assert.Equal(t, StatsICECandidatePairState("Unknown"), s) for i, testCase := range testCases { s, err := toStatsICECandidatePairState(testCase.ice) assert.NoError(t, err) assert.Equal(t, testCase.stats, s, "testCase: %d %v", i, testCase, ) } } func TestPeerConnection_GetStats(t *testing.T) { offerPC, answerPC, err := newPair() assert.NoError(t, err) track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1") require.NoError(t, err) _, err = offerPC.AddTrack(track1) require.NoError(t, err) baseLineReportPCOffer := offerPC.GetStats() baseLineReportPCAnswer := answerPC.GetStats() connStatsOffer := getConnectionStats(t, baseLineReportPCOffer, offerPC) connStatsAnswer := getConnectionStats(t, baseLineReportPCAnswer, answerPC) for _, connStats := range []PeerConnectionStats{connStatsOffer, connStatsAnswer} { assert.Equal(t, uint32(0), connStats.DataChannelsOpened) assert.Equal(t, uint32(0), connStats.DataChannelsClosed) assert.Equal(t, uint32(0), connStats.DataChannelsRequested) assert.Equal(t, uint32(0), connStats.DataChannelsAccepted) } // Create a DC, open it and send a message offerDC, err := offerPC.CreateDataChannel("offerDC", nil) assert.NoError(t, err) msg := []byte("a classic test message") offerDC.OnOpen(func() { assert.NoError(t, offerDC.Send(msg)) }) dcWait := sync.WaitGroup{} dcWait.Add(1) answerDCChan := make(chan *DataChannel) answerPC.OnDataChannel(func(d *DataChannel) { d.OnOpen(func() { answerDCChan <- d }) d.OnMessage(func(m DataChannelMessage) { dcWait.Done() }) }) assert.NoError(t, signalPairForStats(offerPC, answerPC)) waitWithTimeout(t, &dcWait) answerDC := <-answerDCChan reportPCOffer := offerPC.GetStats() reportPCAnswer := answerPC.GetStats() connStatsOffer = getConnectionStats(t, reportPCOffer, offerPC) assert.Equal(t, uint32(1), connStatsOffer.DataChannelsOpened) assert.Equal(t, uint32(0), connStatsOffer.DataChannelsClosed) assert.Equal(t, uint32(1), connStatsOffer.DataChannelsRequested) assert.Equal(t, uint32(0), connStatsOffer.DataChannelsAccepted) dcStatsOffer := getDataChannelStats(t, reportPCOffer, offerDC) assert.Equal(t, DataChannelStateOpen, dcStatsOffer.State) assert.Equal(t, uint32(1), dcStatsOffer.MessagesSent) assert.Equal(t, uint64(len(msg)), dcStatsOffer.BytesSent) assert.NotEmpty(t, findLocalCandidateStats(reportPCOffer)) assert.NotEmpty(t, findRemoteCandidateStats(reportPCOffer)) assert.NotEmpty(t, findCandidatePairStats(t, reportPCOffer)) connStatsAnswer = getConnectionStats(t, reportPCAnswer, answerPC) assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsOpened) assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsClosed) assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsRequested) assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsAccepted) dcStatsAnswer := getDataChannelStats(t, reportPCAnswer, answerDC) assert.Equal(t, DataChannelStateOpen, dcStatsAnswer.State) assert.Equal(t, uint32(1), dcStatsAnswer.MessagesReceived) assert.Equal(t, uint64(len(msg)), dcStatsAnswer.BytesReceived) assert.NotEmpty(t, findLocalCandidateStats(reportPCAnswer)) assert.NotEmpty(t, findRemoteCandidateStats(reportPCAnswer)) assert.NotEmpty(t, findCandidatePairStats(t, reportPCAnswer)) assert.NoError(t, err) for i := range offerPC.api.mediaEngine.videoCodecs { codecStat := getCodecStats(t, reportPCOffer, &(offerPC.api.mediaEngine.videoCodecs[i])) assert.NotEmpty(t, codecStat) } for i := range offerPC.api.mediaEngine.audioCodecs { codecStat := getCodecStats(t, reportPCOffer, &(offerPC.api.mediaEngine.audioCodecs[i])) assert.NotEmpty(t, codecStat) } // Close answer DC now dcWait = sync.WaitGroup{} dcWait.Add(1) offerDC.OnClose(func() { dcWait.Done() }) assert.NoError(t, answerDC.Close()) waitWithTimeout(t, &dcWait) time.Sleep(10 * time.Millisecond) reportPCOffer = offerPC.GetStats() reportPCAnswer = answerPC.GetStats() connStatsOffer = getConnectionStats(t, reportPCOffer, offerPC) assert.Equal(t, uint32(1), connStatsOffer.DataChannelsOpened) assert.Equal(t, uint32(1), connStatsOffer.DataChannelsClosed) assert.Equal(t, uint32(1), connStatsOffer.DataChannelsRequested) assert.Equal(t, uint32(0), connStatsOffer.DataChannelsAccepted) dcStatsOffer = getDataChannelStats(t, reportPCOffer, offerDC) assert.Equal(t, DataChannelStateClosed, dcStatsOffer.State) connStatsAnswer = getConnectionStats(t, reportPCAnswer, answerPC) assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsOpened) assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsClosed) assert.Equal(t, uint32(0), connStatsAnswer.DataChannelsRequested) assert.Equal(t, uint32(1), connStatsAnswer.DataChannelsAccepted) dcStatsAnswer = getDataChannelStats(t, reportPCAnswer, answerDC) assert.Equal(t, DataChannelStateClosed, dcStatsAnswer.State) answerICETransportStats := getTransportStats(t, reportPCAnswer, "iceTransport") offerICETransportStats := getTransportStats(t, reportPCOffer, "iceTransport") assert.GreaterOrEqual(t, offerICETransportStats.BytesSent, answerICETransportStats.BytesReceived) assert.GreaterOrEqual(t, answerICETransportStats.BytesSent, offerICETransportStats.BytesReceived) answerSCTPTransportStats := getTransportStats(t, reportPCAnswer, "sctpTransport") offerSCTPTransportStats := getTransportStats(t, reportPCOffer, "sctpTransport") assert.GreaterOrEqual(t, offerSCTPTransportStats.BytesSent, answerSCTPTransportStats.BytesReceived) assert.GreaterOrEqual(t, answerSCTPTransportStats.BytesSent, offerSCTPTransportStats.BytesReceived) certificates := offerPC.configuration.Certificates for i := range certificates { assert.NotEmpty(t, getCertificateStats(t, reportPCOffer, &certificates[i])) } closePairNow(t, offerPC, answerPC) } func TestPeerConnection_GetStats_Closed(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) assert.NoError(t, pc.Close()) pc.GetStats() } webrtc-3.1.56/test-wasm/000077500000000000000000000000001437620512100150215ustar00rootroot00000000000000webrtc-3.1.56/test-wasm/LICENSE000066400000000000000000000027071437620512100160340ustar00rootroot00000000000000Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. webrtc-3.1.56/test-wasm/go_js_wasm_exec000077500000000000000000000015331437620512100201050ustar00rootroot00000000000000#!/bin/bash # Copyright 2018 The Go Authors. All rights reserved. # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. # Modified work copyright 2019 Alex Browne. # Check Node.js version if [[ $(node --version) =~ v[0-9]\. ]] then echo "Node.js version >= 10 is required" exit 1 fi SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ]; do DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" SOURCE="$(readlink "$SOURCE")" [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" done DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" NODE_WASM_EXEC="$(go env GOROOT)/misc/wasm/wasm_exec_node.js" WASM_EXEC="$(go env GOROOT)/misc/wasm/wasm_exec.js" if test -f "$NODE_WASM_EXEC"; then exec node --require="${DIR}/node_shim.js" "$NODE_WASM_EXEC" "$@" else exec node --require="${DIR}/node_shim.js" "$WASM_EXEC" "$@" fi webrtc-3.1.56/test-wasm/node_shim.js000066400000000000000000000004161437620512100173250ustar00rootroot00000000000000// This file adds RTCPeerConnection to the global context, making Node.js more // closely match the browser API for WebRTC. const wrtc = require('wrtc') global.window = { RTCPeerConnection: wrtc.RTCPeerConnection } global.RTCPeerConnection = wrtc.RTCPeerConnection webrtc-3.1.56/track_local.go000066400000000000000000000057131437620512100157100ustar00rootroot00000000000000package webrtc import ( "github.com/pion/interceptor" "github.com/pion/rtp" ) // TrackLocalWriter is the Writer for outbound RTP Packets type TrackLocalWriter interface { // WriteRTP encrypts a RTP packet and writes to the connection WriteRTP(header *rtp.Header, payload []byte) (int, error) // Write encrypts and writes a full RTP packet Write(b []byte) (int, error) } // TrackLocalContext is the Context passed when a TrackLocal has been Binded/Unbinded from a PeerConnection, and used // in Interceptors. type TrackLocalContext struct { id string params RTPParameters ssrc SSRC writeStream TrackLocalWriter rtcpInterceptor interceptor.RTCPReader } // CodecParameters returns the negotiated RTPCodecParameters. These are the codecs supported by both // PeerConnections and the SSRC/PayloadTypes func (t *TrackLocalContext) CodecParameters() []RTPCodecParameters { return t.params.Codecs } // HeaderExtensions returns the negotiated RTPHeaderExtensionParameters. These are the header extensions supported by // both PeerConnections and the SSRC/PayloadTypes func (t *TrackLocalContext) HeaderExtensions() []RTPHeaderExtensionParameter { return t.params.HeaderExtensions } // SSRC requires the negotiated SSRC of this track // This track may have multiple if RTX is enabled func (t *TrackLocalContext) SSRC() SSRC { return t.ssrc } // WriteStream returns the WriteStream for this TrackLocal. The implementer writes the outbound // media packets to it func (t *TrackLocalContext) WriteStream() TrackLocalWriter { return t.writeStream } // ID is a unique identifier that is used for both Bind/Unbind func (t *TrackLocalContext) ID() string { return t.id } // RTCPReader returns the RTCP interceptor for this TrackLocal. Used to read RTCP of this TrackLocal. func (t *TrackLocalContext) RTCPReader() interceptor.RTCPReader { return t.rtcpInterceptor } // TrackLocal is an interface that controls how the user can send media // The user can provide their own TrackLocal implementations, or use // the implementations in pkg/media type TrackLocal interface { // Bind should implement the way how the media data flows from the Track to the PeerConnection // This will be called internally after signaling is complete and the list of available // codecs has been determined Bind(TrackLocalContext) (RTPCodecParameters, error) // Unbind should implement the teardown logic when the track is no longer needed. This happens // because a track has been stopped. Unbind(TrackLocalContext) error // ID is the unique identifier for this Track. This should be unique for the // stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' // and StreamID would be 'desktop' or 'webcam' ID() string // RID is the RTP Stream ID for this track. RID() string // StreamID is the group this track belongs too. This must be unique StreamID() string // Kind controls if this TrackLocal is audio or video Kind() RTPCodecType } webrtc-3.1.56/track_local_static.go000066400000000000000000000214431437620512100172550ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "strings" "sync" "github.com/pion/rtp" "github.com/pion/webrtc/v3/internal/util" "github.com/pion/webrtc/v3/pkg/media" ) // trackBinding is a single bind for a Track // Bind can be called multiple times, this stores the // result for a single bind call so that it can be used when writing type trackBinding struct { id string ssrc SSRC payloadType PayloadType writeStream TrackLocalWriter } // TrackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets. // If you wish to send a media.Sample use TrackLocalStaticSample type TrackLocalStaticRTP struct { mu sync.RWMutex bindings []trackBinding codec RTPCodecCapability id, rid, streamID string } // NewTrackLocalStaticRTP returns a TrackLocalStaticRTP. func NewTrackLocalStaticRTP(c RTPCodecCapability, id, streamID string, options ...func(*TrackLocalStaticRTP)) (*TrackLocalStaticRTP, error) { t := &TrackLocalStaticRTP{ codec: c, bindings: []trackBinding{}, id: id, streamID: streamID, } for _, option := range options { option(t) } return t, nil } // WithRTPStreamID sets the RTP stream ID for this TrackLocalStaticRTP. func WithRTPStreamID(rid string) func(*TrackLocalStaticRTP) { return func(t *TrackLocalStaticRTP) { t.rid = rid } } // Bind is called by the PeerConnection after negotiation is complete // This asserts that the code requested is supported by the remote peer. // If so it setups all the state (SSRC and PayloadType) to have a call func (s *TrackLocalStaticRTP) Bind(t TrackLocalContext) (RTPCodecParameters, error) { s.mu.Lock() defer s.mu.Unlock() parameters := RTPCodecParameters{RTPCodecCapability: s.codec} if codec, matchType := codecParametersFuzzySearch(parameters, t.CodecParameters()); matchType != codecMatchNone { s.bindings = append(s.bindings, trackBinding{ ssrc: t.SSRC(), payloadType: codec.PayloadType, writeStream: t.WriteStream(), id: t.ID(), }) return codec, nil } return RTPCodecParameters{}, ErrUnsupportedCodec } // Unbind implements the teardown logic when the track is no longer needed. This happens // because a track has been stopped. func (s *TrackLocalStaticRTP) Unbind(t TrackLocalContext) error { s.mu.Lock() defer s.mu.Unlock() for i := range s.bindings { if s.bindings[i].id == t.ID() { s.bindings[i] = s.bindings[len(s.bindings)-1] s.bindings = s.bindings[:len(s.bindings)-1] return nil } } return ErrUnbindFailed } // ID is the unique identifier for this Track. This should be unique for the // stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' // and StreamID would be 'desktop' or 'webcam' func (s *TrackLocalStaticRTP) ID() string { return s.id } // StreamID is the group this track belongs too. This must be unique func (s *TrackLocalStaticRTP) StreamID() string { return s.streamID } // RID is the RTP stream identifier. func (s *TrackLocalStaticRTP) RID() string { return s.rid } // Kind controls if this TrackLocal is audio or video func (s *TrackLocalStaticRTP) Kind() RTPCodecType { switch { case strings.HasPrefix(s.codec.MimeType, "audio/"): return RTPCodecTypeAudio case strings.HasPrefix(s.codec.MimeType, "video/"): return RTPCodecTypeVideo default: return RTPCodecType(0) } } // Codec gets the Codec of the track func (s *TrackLocalStaticRTP) Codec() RTPCodecCapability { return s.codec } // packetPool is a pool of packets used by WriteRTP and Write below // nolint:gochecknoglobals var rtpPacketPool = sync.Pool{ New: func() interface{} { return &rtp.Packet{} }, } func resetPacketPoolAllocation(localPacket *rtp.Packet) { *localPacket = rtp.Packet{} rtpPacketPool.Put(localPacket) } func getPacketAllocationFromPool() *rtp.Packet { ipacket := rtpPacketPool.Get() return ipacket.(*rtp.Packet) //nolint:forcetypeassert } // WriteRTP writes a RTP Packet to the TrackLocalStaticRTP // If one PeerConnection fails the packets will still be sent to // all PeerConnections. The error message will contain the ID of the failed // PeerConnections so you can remove them func (s *TrackLocalStaticRTP) WriteRTP(p *rtp.Packet) error { packet := getPacketAllocationFromPool() defer resetPacketPoolAllocation(packet) *packet = *p return s.writeRTP(packet) } // writeRTP is like WriteRTP, except that it may modify the packet p func (s *TrackLocalStaticRTP) writeRTP(p *rtp.Packet) error { s.mu.RLock() defer s.mu.RUnlock() writeErrs := []error{} for _, b := range s.bindings { p.Header.SSRC = uint32(b.ssrc) p.Header.PayloadType = uint8(b.payloadType) if _, err := b.writeStream.WriteRTP(&p.Header, p.Payload); err != nil { writeErrs = append(writeErrs, err) } } return util.FlattenErrs(writeErrs) } // Write writes a RTP Packet as a buffer to the TrackLocalStaticRTP // If one PeerConnection fails the packets will still be sent to // all PeerConnections. The error message will contain the ID of the failed // PeerConnections so you can remove them func (s *TrackLocalStaticRTP) Write(b []byte) (n int, err error) { packet := getPacketAllocationFromPool() defer resetPacketPoolAllocation(packet) if err = packet.Unmarshal(b); err != nil { return 0, err } return len(b), s.writeRTP(packet) } // TrackLocalStaticSample is a TrackLocal that has a pre-set codec and accepts Samples. // If you wish to send a RTP Packet use TrackLocalStaticRTP type TrackLocalStaticSample struct { packetizer rtp.Packetizer sequencer rtp.Sequencer rtpTrack *TrackLocalStaticRTP clockRate float64 } // NewTrackLocalStaticSample returns a TrackLocalStaticSample func NewTrackLocalStaticSample(c RTPCodecCapability, id, streamID string, options ...func(*TrackLocalStaticRTP)) (*TrackLocalStaticSample, error) { rtpTrack, err := NewTrackLocalStaticRTP(c, id, streamID, options...) if err != nil { return nil, err } return &TrackLocalStaticSample{ rtpTrack: rtpTrack, }, nil } // ID is the unique identifier for this Track. This should be unique for the // stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' // and StreamID would be 'desktop' or 'webcam' func (s *TrackLocalStaticSample) ID() string { return s.rtpTrack.ID() } // StreamID is the group this track belongs too. This must be unique func (s *TrackLocalStaticSample) StreamID() string { return s.rtpTrack.StreamID() } // RID is the RTP stream identifier. func (s *TrackLocalStaticSample) RID() string { return s.rtpTrack.RID() } // Kind controls if this TrackLocal is audio or video func (s *TrackLocalStaticSample) Kind() RTPCodecType { return s.rtpTrack.Kind() } // Codec gets the Codec of the track func (s *TrackLocalStaticSample) Codec() RTPCodecCapability { return s.rtpTrack.Codec() } // Bind is called by the PeerConnection after negotiation is complete // This asserts that the code requested is supported by the remote peer. // If so it setups all the state (SSRC and PayloadType) to have a call func (s *TrackLocalStaticSample) Bind(t TrackLocalContext) (RTPCodecParameters, error) { codec, err := s.rtpTrack.Bind(t) if err != nil { return codec, err } s.rtpTrack.mu.Lock() defer s.rtpTrack.mu.Unlock() // We only need one packetizer if s.packetizer != nil { return codec, nil } payloader, err := payloaderForCodec(codec.RTPCodecCapability) if err != nil { return codec, err } s.sequencer = rtp.NewRandomSequencer() s.packetizer = rtp.NewPacketizer( rtpOutboundMTU, 0, // Value is handled when writing 0, // Value is handled when writing payloader, s.sequencer, codec.ClockRate, ) s.clockRate = float64(codec.RTPCodecCapability.ClockRate) return codec, nil } // Unbind implements the teardown logic when the track is no longer needed. This happens // because a track has been stopped. func (s *TrackLocalStaticSample) Unbind(t TrackLocalContext) error { return s.rtpTrack.Unbind(t) } // WriteSample writes a Sample to the TrackLocalStaticSample // If one PeerConnection fails the packets will still be sent to // all PeerConnections. The error message will contain the ID of the failed // PeerConnections so you can remove them func (s *TrackLocalStaticSample) WriteSample(sample media.Sample) error { s.rtpTrack.mu.RLock() p := s.packetizer clockRate := s.clockRate s.rtpTrack.mu.RUnlock() if p == nil { return nil } // skip packets by the number of previously dropped packets for i := uint16(0); i < sample.PrevDroppedPackets; i++ { s.sequencer.NextSequenceNumber() } samples := uint32(sample.Duration.Seconds() * clockRate) if sample.PrevDroppedPackets > 0 { p.SkipSamples(samples * uint32(sample.PrevDroppedPackets)) } packets := p.Packetize(sample.Data, samples) writeErrs := []error{} for _, p := range packets { if err := s.rtpTrack.WriteRTP(p); err != nil { writeErrs = append(writeErrs, err) } } return util.FlattenErrs(writeErrs) } webrtc-3.1.56/track_local_static_test.go000066400000000000000000000157751437620512100203270ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "context" "errors" "testing" "time" "github.com/pion/rtp" "github.com/pion/transport/v2/test" "github.com/stretchr/testify/assert" ) // If a remote doesn't support a Codec used by a `TrackLocalStatic` // an error should be returned to the user func Test_TrackLocalStatic_NoCodecIntersection(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) t.Run("Offerer", func(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) noCodecPC, err := NewAPI().NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = pc.AddTrack(track) assert.NoError(t, err) assert.ErrorIs(t, signalPair(pc, noCodecPC), ErrUnsupportedCodec) closePairNow(t, noCodecPC, pc) }) t.Run("Answerer", func(t *testing.T) { pc, err := NewPeerConnection(Configuration{}) assert.NoError(t, err) m := &MediaEngine{} assert.NoError(t, m.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeType: "video/VP9", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, PayloadType: 96, }, RTPCodecTypeVideo)) vp9OnlyPC, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{}) assert.NoError(t, err) _, err = vp9OnlyPC.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) _, err = pc.AddTrack(track) assert.NoError(t, err) assert.True(t, errors.Is(signalPair(vp9OnlyPC, pc), ErrUnsupportedCodec)) closePairNow(t, vp9OnlyPC, pc) }) t.Run("Local", func(t *testing.T) { offerer, answerer, err := newPair() assert.NoError(t, err) invalidCodecTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/invalid-codec"}, "video", "pion") assert.NoError(t, err) _, err = offerer.AddTrack(invalidCodecTrack) assert.NoError(t, err) assert.True(t, errors.Is(signalPair(offerer, answerer), ErrUnsupportedCodec)) closePairNow(t, offerer, answerer) }) } // Assert that Bind/Unbind happens when expected func Test_TrackLocalStatic_Closed(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = pcOffer.AddTrack(vp8Writer) assert.NoError(t, err) assert.Equal(t, len(vp8Writer.bindings), 0, "No binding should exist before signaling") assert.NoError(t, signalPair(pcOffer, pcAnswer)) assert.Equal(t, len(vp8Writer.bindings), 1, "binding should exist after signaling") closePairNow(t, pcOffer, pcAnswer) assert.Equal(t, len(vp8Writer.bindings), 0, "No binding should exist after close") } func Test_TrackLocalStatic_PayloadType(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() mediaEngineOne := &MediaEngine{} assert.NoError(t, mediaEngineOne.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, PayloadType: 100, }, RTPCodecTypeVideo)) mediaEngineTwo := &MediaEngine{} assert.NoError(t, mediaEngineTwo.RegisterCodec(RTPCodecParameters{ RTPCodecCapability: RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, PayloadType: 200, }, RTPCodecTypeVideo)) offerer, err := NewAPI(WithMediaEngine(mediaEngineOne)).NewPeerConnection(Configuration{}) assert.NoError(t, err) answerer, err := NewAPI(WithMediaEngine(mediaEngineTwo)).NewPeerConnection(Configuration{}) assert.NoError(t, err) track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = offerer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) _, err = answerer.AddTrack(track) assert.NoError(t, err) onTrackFired, onTrackFiredFunc := context.WithCancel(context.Background()) offerer.OnTrack(func(track *TrackRemote, r *RTPReceiver) { assert.Equal(t, track.PayloadType(), PayloadType(100)) assert.Equal(t, track.Codec().RTPCodecCapability.MimeType, "video/VP8") onTrackFiredFunc() }) assert.NoError(t, signalPair(offerer, answerer)) sendVideoUntilDone(onTrackFired.Done(), t, []*TrackLocalStaticSample{track}) closePairNow(t, offerer, answerer) } // Assert that writing to a Track doesn't modify the input // Even though we can pass a pointer we shouldn't modify the incoming value func Test_TrackLocalStatic_Mutate_Input(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = pcOffer.AddTrack(vp8Writer) assert.NoError(t, err) assert.NoError(t, signalPair(pcOffer, pcAnswer)) pkt := &rtp.Packet{Header: rtp.Header{SSRC: 1, PayloadType: 1}} assert.NoError(t, vp8Writer.WriteRTP(pkt)) assert.Equal(t, pkt.Header.SSRC, uint32(1)) assert.Equal(t, pkt.Header.PayloadType, uint8(1)) closePairNow(t, pcOffer, pcAnswer) } // Assert that writing to a Track that has Binded (but not connected) // does not block func Test_TrackLocalStatic_Binding_NonBlocking(t *testing.T) { lim := test.TimeOut(time.Second * 5) defer lim.Stop() report := test.CheckRoutines(t) defer report() pcOffer, pcAnswer, err := newPair() assert.NoError(t, err) _, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(t, err) vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(t, err) _, err = pcAnswer.AddTrack(vp8Writer) assert.NoError(t, err) offer, err := pcOffer.CreateOffer(nil) assert.NoError(t, err) assert.NoError(t, pcAnswer.SetRemoteDescription(offer)) answer, err := pcAnswer.CreateAnswer(nil) assert.NoError(t, err) assert.NoError(t, pcAnswer.SetLocalDescription(answer)) _, err = vp8Writer.Write(make([]byte, 20)) assert.NoError(t, err) closePairNow(t, pcOffer, pcAnswer) } func BenchmarkTrackLocalWrite(b *testing.B) { offerPC, answerPC, err := newPair() defer closePairNow(b, offerPC, answerPC) if err != nil { b.Fatalf("Failed to create a PC pair for testing") } track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion") assert.NoError(b, err) _, err = offerPC.AddTrack(track) assert.NoError(b, err) _, err = answerPC.AddTransceiverFromKind(RTPCodecTypeVideo) assert.NoError(b, err) b.SetBytes(1024) buf := make([]byte, 1024) for i := 0; i < b.N; i++ { _, err := track.Write(buf) assert.NoError(b, err) } } webrtc-3.1.56/track_remote.go000066400000000000000000000107271437620512100161120ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "sync" "time" "github.com/pion/interceptor" "github.com/pion/rtp" ) // TrackRemote represents a single inbound source of media type TrackRemote struct { mu sync.RWMutex id string streamID string payloadType PayloadType kind RTPCodecType ssrc SSRC codec RTPCodecParameters params RTPParameters rid string receiver *RTPReceiver peeked []byte peekedAttributes interceptor.Attributes } func newTrackRemote(kind RTPCodecType, ssrc SSRC, rid string, receiver *RTPReceiver) *TrackRemote { return &TrackRemote{ kind: kind, ssrc: ssrc, rid: rid, receiver: receiver, } } // ID is the unique identifier for this Track. This should be unique for the // stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' // and StreamID would be 'desktop' or 'webcam' func (t *TrackRemote) ID() string { t.mu.RLock() defer t.mu.RUnlock() return t.id } // RID gets the RTP Stream ID of this Track // With Simulcast you will have multiple tracks with the same ID, but different RID values. // In many cases a TrackRemote will not have an RID, so it is important to assert it is non-zero func (t *TrackRemote) RID() string { t.mu.RLock() defer t.mu.RUnlock() return t.rid } // PayloadType gets the PayloadType of the track func (t *TrackRemote) PayloadType() PayloadType { t.mu.RLock() defer t.mu.RUnlock() return t.payloadType } // Kind gets the Kind of the track func (t *TrackRemote) Kind() RTPCodecType { t.mu.RLock() defer t.mu.RUnlock() return t.kind } // StreamID is the group this track belongs too. This must be unique func (t *TrackRemote) StreamID() string { t.mu.RLock() defer t.mu.RUnlock() return t.streamID } // SSRC gets the SSRC of the track func (t *TrackRemote) SSRC() SSRC { t.mu.RLock() defer t.mu.RUnlock() return t.ssrc } // Msid gets the Msid of the track func (t *TrackRemote) Msid() string { return t.StreamID() + " " + t.ID() } // Codec gets the Codec of the track func (t *TrackRemote) Codec() RTPCodecParameters { t.mu.RLock() defer t.mu.RUnlock() return t.codec } // Read reads data from the track. func (t *TrackRemote) Read(b []byte) (n int, attributes interceptor.Attributes, err error) { t.mu.RLock() r := t.receiver peeked := t.peeked != nil t.mu.RUnlock() if peeked { t.mu.Lock() data := t.peeked attributes = t.peekedAttributes t.peeked = nil t.peekedAttributes = nil t.mu.Unlock() // someone else may have stolen our packet when we // released the lock. Deal with it. if data != nil { n = copy(b, data) err = t.checkAndUpdateTrack(b) return } } n, attributes, err = r.readRTP(b, t) if err != nil { return } err = t.checkAndUpdateTrack(b) return } // checkAndUpdateTrack checks payloadType for every incoming packet // once a different payloadType is detected the track will be updated func (t *TrackRemote) checkAndUpdateTrack(b []byte) error { if len(b) < 2 { return errRTPTooShort } if payloadType := PayloadType(b[1] & rtpPayloadTypeBitmask); payloadType != t.PayloadType() { t.mu.Lock() defer t.mu.Unlock() params, err := t.receiver.api.mediaEngine.getRTPParametersByPayloadType(payloadType) if err != nil { return err } t.kind = t.receiver.kind t.payloadType = payloadType t.codec = params.Codecs[0] t.params = params } return nil } // ReadRTP is a convenience method that wraps Read and unmarshals for you. func (t *TrackRemote) ReadRTP() (*rtp.Packet, interceptor.Attributes, error) { b := make([]byte, t.receiver.api.settingEngine.getReceiveMTU()) i, attributes, err := t.Read(b) if err != nil { return nil, nil, err } r := &rtp.Packet{} if err := r.Unmarshal(b[:i]); err != nil { return nil, nil, err } return r, attributes, nil } // peek is like Read, but it doesn't discard the packet read func (t *TrackRemote) peek(b []byte) (n int, a interceptor.Attributes, err error) { n, a, err = t.Read(b) if err != nil { return } t.mu.Lock() // this might overwrite data if somebody peeked between the Read // and us getting the lock. Oh well, we'll just drop a packet in // that case. data := make([]byte, n) n = copy(data, b[:n]) t.peeked = data t.peekedAttributes = a t.mu.Unlock() return } // SetReadDeadline sets the max amount of time the RTP stream will block before returning. 0 is forever. func (t *TrackRemote) SetReadDeadline(deadline time.Time) error { return t.receiver.setRTPReadDeadline(deadline, t) } webrtc-3.1.56/track_test.go000066400000000000000000000000551437620512100155670ustar00rootroot00000000000000//go:build !js // +build !js package webrtc webrtc-3.1.56/vnet_test.go000066400000000000000000000036501437620512100154430ustar00rootroot00000000000000//go:build !js // +build !js package webrtc import ( "testing" "time" "github.com/pion/logging" "github.com/pion/transport/v2/vnet" "github.com/stretchr/testify/assert" ) func createVNetPair(t *testing.T) (*PeerConnection, *PeerConnection, *vnet.Router) { // Create a root router wan, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "1.2.3.0/24", LoggerFactory: logging.NewDefaultLoggerFactory(), }) assert.NoError(t, err) // Create a network interface for offerer offerVNet, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"1.2.3.4"}, }) assert.NoError(t, err) // Add the network interface to the router assert.NoError(t, wan.AddNet(offerVNet)) offerSettingEngine := SettingEngine{} offerSettingEngine.SetVNet(offerVNet) offerSettingEngine.SetICETimeouts(time.Second, time.Second, time.Millisecond*200) // Create a network interface for answerer answerVNet, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"1.2.3.5"}, }) assert.NoError(t, err) // Add the network interface to the router assert.NoError(t, wan.AddNet(answerVNet)) answerSettingEngine := SettingEngine{} answerSettingEngine.SetVNet(answerVNet) answerSettingEngine.SetICETimeouts(time.Second, time.Second, time.Millisecond*200) // Start the virtual network by calling Start() on the root router assert.NoError(t, wan.Start()) offerMediaEngine := &MediaEngine{} assert.NoError(t, offerMediaEngine.RegisterDefaultCodecs()) offerPeerConnection, err := NewAPI(WithSettingEngine(offerSettingEngine), WithMediaEngine(offerMediaEngine)).NewPeerConnection(Configuration{}) assert.NoError(t, err) answerMediaEngine := &MediaEngine{} assert.NoError(t, answerMediaEngine.RegisterDefaultCodecs()) answerPeerConnection, err := NewAPI(WithSettingEngine(answerSettingEngine), WithMediaEngine(answerMediaEngine)).NewPeerConnection(Configuration{}) assert.NoError(t, err) return offerPeerConnection, answerPeerConnection, wan } webrtc-3.1.56/webrtc.go000066400000000000000000000012011437620512100147040ustar00rootroot00000000000000// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document. package webrtc // SSRC represents a synchronization source // A synchronization source is a randomly chosen // value meant to be globally unique within a particular // RTP session. Used to identify a single stream of media. // // https://tools.ietf.org/html/rfc3550#section-3 type SSRC uint32 // PayloadType identifies the format of the RTP payload and determines // its interpretation by the application. Each codec in a RTP Session // will have a different PayloadType // // https://tools.ietf.org/html/rfc3550#section-3 type PayloadType uint8 webrtc-3.1.56/yarn.lock000066400000000000000000000770351437620512100147340ustar00rootroot00000000000000# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== ajv@^6.5.5: version "6.12.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== dependencies: delegates "^1.0.0" readable-stream "^2.0.6" asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== dependencies: safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: version "1.10.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= dependencies: tweetnacl "^0.14.3" brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= dependencies: assert-plus "^1.0.0" debug@^2.1.2: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== dependencies: webidl-conversions "^4.0.2" ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= dependencies: jsbn "~0.1.0" safer-buffer "^2.1.0" extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= fast-deep-equal@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== dependencies: asynckit "^0.4.0" combined-stream "^1.0.6" mime-types "^2.1.12" fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== dependencies: minipass "^2.2.1" fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= dependencies: aproba "^1.0.3" console-control-strings "^1.0.0" has-unicode "^2.0.0" object-assign "^4.1.0" signal-exit "^3.0.0" string-width "^1.0.1" strip-ansi "^3.0.1" wide-align "^1.1.0" getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= dependencies: assert-plus "^1.0.0" glob@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== dependencies: ajv "^6.5.5" har-schema "^2.0.0" has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= dependencies: assert-plus "^1.0.0" jsprim "^1.2.2" sshpk "^1.7.0" iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" ignore-walk@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== dependencies: minimatch "^3.0.4" inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" wrappy "1" inherits@2, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= dependencies: number-is-nan "^1.0.0" is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= dependencies: assert-plus "1.0.0" extsprintf "1.3.0" json-schema "0.2.3" verror "1.10.0" mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== mime-types@^2.1.12, mime-types@~2.1.19: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: mime-db "1.44.0" minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= minipass@^2.2.1, minipass@^2.3.4: version "2.3.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== dependencies: safe-buffer "^5.1.2" yallist "^3.0.0" minizlib@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== dependencies: minipass "^2.2.1" mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= needle@^2.2.1: version "2.2.4" resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== dependencies: debug "^2.1.2" iconv-lite "^0.4.4" sax "^1.2.4" node-pre-gyp@^0.13.0: version "0.13.0" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz#df9ab7b68dd6498137717838e4f92a33fc9daa42" integrity sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" needle "^2.2.1" nopt "^4.0.1" npm-packlist "^1.1.6" npmlog "^4.0.2" rc "^1.2.7" rimraf "^2.6.1" semver "^5.3.0" tar "^4" nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= dependencies: abbrev "1" osenv "^0.1.4" npm-bundled@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== npm-packlist@^1.1.6: version "1.4.1" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== dependencies: are-we-there-yet "~1.1.2" console-control-strings "~1.1.0" gauge "~2.7.3" set-blocking "~2.0.0" number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= os-tmpdir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= osenv@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== dependencies: os-homedir "^1.0.0" os-tmpdir "^1.0.0" path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== dependencies: deep-extend "^0.6.0" ini "~1.3.0" minimist "^1.2.0" strip-json-comments "~2.0.1" readable-stream@^2.0.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" isarray "~1.0.0" process-nextick-args "~2.0.0" safe-buffer "~5.1.1" string_decoder "~1.1.1" util-deprecate "~1.0.1" request@2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" caseless "~0.12.0" combined-stream "~1.0.6" extend "~3.0.2" forever-agent "~0.6.1" form-data "~2.3.2" har-validator "~5.1.3" http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" mime-types "~2.1.19" oauth-sign "~0.9.0" performance-now "^2.1.0" qs "~6.5.2" safe-buffer "^5.1.2" tough-cookie "~2.5.0" tunnel-agent "^0.6.0" uuid "^3.3.2" rimraf@^2.6.1: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== dependencies: glob "^7.1.3" safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== semver@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" bcrypt-pbkdf "^1.0.0" dashdash "^1.12.0" ecc-jsbn "~0.1.1" getpass "^0.1.1" jsbn "~0.1.0" safer-buffer "^2.0.2" tweetnacl "~0.14.0" string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= dependencies: code-point-at "^1.0.0" is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" "string-width@^1.0.2 || 2": version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== dependencies: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= dependencies: ansi-regex "^2.0.0" strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= dependencies: ansi-regex "^3.0.0" strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= tar@^4: version "4.4.8" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== dependencies: chownr "^1.1.1" fs-minipass "^1.2.5" minipass "^2.3.4" minizlib "^1.1.1" mkdirp "^0.5.0" safe-buffer "^5.1.2" yallist "^3.0.2" tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== dependencies: psl "^1.1.28" punycode "^2.1.1" tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= dependencies: safe-buffer "^5.0.1" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== dependencies: punycode "^2.1.0" util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= dependencies: assert-plus "^1.0.0" core-util-is "1.0.2" extsprintf "^1.2.0" webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== dependencies: string-width "^1.0.2 || 2" wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= wrtc@0.4.7: version "0.4.7" resolved "https://registry.yarnpkg.com/wrtc/-/wrtc-0.4.7.tgz#c61530cd662713e50bffe64b7a78673ce070426c" integrity sha512-P6Hn7VT4lfSH49HxLHcHhDq+aFf/jd9dPY7lDHeFhZ22N3858EKuwm2jmnlPzpsRGEPaoF6XwkcxY5SYnt4f/g== dependencies: node-pre-gyp "^0.13.0" optionalDependencies: domexception "^1.0.1" yallist@^3.0.0, yallist@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==