From 001f6887293e7f8fb838f2bbadbf21af95f2d68e Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 1 Oct 2021 02:47:18 +0900 Subject: [PATCH] Better audio management (#10) * add audio_session package * `AudioManager` initial design * try to integrate audio manager * configure audio session * `DisposeAware` disposing if already published causes exception. * organize * guard flutter_webrtc calls * websocket async fix * use `ConnectionState` instead of `_isClosed` and `isReconnecting` * change create audio track defaults * update protos * protocol 3 speaker updates * fix exception * emit `RoomDisconnectedEvent` only once * fix exception * keep track of local / remote audio tracks * explicit types * manage track state * re-structure audio management * change defaults * unpublish all * use experimental build * change defaults * configuring is optional * call native `RTCAudioSession.setConfiguration` * defaults adjustment * iOS only for now * use lib 92.4515.07 * clean up * revert audio options for now * remove pod source * rename apple related audio * `createListener` method * organize native audio * `SpeakingChangedEvent` only on `Participant` * refactoring * fix ios compile * fix configure audio only for iOS logic * minor fix & clean up * change dispose logic * format protos * fix unpublishTrack * `createListener` into a mixin * simplify * use flutter_webrtc master * unpublish for example * update ios icon * android icon * web icon * favicon --- .../android/app/src/main/AndroidManifest.xml | 2 +- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../res/mipmap-hdpi/livekit_ic_launcher.png | Bin 0 -> 1104 bytes .../mipmap-hdpi/livekit_ic_launcher_round.png | Bin 0 -> 1643 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../res/mipmap-mdpi/livekit_ic_launcher.png | Bin 0 -> 867 bytes .../mipmap-mdpi/livekit_ic_launcher_round.png | Bin 0 -> 1217 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../res/mipmap-xhdpi/livekit_ic_launcher.png | Bin 0 -> 1242 bytes .../livekit_ic_launcher_round.png | Bin 0 -> 2039 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../res/mipmap-xxhdpi/livekit_ic_launcher.png | Bin 0 -> 1717 bytes .../livekit_ic_launcher_round.png | Bin 0 -> 2884 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../mipmap-xxxhdpi/livekit_ic_launcher.png | Bin 0 -> 2184 bytes .../livekit_ic_launcher_round.png | Bin 0 -> 3826 bytes example/ios/Podfile.lock | 15 +- example/ios/Runner.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/xcschemes/Runner.xcscheme | 10 +- .../AppIcon-LiveKit.appiconset/Contents.json | 116 +++++++++ .../ios-marketing-1024@1x.png | Bin 0 -> 8044 bytes .../AppIcon-LiveKit.appiconset/ipad-20@1x.png | Bin 0 -> 539 bytes .../AppIcon-LiveKit.appiconset/ipad-20@2x.png | Bin 0 -> 577 bytes .../AppIcon-LiveKit.appiconset/ipad-29@1x.png | Bin 0 -> 568 bytes .../AppIcon-LiveKit.appiconset/ipad-29@2x.png | Bin 0 -> 575 bytes .../AppIcon-LiveKit.appiconset/ipad-40@1x.png | Bin 0 -> 577 bytes .../AppIcon-LiveKit.appiconset/ipad-40@2x.png | Bin 0 -> 668 bytes .../AppIcon-LiveKit.appiconset/ipad-76@1x.png | Bin 0 -> 619 bytes .../AppIcon-LiveKit.appiconset/ipad-76@2x.png | Bin 0 -> 881 bytes .../ipad-83.5@2x.png | Bin 0 -> 954 bytes .../iphone-20@2x.png | Bin 0 -> 577 bytes .../iphone-20@3x.png | Bin 0 -> 651 bytes .../iphone-29@2x.png | Bin 0 -> 575 bytes .../iphone-29@3x.png | Bin 0 -> 654 bytes .../iphone-40@2x.png | Bin 0 -> 668 bytes .../iphone-40@3x.png | Bin 0 -> 771 bytes .../iphone-60@2x.png | Bin 0 -> 771 bytes .../iphone-60@3x.png | Bin 0 -> 955 bytes .../AppIcon.appiconset/Contents.json | 122 ---------- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes .../ios/Runner/Assets.xcassets/Contents.json | 6 + example/ios/Runner/Info.plist | 2 +- example/lib/exts.dart | 18 ++ example/lib/main.dart | 4 +- example/lib/pages/room.dart | 7 +- example/lib/widgets/controls.dart | 16 +- example/pubspec.lock | 8 +- example/web/favicon.png | Bin 917 -> 2968 bytes example/web/icons/Icon-192.png | Bin 5292 -> 4716 bytes example/web/icons/Icon-512.png | Bin 8252 -> 9587 bytes ios/.gitignore | 38 +++ ios/Assets/.gitkeep | 0 ios/Classes/LivekitClientPlugin.h | 4 + ios/Classes/LivekitClientPlugin.m | 15 ++ ios/Classes/SwiftLiveKitClientPlugin.swift | 114 +++++++++ ios/livekit_client.podspec | 26 ++ lib/livekit_client.dart | 2 +- lib/src/classes/change_notifier.dart | 32 --- lib/src/events.dart | 51 ++-- lib/src/{errors.dart => exceptions.dart} | 3 - lib/src/managers/delay.dart | 18 +- lib/src/managers/event.dart | 58 +++-- lib/src/participant/local_participant.dart | 88 ++++--- lib/src/participant/participant.dart | 29 +-- lib/src/participant/remote_participant.dart | 11 +- lib/src/proto/livekit_models.pb.dart | 126 ++++++++++ lib/src/proto/livekit_models.pbjson.dart | 17 +- lib/src/proto/livekit_rtc.pb.dart | 197 ++++++++++++--- lib/src/proto/livekit_rtc.pbjson.dart | 51 +++- lib/src/room.dart | 182 +++++++------- lib/src/rtc_engine.dart | 230 +++++++++--------- lib/src/signal_client.dart | 63 ++--- lib/src/support/disposable.dart | 90 +++++++ lib/src/support/native_audio.dart | 122 ++++++++++ .../platform => support/platforms}/io.dart | 41 ++-- .../platform => support/platforms}/web.dart | 14 +- .../interface.dart => support/websocket.dart} | 10 +- lib/src/track/audio_track.dart | 167 ++++++++++++- lib/src/track/local_audio_track.dart | 8 +- lib/src/track/local_video_track.dart | 2 +- lib/src/track/remote_track_publication.dart | 6 +- lib/src/track/track.dart | 42 +++- lib/src/track/track_publication.dart | 5 +- lib/src/track/video_track.dart | 10 +- lib/src/transport.dart | 93 ++++--- pubspec.lock | 8 +- pubspec.yaml | 29 +-- 101 files changed, 1658 insertions(+), 681 deletions(-) delete mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-hdpi/livekit_ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-hdpi/livekit_ic_launcher_round.png delete mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/livekit_ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/livekit_ic_launcher_round.png delete mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/livekit_ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/livekit_ic_launcher_round.png delete mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/livekit_ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/livekit_ic_launcher_round.png delete mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/livekit_ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/livekit_ic_launcher_round.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/Contents.json create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ios-marketing-1024@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-20@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-20@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-29@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-29@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-40@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-40@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-76@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-76@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-83.5@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-20@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-20@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-29@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-29@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-40@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-40@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-60@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-60@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/Contents.json create mode 100644 ios/.gitignore create mode 100644 ios/Assets/.gitkeep create mode 100644 ios/Classes/LivekitClientPlugin.h create mode 100644 ios/Classes/LivekitClientPlugin.m create mode 100644 ios/Classes/SwiftLiveKitClientPlugin.swift create mode 100644 ios/livekit_client.podspec delete mode 100644 lib/src/classes/change_notifier.dart rename lib/src/{errors.dart => exceptions.dart} (99%) create mode 100644 lib/src/support/disposable.dart create mode 100644 lib/src/support/native_audio.dart rename lib/src/{ws/platform => support/platforms}/io.dart (54%) rename lib/src/{ws/platform => support/platforms}/web.dart (83%) rename lib/src/{ws/interface.dart => support/websocket.dart} (82%) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index e439f3f..d690627 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ + android:icon="@mipmap/livekit_ic_launcher"> P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/example/android/app/src/main/res/mipmap-hdpi/livekit_ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/livekit_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..062038ba7c14ce3e5853304b744da73907c5946d GIT binary patch literal 1104 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9GG!XV7ZFl&wk0|N__ zr;B4q#jUq@&gWlFlQ~{^|Jpq^cZa|1)4U!Z+P^_8Oe?oNjI*gMqqu&_)23@CqV1l} zGUcv4^12&*W!QE*3v=FR487g=)+JK-Hs`G7D_8uZMIJxd{Qk2^FPCpX;C%O}8`mG{ z79YIy`PJj&$JXUU>TYwnWBd; zwWWP?9mGo>ZLtp6d*$5ft5VFSkvpDV)t&e8+^b`i{tXV{TNPK=%|GmL_WGjTpG2xp zIp{`Da%gIdQqvK(crPd*(b0&EW-(v<|7_uIwhN1u!d^RsN`?3ae~MUO-^lby!)v90af14OnR(S4gveEm5OBXL&dMTZp8PfM9o~6vqLG{IXcYemXDhocQ zN{KM~X0Xg!!@RVd;pLv*iexrJ0cM;ifceCc&l#Oxq$X@;U84Q-N7)*_Ex%6vP!4B4 zza;+$%hSdG+2v2Bn<(h7pYX)z(!`o$y74S0wyQ3ZI`|~*Yuv*5yBe$X_B-6q;Y{w< zn^bT8>S1ls&5M_g7>h67;=fkpL0+4{)Kyi@(Pk4$ON<&i?flp*%IbJ)i&ek;-(fnt zM9u&H`S(90YJbQ)-Y?dhTKi>c!@H!S|4Rcr=Evx*yzIMAee&x`+g?SUO}pHgW@w~wn@)c6QtkSZx2tDw`<_=~_+s}s|ddRplA{?J9%Y)J}8Xu&j4ig~)ubHRRz6_p>?Y}aUfXBcq0 zZriS0!!r)L*11RDJW$|Wb;e|3<1%iOBp@O=HlqdA2QY}QNOP0rc)Bd`3<|4IeLe5RN8?<|chURJm@YtvtDmOT}l U)-bzopr0H|06xBvhE literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-hdpi/livekit_ic_launcher_round.png b/example/android/app/src/main/res/mipmap-hdpi/livekit_ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..15a5dc4a0c7b40d6cbe47d58712b893b19fb3e07 GIT binary patch literal 1643 zcmV-x29)`UP)pEf$)G~Y$T}Bm=T;jXyV4&-rFwRyT5G)+~>{lr4GH|rs2VlFur6vT|9%9W;B zj{G`;znw^qMFlY$s#{gMs?{z89!^G%g#|HTJmKm@SF243T2EGv#RVCXz-Cc$N)hm# zdy$g?f}+(;bHALboF|iMkTXS3wbRN zM>}W%hy8vUf}}~(jp`N@M|j$j(uNLoA0UyGeWKFFFBRJJx0f2c9;$Z(j~E(&!|gUC zafyny+`?tY zPUL(vOPVIg8xMz^2?4B)Jmq zQ7mx;mLkaCb5PKAJ~^4xPFDoE21Ig55N~;2dry0%iyeXEIV9-VJ@2$fkol-?y;(1) zNB#}@ee*i60YOCwcsz>*jiKFnJf#yqkhHGE5xkv+f;7l^NywQo1YHUAEE2>gC*Kl! z>JUNKA^30>2;!5IJJWJ*6@hO`uDV%P5TBgfoR`joL1<~>hTD>Y#&F%b1vxtp36Db5 zFPS4q>IFr43z@i|WOgPa78b-ki*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uu~SB)4&r;!(=UoX)Avw zQ&xVDuA5V}-29Z4YgJBL6x3~k{VnM85;y^ywsL)^1@8n6{+tCk&XEK!8<9KTBm$fY zNwg?_%_D>Ypl4!^1hmg#72ppxuYWVd0!)z!zKH}72}A;sK-dLh84aSi0&AYYt}kFy zP)Q(s_4J_9Ob;2F3}0?ZeXSykA0Y^j*2?I2AOGAOY%x~qc3iGfuOgCBJK zsMcXQHV$kX)qwE5HR7jT$1_XEL#u#UNtEto1ESpE*VhJm-xx^m=$O$pOl!fJA=1Qr zv4~>5hJ7@^sGLj*l+KqW$k4G^LKlgQd@UeR$Rm*#V>p+?gsOry{1SBJ%22t!WIzTk z(Y<0|Y+J>+nnx;MK&lXoaUw|)JPW&xy6_=z91Xc1d^126bXz82yhaK&B7k60X3qFHrCVg2LMlE-+mCFXN`J=M&*Dqz!L>|>GW;p-=002ovPDHLkV1i&1cG3U< literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-mdpi/livekit_ic_launcher_round.png b/example/android/app/src/main/res/mipmap-mdpi/livekit_ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f7181253e456934dd120f2771bdd9a22b36d9a GIT binary patch literal 1217 zcmV;y1U~zTP)a^Q*4D6j{{%;^0*pBv9o~h`d;@ z@NZXN^(G2121a6Fuk-ylC~ptnBmu?((b~cWBcjvh z00h=|3*Q0(CJ%|1pA`P-?W;{22{1Py(XpFAN+f&>1!$tAe7IBiE>LKD0e32OKKelk;!J`RIJ>$^tK||E$sz;ODKE$GPWawS zdMB^}$e$R5Fyzh%o}EO3iQ;9nmL=qyn3vA~1>g@E6ukbc|mZ(@HhqrU$jLrHK< zwhm$mN8qw2kZ}b>;j)7T##EFNJB9A3F%3wR&`_RUiy72= z!3|Q=egccY;9Qp8b@ths44*%8DiMn2ySk(K0-z_eO~4BzwG5;GL2OGSbDDEVP(AGs zzyOcGo)o&9zkfM`2u{&A~|h#nNBY} zygD^jfL9y7DXQyTF(jTs_z`zB8YaLeY<8^0DOn4t?-_ f82$Iv<8S{5ZF6&&SfxL_00000NkvXXu0mjfp<^4h literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391482be68e9e4a07fab769b5de337d16eb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 721 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!iOI#yLg7ec#$`gxH85~pclTsBt za}(23gHjVyDhp4h+5i=O3-AeX1=1l$e`s#|#^}+&7(N@w0CIr{$Oe+Uk^K-ZP~83C zcc@hG6rikF&NPT(23>y!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/livekit_ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/livekit_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..58572cf7c4741f8190f4ff37df0b73c577e07f25 GIT binary patch literal 1242 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGooCO|{#S9GG!XV7ZFl&wk0|QHs zr;B4q#jUq@-{%L1O0a*}{J(`Y{-xLrlYN2CA9{H-3eSSjsV^-QIi?b0o?XZYh@^wPlo zr~>o(=s$b+-1`5*_2F-xgw57@Mu#5gb?e`McIS2K$KSjQCY!ode2>)ruOP1eru5@) z5eL-=G8I`Lm8Th>Xe}%i+279?HFpBvr}Z|45585MXFt$qoS1q1pv=irkFC1zzh`*+ zq(5)}-5AD(JcjC>o4=nnFAiT!)}RnXjPvrSh?bVx+&OzxD~6|$_b&7yX-!^bAy<+-(;#c7`^tzKJfiF=*X z=xcm?fkn+tRbk2B>wJX6W4-PaWt`innJKOjd112UIUVICJGkYpWZlYV+Sbl^YKgry z<0PI57dRFO)$=$!&;5`riz_tnh*zv+P@Bgc%F!;7P$RtH0^0@AuM$g`ES`Jp%KvxK zh53A2-Fev!FD@PN=P8)Yxa6+W1+jM)UmXfoX{-z~|6b>`n_2JUsw;C2zxu^net%)lHJ?uHIlFVQ`blfPqm}!9?ooL@f5JaL=@8CWPg;K|yFIrtnmBQZd;YwY7oJ=- z@{KFb{FL7N@Tc8_*H`rQ&feSM-MLig`MQZ6*ZqE`D6OCOB{Tlc;*IOC|F9{De$Tz4 z*fIRxc5u zo)#>bXwcyQ>oCI|*59HBw%5p&a4q_37Qq-(sCkNE?R56dX>r$CaR*StKgJIX30wzO zYBY#4%wX(@D2ilEVNA&3KGm^sMmZzHF$TLpSpzPHWQN~I3eHGN%=%c&aG{Rj|IZiA z=VB&kGGy2{*xz9ah!yyp&*X6X)&JO9kLu&)PbCxi#gDT(oDcseC0@V2;?{#lG7nf7 zUPLmTvwrlSP5gzf-IVu?3)Hp$FSv2`!RLet+*?^1rtfzopr0PSEKdjJ3c literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xhdpi/livekit_ic_launcher_round.png b/example/android/app/src/main/res/mipmap-xhdpi/livekit_ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..706922f5f945722d46ceb98fe0702229c20670f7 GIT binary patch literal 2039 zcmV}C^p}VE+-sb1`>`p_W zwB2oY&&=$eNj@1;*iQI<_FU%7nR9G5lO9B52g|@FP1E+_+zS2#*FY!e10kJ0{b$$o zf77aeW|RIq>^5;sd?2vrz$&mGfn5YKO7GE$-+dV8^*ApOiQ}j|=bzs<9IZqsXAw%2 zI34=6&J1YLim$`+{G1Bi<&kE(JaSRlX{iMQh9F=h6yhi#eue;ofT5isZ%aeiS6CnN z&dm1Ui=7_n63zqx1Oa3M0R#aA0gM|BA>U?*n4L|*&qm-MgANw_2qqOU!tMI7uiWw? zoE~{8!v1S?@D>p;!WFQ>g5P@id8b=Ggmdg^;af-mLqlmeULW?pWRY*ZREmIm#|z$q z0!HY8gtexw>k1a zRKmBq0G(d&meJq*l?XGCYWP_q042dUfa++CQ9Ye?V0bFwXNv%x5JYU`8@^lGI8hff z#{@89Xcx+O)%ovC=bN9U0vO@?kZ*OS!(WE*g3}E@O9e0 zR?^-@Mfh`^Zn-U8;b*mg#C<}0T5r}6*uiv$pX~zT5<=c@(-r=E$qp|F0Z$I%c}u4| zzVl)-18YLSgaddBrz4;#6XEBO08~Z3qf_m2uE<2ylWPLlONC zy_VABO_y7)$Yl7rCm_z2WP3fcz%3yl?Q|qn3h?cBWjg#E6+n}YnxgKXfpD%10U6`w zgpkf&wMcs;U>5{@!bbqZG0~V$E1*n+l163jgn$Rb+@^$% zIR+_4Uxg*w5xPseAz>FEnx9_%oR;li5D*W_PRUeprknhOVH(zAG!Tu_KrF@wTx$^L zo@j&)hP!ERxQo7k2u=)vu@izzjw@Awcu{;zp>@p)HQe>no^F{wmHj~G!XCe$8gTxy zyPM=lhz2N153~sN^P_x>07s*G77#Di995|pBDQvPQB9YRw)*%{1FF4V+9^r&Afi`+ z8rRy-^H}(WO^O{v0rA4|qe>+w6sqlzXuIU2I$synb@5?4*aivpCyS~xFG}>Cg3V70bU`%FadX%fLyajcE``3+q6kwJ*-aHBRP1jN7jWi}%Yy*ld8VF5vezuIQxf!MNungo278lhoGmLr)3@n0*JhqivZu69^cDfMy7Au>BKk zxpY8)9RkWBz=J>{90f#hJr4wIfdIQr9PyV#o<%sTza<3CIM-Fa{_{u%b!CbfY5F0=kW{|C^5 VzvyNakuU%N002ovPDHLkV1n5crEmZM literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/livekit_ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/livekit_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b9ab35d4dd9eeb4505db7763e00e8a34b411bd00 GIT binary patch literal 1717 zcmb_dX;ji#6o25Z*-E*PN~R?vYATtcCYcLDrnwwTTg-C76;~u+`e!wxB4%bP70R(u z3%4?toEjTX?ur>qHi%0{3sO|Ztr{C^=;M6t%!m7a@7(+QeYo$wdvA{aQEy#sLu~+n zt`ET@K#2w4OjBKX%Dko?D?y7wIFSl~&bDs`1Gnzz1E3!5<8de`b98=OkQ5ZG-?J#P z>Tq-KU>A4tkY540`X_DA+?Mbo4Dv26T#Hxdh3Y3&YdK!<@t;HDJ1kOzBFMR2dP~Z< zreM6f!BWf$oUTutWVCYxu_hGxsQS}vhpA(}Eu;P*XmxCEb=n&daJPbAV`rQcNgw=7 zs~-mUW}RxtoMcCwhiff+hbhf-5c$p+FtR+ z+J53>iz*)Q!`;Uz@-dkXfZc2C+*Cp@*N`sN;GlT5w|O5vEz&xKzx6q|06s_1WnPu0 zzX1=lRbr~qTr<_pkUK^;-2=z#!Blw#PA0$*rgSY3tKoPj0JAvX*zKj%ae*ctz&Arc z6$N15d9~QdD|N_D;K2P~RuC4G@L`ByWD7hNN$Kh?f|-+`&a#a$jSRJwdPQH=SVPBG z&VbzMbHvDdNqpGp;o6b1_m!f{cA3vZoIU$?XcvkONb(V84bLv_=zhzbzm#==R#RTk zipe8?F+;b!GY|+5`xb=-7*ZTeg<&ez+;8o5=gt+M95|5tdRZE zEX-e$>T9V+GZ|YO51#d_$c=dpDIBV$nqv#xA^{fJ<&HL0zORnyKy^dz@;v!iA@$7$A~+MJZvs((<|+q zCk8zdCwC336`EM~$F!pk^vtnn5$8hV2d9z3rMJaRO^jQG#dkw0-w}j-%vTq5-I+a+ z&#BM)x=rePyA|_!w3VBA$4f%&mwE>bXz{1K3_om?GYCI7t)pv%5B%r>+<`jWOkaPX z7I~gpxmv`uW#L2-=VqOSjgGg9tC-J)E6FJ-=}{tPv_RuVW7S3f(P!zA=@fSL)nj;r zhc+{dr(=>sLQPGMg4`8xv+fp`B+B$utQz8^>B)BXXv_cc!!f*_5J1?8!La5xLd}#P5O`*|TtVPk<_KADPwWe1z$ZgDVMe?3;tzH=JE=*85cp1%gYF zYh%kt{6|sVN}{&Rt(=oGvu{LG8PSaCGvj$0&EOJ>Oq2~vmgd3v_N~PZL>&lV1rycc zvHNKj&M*{M*CuTh>xG6ss ScC>O20Uyt!9`)|wS^ol+60?E; literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/livekit_ic_launcher_round.png b/example/android/app/src/main/res/mipmap-xxhdpi/livekit_ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..10daf80a0dfcd0b990c4e0e257f192d79e5aced2 GIT binary patch literal 2884 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 VI#l{=G>rhTm>F9V80TH@{~wndHQxXL literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/livekit_ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/livekit_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..04dbc25edabf8145431850c3937338905a12ced2 GIT binary patch literal 2184 zcmd^<`9IrP7sqdsk`Te56|G6fI@(I?RF|p2lr%+&rz2>ej9L;+Z9#?5w6;-Qw3I%K zeXFeoQ4h7XwX{O4r5MbsArVDOrK+g;ntx(`nEQI2d(J)gob$`)yxvLY9PKc&`(*(D z7^1zc3*_5(4J8fTE`KnPkdq0rr!WCPEAJW%Wd4Q)0CkLL`=e{b#L^Tm(A5Xqy;28P zeVz**?JY>dUcjm<1z&31|D9FQT)`u85Iq?cFW=b_U+Ze2YfU%1o|n{=7Dr0lgYb_t z(|^P~R;udDX+Jw@Xyb>=&2X&o_mJgsnK|#Xwn8T6wA=bw1}FU%SjvXwUP+~<$>m

p!8mfV#Gwbx}ftk`MGK*H`>p$K=8*~!)N0aksfu1EOEH=oLIGwIr zSyy?{d&qUG<3Wrvk)xY2YgbH@CH|-0Cz?%vZL7Y-Ml_19DGYWdzPA8|z z+ibuT-2tJ&tfKqr)+8ycN&zr|r)=g0LA`KC`)ntYBLiaT?B3KIst1;*08D_bMOWC} zI$=Ko0G*s>O*hH!yc}bxpyQQuNX{WZ_@B|v;NrC9waY<=dZCOZAgC6Of9BSJql%wa z5$)O_BLVr~+LIDUUljez>1am5XU1JfYYl#lbIyX?`!WRp&Yf-J!o_FvM8x zuZ|qz`b5^)ELgWk#GNtqE#vU*GZ{A zZ&C?0ag}55q~jG;U@CHbAXckdwsk;_+Q0{$J2?S_X za?$bQ13rkVMpH|dFBxkvJ8SPCZ*EZ|u+~dh4al3Vt*3E*I#}_tTs>YTg9fTpB*)No|nFEGCZTE~c3IP53N{<6cFeHR&}ER~NX= zwOU%d)7x*+ z-uwnr5;d>dmq%a)3^=^rc5aX#9YS))ht+V(|7_P~FhtN1UnjSHtvH6aaAy z!Di{jQr0Ckbop>U-8lf7t3izHxcy$w*=Uc%PiiN+Uhiw1759LD!w0f2f_G^$m-(QT;QOQ`01Lb{dA zla^H_?m~yz0l34kEooEi4C3TCQH{mF^oq6wW(R2Z?mQ48LIFw!tWzhQ`}6~A6P7VK zV?NGg`bnET93|v{07uXHFvqvyqfAV*cooCnmwa7C0UcCccT3m&^z4@zo<8}C%DN2N zOcD^Tch@`Sb#C>(wge++Z*HYj3KN9on?r}MW>nvjk}OXi(DjervCtfhrEJJ+80~Hr N@h3;y$JW#ve*rk9`au8y literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/livekit_ic_launcher_round.png b/example/android/app/src/main/res/mipmap-xxxhdpi/livekit_ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b03110e1537289e015c98ae3f9b38e98346d02b4 GIT binary patch literal 3826 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 Bs1pDH literal 0 HcmV?d00001 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 416537a..04b3dde 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,17 +3,21 @@ PODS: - flutter_webrtc (0.2.2): - Flutter - Libyuv (= 1703) - - WebRTC-SDK (= 92.4515.05) + - WebRTC-SDK (= 92.4515.07) - Libyuv (1703) + - livekit_client (0.0.1): + - Flutter + - WebRTC-SDK (= 92.4515.07) - path_provider (0.0.1): - Flutter - shared_preferences (0.0.1): - Flutter - - WebRTC-SDK (92.4515.05) + - WebRTC-SDK (92.4515.07) DEPENDENCIES: - Flutter (from `Flutter`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) + - livekit_client (from `.symlinks/plugins/livekit_client/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) @@ -27,6 +31,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_webrtc: :path: ".symlinks/plugins/flutter_webrtc/ios" + livekit_client: + :path: ".symlinks/plugins/livekit_client/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" shared_preferences: @@ -34,11 +40,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - flutter_webrtc: c0cb88c7cbd057e6e667ab1560e10c74e2ceb65b + flutter_webrtc: 6b71a1fc1d931a3dfe879e578ea10e2e6cd84f53 Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437 + livekit_client: 9216ac9adba8d54cefc870bbcb657ec1da534309 path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de shared_preferences: 5033afbb22d372e15aff8ff766df9021b845f273 - WebRTC-SDK: 7c76a541dbbffb0fc212aeb9902ec45a43c23996 + WebRTC-SDK: 540a44f2e30e89c75e331d5c4a372253ba668233 PODFILE CHECKSUM: 82aed1035f46bfa5b522f0d0dbf4730f17ec65ff diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 3a41195..cf77f1e 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -155,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -321,6 +321,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -352,7 +353,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +397,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -452,6 +454,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -485,7 +488,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -511,7 +514,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140c..c87d15a 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - #SAY~}fk$L90|U1(2s1Lwnj^u$ zATQ_X;uunK>+MZLzJm-R#~;R>aE)0gZV<^MWWK?osenOy_PV>O-;x4Dc@rma>~-7w ze*Sxb_tU2@xyjj~sUIJ2%)RsObIVF`2cJ%94wiO3`_^kurMevFfb;hF);`* zG%y$#Gcs@hWoFC-xk-WHj1N!|P$o?cs0b)yEDTh{fI~Zp%1N+|495^*I^zR2CW4+K z!W_&nAv*{O*+$(6gV`nkObDbzG)x~6>4Zixr$GJG3-J`w0ZA5G1u{{Yh=j0(W?@>z zTmf4n&a6(lE0GEsk_#4MZKF;xhHe`NL;3`AibHgBuoRCJ6t;LwXL7((NaN8*xQu6M zFdkZU2O}`hsaV4y1s*3ux&Up8c?W9O%Me#H9pD`;*}w%FLjm(>$p)&JIe-m6!kM0& zb|LOMX+brkLDc<*8Dht);zAu9X1iZDV-N_wG(Wy!#|Fwapx zrGX2^(UJ{5Ji>H9bx5_NNJ^L_rE4k-8DVu?1GKa!$uWdWfYHtid~|Q*y zsTEl4f`>;zDHdE%kR=38pwux6RKWn<0P3Jp;v%RqP~U-DUsQGM>#Rm$B+g`@{}k^{qzMoL}OfRZB`7z#KWQA8+?ZOE`0#l}FhEqGvr{9FqU zIz%*3sXTz2Ozj8(H4_o8go@c|Eb`jUFNkID(AfvSZJuyg`X&S6W`FXqWl-e>+t?-s*T1|aZs^>bP0 Hl+XkK>n7L@ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..7433fbc428876e75917718c189b3d41430ce2eb0 GIT binary patch literal 539 zcmV+$0_6RPP)0eZK>j0 z{RUQ4h@$b02p-iWDwSwG+7?Ay@lu?bOg7scg9~f6vaY+G>B{Z~Jml)|gFXG9mthvX zJ|MRCL(;sTo%&fYP17(;>sa3noxazuMm03-0-kF)Lu~B_#QP=t`J9LE9}L~#?pYaw zhd*lYlZMA!z?-kc)^S9HYz z>Z!_|sOp*eH>usM>C(DR6E2{BfjSoHs~BzB7J~%#Lez`6AVO~;{9Y{bamE7pWso>*lC0DTWE^ zhqxbctV$0=$G1GJ8K&+68ds=em6r(YiUWTjTRSRn0|E-=;OX5{oohy9$EaLAB-c(B z0(*IXTJ-O5fgQ0mElzx+vCG2DMPb$jyy#XOJxWEFQZ*>oFGy^Dw*R9TZQ2xCGT6JH zbpw?l#nH#}Y=p)Y$rGoZrAKTRe@f^)A{~b%KxI&Ign0j#!?I^tvhL5q-j6RSu=nf! di1r;P)rZEw( z>Om4O9$02!8H8J6MyHoVaeA9=0UhdMe5 z-*)Qv9W*Hd)H02CqfD=@lXu3MX1whkW}Vty2i_3@y(>n%z7^x@3*+H8zWl>}ZrO`= zqy@`*_4DAf*W7ZO6%mk_jAdAc zW%#d!=eSTJ&xby+afY%GXHmFugoE$e^j`Y|;(Eb#5)UMwaiK(!4_DZPiFAr}7H)#! z5dsS~KfN#nGrsi17V4U^udf=JFHEa2tr{*}WrJJv!S>!)`%i`cu37@%yw(&tce8on z(LrL)Qf4eAAL;V6b!Ev+t)tW$PQ4ZYoudt*j!dI#YQJZJCyP8eA1OEuy)VtdO)^lT z17!iwInog7NH;D%-VZjrb=IN}e{g}OmQ8ho^uMLT9~&8#VHuX;e=mFtnct=_tDYHC P00000NkvXXu0mjf3w8d% literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..19f67df8e371365466782a8a26322f30039e5fdb GIT binary patch literal 568 zcmV-80>}M{P)PbXFR7l6|)J;zlK^VaC`8*<5;qHMJkU(nM5-Y}pQXp!Z)x-n} zegjN2QPCDt0tp_`wkF~WaBMVU)aV7eoo+WHPa8KGyG^#+U8apMEc;+I;kc9EX_NhD zvh&Q63vU#+i<`>u2EF~UYTI_pvTWyS+3hE5CP+pxX_RI##Or$9f9Z~F(1lL7X<4my z>yKuq`^1DBMseOKB{3wjrMQi4D5`w{Tt^TCdC% z;H%MiU)MkA4m-O3NpED0?-_G{)r1uc@u#YYbhYn(E%21Zl;U_%j>--(S&r=Oz57w` z>>XdEe+k?M9z)+EgtF|qO5-J(pbiqHld}A_-tIP?pZ>RFyS_yTW?4d|S4;F7br9+3 zukL^E*Xy28Hw}m-@I8RwGZwGVD{?!1n3xbTe1*=O8J4&>2r`fXx4~ubr6Ba2N#AH( zp^1uvRH3o5ywmvWetTy%L{d9*Q#-p*8-2loITp%0g!3$rWw#1&?@LYG^}85yI?53f z{M2PG(*5FsJOtmPqr|NlURTe*rh%7b6zQJgh-uyxp{f4Od5M1bIz zOV0kg?5i@5u64@%I%8&zkf)mTMoq4bH%rX+ez|aXkI1SM-1n3DSrqs}8gIUT{||DZk!!ZF~Cn zH#fNB@7_IqY})M`c?`Eo?v~%z&stbNMPpLb(+Agn95#RVz`gj+60TIP$+>ejD*8qj z$`~deaR2#O@ud>`_H)6k|NdM!6`9lZa+O}R-}U1fx&?CbJ<}wjir#%#xHKxK>E+~I zIZktyn#dRb|6YRR;P(dQz0KO6Lgw+;Zwi_08&EsxPpsi);;pU*n> zXJ+xEIQfsSrED&(`@xwd{nc7*@4Hg<*at5|9Uh6;-r6>Q+ADv)NiMgJ|6$ph`nOMY zef8m=wNsu7ufF$KHhRs!nMITG+Gnqn;QW`*qW}$ANFaZo{hOO<1r;P)rZEw( z>Om4O9$02!8H8J6MyHoVaeA9=0UhdMe5 z-*)Qv9W*Hd)H02CqfD=@lXu3MX1whkW}Vty2i_3@y(>n%z7^x@3*+H8zWl>}ZrO`= zqy@`*_4DAf*W7ZO6%mk_jAdAc zW%#d!=eSTJ&xby+afY%GXHmFugoE$e^j`Y|;(Eb#5)UMwaiK(!4_DZPiFAr}7H)#! z5dsS~KfN#nGrsi17V4U^udf=JFHEa2tr{*}WrJJv!S>!)`%i`cu37@%yw(&tce8on z(LrL)Qf4eAAL;V6b!Ev+t)tW$PQ4ZYoudt*j!dI#YQJZJCyP8eA1OEuy)VtdO)^lT z17!iwInog7NH;D%-VZjrb=IN}e{g}OmQ8ho^uMLT9~&8#VHuX;e=mFtnct=_tDYHC P00000NkvXXu0mjf3w8d% literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..56efb4fcc77cd73d2f6e27aae55fc3cdb0d771c2 GIT binary patch literal 668 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51SA=YQ-1*|&H|6fVg?3oVGw3ym^DX&fq}`- z)5S5Q;?~=nzS+WoBF8@dvd{qWzyw%wMO*2cD~ zyiY&6^X&BgD|y>*zbn}_&3|`x-IJ{g-Dd5~=(bhzVvZ+Q0`e+cY$2On+5!8l_G4O z$)1kEUN`1S6{@3q)l|LX1g z`JR*7-e;Mxo>A=AxuPDX-E7{opk>Ax5!>qJ&$wn~6!K&77sRh0TATheHC8RC+j%nm R4KTSec)I$ztaD0e0sto4EKmRd literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..52545ede34c9362870a7893038395c6c067dc368 GIT binary patch literal 619 zcmeAS@N?(olHy`uVBq!ia0vp^J|N7&1SCDa9;*RToCO|{#S9GG!XV7ZFl&wk0|S$+ zr;B4q#jUqD&t?ldN*w?Avwlr|%c2{(2ew}eHs{ElWzs%FNbunF)m&8@#ItYBiaL0! z$+lvOl(22b(M5tf5&S35?byA^dbX~K;;CyXcU><0K3A;q)VyZq=emkCJ?or3Nvo!> znU!vSBzw1P+r#&9D>m(Fu3dh6r;x;T=DJ=bHkAnuOq`g6oY|9|yn%8G&2j&olegPD z)V(cR$9C*&!~F#vs`qESiml1+o96nI{ie$PuU0!3)LCYTXe2E@FZ=b_wYis9pL?5| zUwb}2^Vj*BrSr`GidXNBzI%;*(TxvHDj9+EW_|urer?K457VA0g8R25zWDlU&109( zA=bkG#ib)BvDo&lh|9XHcf;oV(VmWY#}1w6{IT2XKRGXb{a4>CXcCLAXYRe(cebSG z%zON3Qb+ugGX-|$4wrm|JQNyP(1=eHl;u5}yn?i=gx|m2bF{dvW6s9Y#S_hbFfZ@F z^Zx7qN2`ttou0HnUGS=RZY1mVjyV@?lEXJtUds}hy=1aY<$d*xYxaft;q@<5_RpB< zAGS#=Q zYFA-vtGh}Xx20xf$?t!yKJD)Q@DSD9zpF literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc2562981f0d6a5805a46a7715f38cd3ebc80c5 GIT binary patch literal 881 zcmeAS@N?(olHy`uVBq!ia0vp^GeDSw2}n*~u)PdOaTa()7Bet#3xhBt!>lL5m*z4pZD99tw5@2^-EJ1iU7;$` z-0QJ5;(u#}!9^Bj!)0?{=IlN#a>D3f>g}bkb81=H`1T)ue)2o3&%E=0=FPn9V^pEd zfAvH8&x844`v10FeEoIX_UgiGm*f6ttt)*$|CNq^{<8P%+Kik+DjpLQ9Gh5Zz<9FR zS?}3GseM!BVk1q&{AMZnE|NE&nszU;6E?-TpUa-{%e6 zuI|}W`aWpGr0dzPYwUEtofXNC&-Z!q@`jhyG`CC}-}{kqKR4?9zAX7_&5cOUt%VzB zX@5H_#vE_9{MF5rS-xq3Rx?vYpPyIwum9v^RhiORzm&jLI{l5b2n=vA+tr_#a6emi z$GtgcHb2(B`O(DdYOLw5N&mjA;J>+6Wqb8PuX{UUj=b`kU;M)DbBXcnRq4UmfmU0m z`ChqvyT4p*lX$*T{ND|ttNWv>&uzW;=gGr~M~|I2d+dbM0=9fJyMLvN%f25xGoL@L zdFq~uEj?E=ejeKW^mKak`m0@gjgQ{D>2X%1eP4w2huo?J#qO|8)q8rby8OJ9@zcw6 z%6!q^&%6FkS27LfslRIR$+@m#rQF(->k*QxKmLn(H#0Bv{OSMqYMDZ{*`8fzpB}dO l@7HBqv`aZalh5DTukz2;#d=FvB`^;$c)I$ztaD0e0ss+=ZgT(t literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/ipad-83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..25f241d9d83e46c197df82f47973031f8f90c1ce GIT binary patch literal 954 zcmeAS@N?(olHy`uVBq!ia0vp^%R!if2}u4fxxE`maTa()7Bet#3xhBt!>l!J`-6@{iGNWR8vsY-;!-h8H zcCl0YA8k~M3-E87D<8bOH0(s$E|=>|UvKNxZ#Z)J@{`Y}zq9&$nwkD==ig1Y<|-BS zE`9W%x8&Wy`OOC(+&rf6=4Ra%?Z4kb-ajnS|F2LcS^DbA%HZXGbFJvbsQ3GN+x3Z5 zc~^7y%gef7>&@8CvpK&0zx-d_`-kp(Yplv5|KI;{# zgimC2Mz8qUjcIn%*xyFX+kZDlZ~c1yZFWBn`ftl$$8*>G>51^v$G2`>@tSqwathb2 zl~ay=uGx9}?j3u!!tecWH$60H&08n)bJLpm=-Dxz>Yq!DSaX$@F0TA{c-k+&8T1M- zNZkCN|I2U3+vWQ+@HAH)oUHc=}tsj&phD<$oU@-Fg`| zOXt$csSAA9E_v))_x>f{%&+0+N}S5KeZExk{Hy-FJ(a7k?zoqwb8Kzh-mfSBemk?i z-~RX0kIy%rj;mSDb$8;c*6pjN+rQcKtF-$4&u{l;?&zJ9FTd;8@5b3b8>KZXZob>7 zRdRe)?EA3lb7gV6L;B85TU=at+ur()ec0=A`K!~r9;EK>4_hd`x=eQEw>d9){(roE zI`)Q}{*5}maF75ze|`U95yjo6%mOvQ%*f#B>gTe~DWM4f>(B%< literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e35c1fcc4c2e26d8ccb1bf4d561a6b97f070386e GIT binary patch literal 577 zcmV-H0>1r;P)rZEw( z>Om4O9$02!8H8J6MyHoVaeA9=0UhdMe5 z-*)Qv9W*Hd)H02CqfD=@lXu3MX1whkW}Vty2i_3@y(>n%z7^x@3*+H8zWl>}ZrO`= zqy@`*_4DAf*W7ZO6%mk_jAdAc zW%#d!=eSTJ&xby+afY%GXHmFugoE$e^j`Y|;(Eb#5)UMwaiK(!4_DZPiFAr}7H)#! z5dsS~KfN#nGrsi17V4U^udf=JFHEa2tr{*}WrJJv!S>!)`%i`cu37@%yw(&tce8on z(LrL)Qf4eAAL;V6b!Ev+t)tW$PQ4ZYoudt*j!dI#YQJZJCyP8eA1OEuy)VtdO)^lT z17!iwInog7NH;D%-VZjrb=IN}e{g}OmQ8ho^uMLT9~&8#VHuX;e=mFtnct=_tDYHC P00000NkvXXu0mjf3w8d% literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9d06245833acf42314e360d86d6b704620972f71 GIT binary patch literal 651 zcmV;60(AX}P)7nm-eD^>;ExE)MN-XkJolkDzA~uUADx$dQ2o+58E+M}??|iq&E^Ta?D-^oZ zZ+U5(OcOkX@Dz^QG{z-_W+^?0&?1odh|e1et#6Yu!3M$xj$0YyaM;y<=n+gB(0~Rs zpaBhNKm+>!KreBHl8ZcD=lT{25r!GL<5fmjH&K<)j_ZYF&mSByWz)u1xI)Qgo_@#m zpQKEq5t6utZ$`)H9-~7Y0+I`Dg%YpZsW-PAXTEm-bbO4p|cI9aJh9BpaBhN lKm!`kfCe<60S#z%^j|L{yIe#W=>Y%$002ovPDHLkV1kiDDy;wj literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6b421635fad35f2412dadae9feca80f2b8b091e0 GIT binary patch literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^Rv^s51SIor-mM2xoCO|{#S9GG!XV7ZFl&wk0|Vnn zPZ!6KjC*fyocF%tAada2&*m@v7vgrUPUybWv`%bI-j@Y!OT$*gxw>p{f4Od5M1bIz zOV0kg?5i@5u64@%I%8&zkf)mTMoq4bH%rX+ez|aXkI1SM-1n3DSrqs}8gIUT{||DZk!!ZF~Cn zH#fNB@7_IqY})M`c?`Eo?v~%z&stbNMPpLb(+Agn95#RVz`gj+60TIP$+>ejD*8qj z$`~deaR2#O@ud>`_H)6k|NdM!6`9lZa+O}R-}U1fx&?CbJ<}wjir#%#xHKxK>E+~I zIZktyn#dRb|6YRR;P(dQz0KO6Lgw+;Zwi_08&EsxPpsi);;pU*n> zXJ+xEIQfsSrED&(`@xwd{nc7*@4Hg<*at5|9Uh6;-r6>Q+ADv)NiMgJ|6$ph`nOMY zef8m=wNsu7ufF$KHhRs!nMITG+Gnqn;QW`*qW}$ANFaZo{hOO< zO0Im(y?(!cvWlUaVx)V&q}sF0H+rsr5}(*8^g1$f1vD^;EMQ>OaKOp@(S9iD$mQDm z>b+B~x;A9XmTp~}R}%ZZJVnS@X66Y26B9MFXVWEZyZ3xiFj3m*Bena-vE^?MW$)kc z(*D+4+jo_`*Rv~3bgS=8j_W_(TqpG30+dO8Pn-$n2_ZkJ7; z_j#6pm7SEyBG2EuBkFuFPjm8F|GHH+a;o1|!BEG5U;ntBbUa_mYxVF+a#)MC&MeJ+ zf1;~It8WEIQxHdVhpVRlrAYkVdDx~01^_#h=+Ks8AegRXx zXP@4+_>Nr0a_kGo`{g6HX`>Xix?`jqM zQuA83%-tn9Z`+}j&mFg|+^m0cR+MD_?Wg@&>y)40Iddx>r^pZZ&a?LtqYt^Rkd zxpV8sF3qiXFKyE*?MxPaI$_(H8jDKxGkZCX?OwjOBzo76le%T1Z~vQT5)m!(T=fj4 Y9~UMj&0Tc}n7|l3UHx3vIVCg!0AMo|ZvX%Q literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..56efb4fcc77cd73d2f6e27aae55fc3cdb0d771c2 GIT binary patch literal 668 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51SA=YQ-1*|&H|6fVg?3oVGw3ym^DX&fq}`- z)5S5Q;?~=nzS+WoBF8@dvd{qWzyw%wMO*2cD~ zyiY&6^X&BgD|y>*zbn}_&3|`x-IJ{g-Dd5~=(bhzVvZ+Q0`e+cY$2On+5!8l_G4O z$)1kEUN`1S6{@3q)l|LX1g z`JR*7-e;Mxo>A=AxuPDX-E7{opk>Ax5!>qJ&$wn~6!K&77sRh0TATheHC8RC+j%nm R4KTSec)I$ztaD0e0sto4EKmRd literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..23f58754a1d4c9d5742a0d62f1ada9a1f72da21c GIT binary patch literal 771 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1SGeyEo=o+oCO|{#S9GG!XV7ZFl&wk0|V1m zPZ!6Kid%2*`1%VwN*wt3*Y1$xo*%qR99BpymN+=OiLuPGW7_oQPL)?DH#@a5xb?_8 zav#h1!{?R|pLTmmut`Z}@r|2N-jB>bWKQ}2q<;RB>FUPCH%s&Kv`)5}U47tY@@eIs z>N#doy*k+^)?BXTGyZ*K#qVSDO%L|u@Lh0V<~1Rk_`m+bF4nneytgN4ewv*1DT%Ymq*YvMfOKl%de}3v?jL!9*pwCmiYCe5d@czDOi{9+X+F1{G ziI@D(Dt!}eBK9lUQQ>lhK+ms#AtHZ0F59bjJ}Ih-%X(PUZM8q_>oz5QuDt;(P8!VN zXv$o=Z~KDW(@~qYIqr_0DBYT;5psE#6clY^gPuo(kbJ4l|t9IR< zxcbojYf6=Y7IJK@J8FgR?v4;%_uN%?|DwC=PkmO-cVBUGZbp)|4ohK83SaM9>Decq z#mrd4ef{ZGeZRM{k(ZhCS24dXimi*?`)%8+x^)%twO$@5st}Q7m3yvxW;-YT>UOh75Ba_9mKU1{ zu1i*VS@e1@_ttr;H?R8zeqaA$VUW(B`Hhzw*rYNRG?GjF%Ky&2#^Zap?OXmyz*NoP M>FVdQ&MBb@0LmFvg#Z8m literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..23f58754a1d4c9d5742a0d62f1ada9a1f72da21c GIT binary patch literal 771 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1SGeyEo=o+oCO|{#S9GG!XV7ZFl&wk0|V1m zPZ!6Kid%2*`1%VwN*wt3*Y1$xo*%qR99BpymN+=OiLuPGW7_oQPL)?DH#@a5xb?_8 zav#h1!{?R|pLTmmut`Z}@r|2N-jB>bWKQ}2q<;RB>FUPCH%s&Kv`)5}U47tY@@eIs z>N#doy*k+^)?BXTGyZ*K#qVSDO%L|u@Lh0V<~1Rk_`m+bF4nneytgN4ewv*1DT%Ymq*YvMfOKl%de}3v?jL!9*pwCmiYCe5d@czDOi{9+X+F1{G ziI@D(Dt!}eBK9lUQQ>lhK+ms#AtHZ0F59bjJ}Ih-%X(PUZM8q_>oz5QuDt;(P8!VN zXv$o=Z~KDW(@~qYIqr_0DBYT;5psE#6clY^gPuo(kbJ4l|t9IR< zxcbojYf6=Y7IJK@J8FgR?v4;%_uN%?|DwC=PkmO-cVBUGZbp)|4ohK83SaM9>Decq z#mrd4ef{ZGeZRM{k(ZhCS24dXimi*?`)%8+x^)%twO$@5st}Q7m3yvxW;-YT>UOh75Ba_9mKU1{ zu1i*VS@e1@_ttr;H?R8zeqaA$VUW(B`Hhzw*rYNRG?GjF%Ky&2#^Zap?OXmyz*NoP M>FVdQ&MBb@0LmFvg#Z8m literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon-LiveKit.appiconset/iphone-60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..51bb77d2b7b961bb6c1c4c8bcac5d9160e69cc4f GIT binary patch literal 955 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD2}o{QKQR?ZaTa()7Bet#3xhBt!>lsy%6Y}? zVyf4C%WW&?{qlP=OZ@U=IObe2KWo?g<-q$7|5@7Q&li_14_Yg}QEHudy7o+ay4A*Q`sY2FMT0G{{ujv1_HKMN-&cM9vRm7e z7XAEgf8VWVoyn(Tx6Jy!o-I=Ln|k>Re|J*U}Zk=xLp4?Dz osjF3j8Ws;4nE3`3QMTQl(RKZERoR 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8CFBundleInfoDictionaryVersion 6.0 CFBundleName - livekit_example + LiveKit CFBundlePackageType APPL CFBundleShortVersionString diff --git a/example/lib/exts.dart b/example/lib/exts.dart index 45a362f..5068044 100644 --- a/example/lib/exts.dart +++ b/example/lib/exts.dart @@ -20,6 +20,24 @@ extension LKExampleExt on BuildContext { ), ); + Future showUnPublishDialog() => showDialog( + context: this, + builder: (ctx) => AlertDialog( + title: const Text('UnPublish'), + content: const Text('Would you like to un-publish your Camera & Mic ?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('NO'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('YES'), + ), + ], + ), + ); + Future showErrorDialog(dynamic exception) => showDialog( context: this, builder: (ctx) => AlertDialog( diff --git a/example/lib/main.dart b/example/lib/main.dart index 54ba4ef..e5816b6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_example/theme.dart'; import 'package:logging/logging.dart'; import 'pages/connect.dart'; void main() { - print('This is a test for ${SignalTrickleEvent} test.'); // configure logs for debugging Logger.root.level = Level.FINE; Logger.root.onRecord.listen((record) { - print('${record.level.name}: ${record.time}: ${record.message}'); + print('${record.level.name}: ${record.message}'); }); WidgetsFlutterBinding.ensureInitialized(); diff --git a/example/lib/pages/room.dart b/example/lib/pages/room.dart index b1616e8..53b4181 100644 --- a/example/lib/pages/room.dart +++ b/example/lib/pages/room.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math' as math; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:livekit_client/livekit_client.dart'; @@ -25,7 +26,7 @@ class RoomPage extends StatefulWidget { class _RoomPageState extends State { // List participants = []; - late final _listener = EventsListener(widget.room.events); + late final EventsListener _listener = widget.room.createListener(); @override void initState() { @@ -48,7 +49,9 @@ class _RoomPageState extends State { } void _setUpListeners() => _listener - ..on((_) => Navigator.pop(context)) + ..on((_) async { + WidgetsBinding.instance?.addPostFrameCallback((timeStamp) => Navigator.pop(context)); + }) ..on((event) { String decoded = 'Failed to decode'; try { diff --git a/example/lib/widgets/controls.dart b/example/lib/widgets/controls.dart index 0ba975d..3be30ca 100644 --- a/example/lib/widgets/controls.dart +++ b/example/lib/widgets/controls.dart @@ -1,9 +1,10 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:eva_icons_flutter/eva_icons_flutter.dart'; import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; -import 'package:collection/collection.dart'; + import '../exts.dart'; class ControlsWidget extends StatefulWidget { @@ -44,6 +45,15 @@ class _ControlsWidgetState extends State { setState(() {}); } + void _unpublishAll() async { + final result = await context.showUnPublishDialog(); + if (result == true) { + await participant.unpublishAllTracks(); + // Force to update UI for now + participant.notifyListeners(); + } + } + void _muteAudio() { if (participant.hasAudio) { final audioPub = participant.audioTracks.first; @@ -146,6 +156,10 @@ class _ControlsWidgetState extends State { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + IconButton( + onPressed: _unpublishAll, + icon: const Icon(EvaIcons.closeCircleOutline), + ), if (canMute) IconButton( onPressed: _muteAudio, diff --git a/example/pubspec.lock b/example/pubspec.lock index 68cbe27..c04b216 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -111,11 +111,11 @@ packages: dependency: transitive description: path: "." - ref: use-custom-webrtc-build - resolved-ref: "4942e7faec2e5775d35c42e22c2929ca6ca53769" - url: "https://github.com/livekit/flutter-webrtc" + ref: master + resolved-ref: "32d83d85fa2faebcd37a19534f8a84273b12cbce" + url: "https://github.com/flutter-webrtc/flutter-webrtc" source: git - version: "0.6.7" + version: "0.6.8" google_fonts: dependency: "direct main" description: diff --git a/example/web/favicon.png b/example/web/favicon.png index 8aaa46ac1ae21512746f852a42ba87e4165dfdd1..7437724ee3f3f834ac98691849cb462e12f2c275 100644 GIT binary patch literal 2968 zcmaKu3se)=7CK7;Ovp%*3CSSDQb15X zTI6FbmLj6{b?Fl=Sc)x6S_CVFitQ8XD`=%6AS{Hgf^Ef5-kV7RL{{iz&D@)F&pzj# zbMCz}h2h&m>};KF0RXTQh6*BxmPy`=tcmANi>Z77plDG(KU~P?)56u56qO?Y;8yxx ziO55+gW8rQEuhBM*4BQ>s>!4J)a~#(9=@Wh`kG=qZu0EV;Qq(9Jz}?af(nbSE;yZC zbofe4SZUcQFhG88lq2=do#@t7^=WTwbf+d4mT%wKi6a{&R~j7OvPF+acJ2DZ;fttW z&pH+>2Rakhu#enGd*y^(WqYJ_U8vVp{m&&;^ZPZ5qfd)v9@^8=+M+nxa6vrc6FcGd zF#6ke3+(M+aBE~u{tk1>cW8sG>>zWdl_kEJtUzoK1 z=DCKjhLZIi_N%;uscW`oyFN$kek?rA=z-hH<*A%XukuS%l8(DM{en1`cISAgvu~{2 zX>b(#Camc`^t37G-A}rYDq4HE-hJf<9{9<`xm{Z#uRi_k!*}kS2yLEZb#LGN$fx`E zfJ2k6rI@$s`~keOeE<0ehl=gmwEn8fV*XbfT;nB+T^+yJ)R_CbBef~#RQ$00SYFTe zP9Vu5?xI9U)Z!!}7D`1Tz@6wX0<0;^0864rA$|bG8Can213(PLWv0KI;%3ybAavLO z@dTC=w=^FGtc>~xh?cxUiJP!wXxU6Qi_2ninJgNU&1HFWz1aX@QEf3pF{>?S`y{SA z&w7(+Y*eAK8UR@AK;9IfqLK;#))&wyF)kKu;-4Q~ zhT|$O2&Sc_G19ykm^uk$aX1{1$p+bMPeQ{}qgCQ?x~EdJ+F)ebjsVdls!1gIM%o9mRXFVx3#;}M0L2?9SF_@q^3`eE&VC2ZW7?C59XeO}N+@-@R zJ*H5Yb*RCE_7fo(Q(~6XL}^tB7=dW8RCOW}v>#F8tIf@wn8e>3G6$(-7>;RV*gWXW zupxAPNe!9ik$L3uF*&A=RKbY|#Nz2wYbu(pi$OaaO-JNn0jfZh8lr{}i^DcTb4SdQ zvGbB9BPJwSGOkboCm}mg2`+{89`S|Hb^qx$0P3+8UbuRHCVf>DOG!Y?zG+_|mm&Nh*^<*=Mb!t}6tR4N~ zKm>+BL@8&nS+S8aq|4-R*JslpC0-qnEfN0oYY4Kf7JO7syzJyc!R9F0f-&(HbvM&q z(Npz!J0!U7b6GHhE>$9)jn_1 z^$d>N{H7c~OAT2t75|Sxb>#Oy-CNw_d!l3wRr_aMdGT0NM)|gwfRF7CLcJ4viu;@I zjCeS?IjMrH?+4xn8XvB;9JlA%gjrWo@KzU)zbm`*?cu1Z!>2nIE?3_T3?I9#$?jcO z^i%!GCI~%9XfZ}-%6H#q~7_mWH9K{Qt>{9lh}RY-&Z98tLwVs*tJmu(O7%7;2M7_Otj~s zvukVRmh0|+rj)Gx#a}m(5yV+}@Nnf`Jd^S2zaBMbes)k%&#YebIJlALrgJJU*jJBP z?i1G2Z{0W=RZ+DgVbh~!?-rbH+x_FVCCSMK04V>h8kpEq+5IKI#nAs+XdnHvz5lx53svghT?;WA3rlJ}OZwfy?o)PKGKhIf?} U4oLUDB0q_QLE8kE0~2!o58|N=iU0rr delta 601 zcmV-f0;c_#7nKJ`iBL{Q4GJ0x0000DNk~Le0000G0000G2nGNE03Y-JVE_OC0drDE zLIAGL9O(c6010qNS#tmYE+YT{E+YYWr9XB600CK(V**eEH7+!hwgP~YZ385e$OSl) z;s`E(Ou{Ew0005iNklYp&slpFPnL0%RHV8+M(6~Ud5K^Sra$zh&ame1i&CH$GUY`?hal1Ra@4cC~ zb3)U=h+&@!F?`V*4)r4pPJ)uL{}O}*c1ZDmqM`8fHU#gd(!lo96g)ck9kT4l=ZPPRg%Wd}Pw1cWy9_S@3Mq z{%(O6Q^3J;4DN(+SvIDZfiZ`{d!#%`%JT$%m-F$+w~MynWHOgvz@^_3udZbTo+W#K z->>?Rq*`ezq+J0?#pa$k@M&$vKnZNdnF2WN4|yMLTEG0!#@3ZYzS417e33OD5 zlsk|#26p^>Ofp8Da+vkeY)k5-Q+)b#_!+G~K?J5AFnaMEo0#}^JLTKuFO7va8s1wy zw{xg#jaGCqv^r_X1EysSej0~Wtxy3}bVH~F} nj)={jkpu={NJR42qzium05Tpns9Bec00000NkvXXu0mjfks||j diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png index b749bfef07473333cf1dd31e9eed89862a5d52aa..ba48301fe2125e7f8b898f1d9ef4295b9771619f 100644 GIT binary patch literal 4716 zcmeHKe>7C-8^1GD(kgBFQ8qiNv;C0yfygulAyK0AgT-c;J7boaJ7xwmCS%g3e5Kj; zmn~`9QgkX}W0s7uDYiD*mPCF`N+gCA4I0Du&Jc36XV3SX?|*ye-21-sJkRHO-uLso z&->n+?7P#$K!3SD000J_+uU};ZJFlQn+rd8H$~0^00fWb>gwz1>WcDZK@?gf2motO z#((W?>9)(XGJ$f;G~oL6>%ZWxCYf%`+hy~uuW`+}5_(Ti|M~V8F&+91{%Zr>lT#&g z#KP1g`TM+1ev^T5j?Cv~T@tK7X^M zV9(R@Mj}RARmf`V|J;sMFM}l3mQgjAZ;YNDGmB>Tl^~2Z-qJ~;HQXw{MNcWb?BBUD z;Pu+py?5>+p>-#WhT0-1gyqIWle**I5!Z~Mx@~svn;YaMH?vIl{2}TyX~Y9qoLa)NF*W5E)X}i^Q^$wx=ThQQ-WHGvdRGAhT?Qq5>?baR0+K+x;X1xjzK{`Te%Ceq61O%kz!3 z589SUM{kJ;E3@M`XVPM_TfPz*RPvmdXGE?=8_a{r3(SoQHWeT4GcuJXW(2=9R3tU{ zQ~^|E&=s;LoQoepe@}|HH?SV=>j85S9|JmY4*~xG#ByMcwhsXN5N4D8FA!@dY>=>x zH2^<>rSMHTN&|E!?Bn28b8Ulf*wc7xjkU(%aacSShr(Lpakh9{YXCr=Lr$W~&go3` zHSq9E@g(9%X6#QqQS`|X(DOp(?*#tn`!SqH3XvlyJ1Zi&IAO^ z2AD3K2GFiy{5Be!1F?A15)G$YTf}}~)cewRib$Ha7?~s%8`LaSM+;3<&03wT-pS(B zV6;mf&xRdAoVXd&ZJ?}PEORNJPi+$%tYcObUnnNtCy4Cb z9$QeJ?yhz?2Rl0}Yc#xCXSJKvk9L!Qrw0<@gigVx#1p7-(xz?lT29BM@DW1PhZ%f# z91-=&8+KfT54265ZZ3Pc2*oMyZ=P2yKC)mn#rEMsUh9#zR8eaUx9dW^$sb2JdciI+ zUkOT>$EgF4SARD@zy4GRJO9yzBR5+rkC~JA8tWgFm+KT??1T;oq_%n0Ed!;S(FzQt zREQJQCC$<&8_f!vGZD88VlO*ws8rrj%Ni4&{1hK*<6$uD`2wA4>g;?RKp5J5QgjMH z{6;WMUW@`llUCU;K>?@ct->L7o$oW-C`esYf5_^sLg3i46HPmW0NMQbZ9PK(SAH^m zzM=5`;ghBaB#N?$WrIL|&BK1Y${D!*kq6^5XTW;V)=*tN;g7AqRq5(ExBt5B-(WLo z44)UWm~B?HU-yO%Hn=Zj%W)wGFm3@tC8~m^;W)2|_LVx@15a02wr18Apy|}p>UG&~ zS09W4Mk*Xu-anMgR>!=G=)YH!J@_`YzPgXm`=;MKu!jE#clKtzdtKg&!Hxwzm$)y! zl%zF_AGpZV+YcPSEn2f&F_aHFb~RS>WoEaDn{TS9s#7QY_z9Z|n@bB+7aYgjw>*V} z4O{*aI21bNRF4)(^ZWWf(Y@G65~y+?k2%Gu^G1Fm>{G8<-?#mfh1cQ-kJgnADFgWi z^;WHit`7_5JH5?H$WV-2Rxx)5HS&ENl=0$xrQ~X-ig>A9JmO?7QuwIiRjLkg!OiBJ z6?IO>il4|?7t^FMqpewKR!;-uEeDlA_Xd?bAgULWz&8u(QBvt|Svz0L`SP>S&}P0) ziBckLk;<0aHM%eytjm9nn3TXbzajUKNJP zI@N1YW9uI)n`%^U7o;biKcu-yy^5jX^~;2T*dHImt9^jr5ruLFkRN$D!WC1^UOEr1 zn6h)V|Em69Y_PL^UW6X){8C9eKd5@RIZqHd!cJ8pqpfF}T7vRAqI-SRjJ)K6p496UK7$||yhV$D z#VQ#X`CO2(eT|8vy=Hx5nErxmLEU`J=)$IkzVMaDPS@=jG#DxAkM++=RC-io1+EL~ z%D+@iA1p2yi+k2C{zaJ=e~>pKC?cjtE~@F?)Sl5xE&q*@@o`>5ozU z6(Fk|<%v4O+tZb5X?5>uk)LzUpT^aqf`tArOM;g?9xVP;5)ogb9w<=1RA)Uh7fRLl zBVK1IWnz)y!DkEWj%^+~p)Vdz&n52qT%XdW$B{$xwOSw zGWJ;f<^&e7OtMMIvS`RjdF~%uDyrgP9#j^K_pf;FAbJ$VX#O1vO&{R#7q`jBa*cDT zgoAej;}00~r9GAXc4@9>WP1-7o80j@Q$03sZT+9-=>WpC@j$H+d`uyZDeHCh%IZhY z=fFj+?|h>YKFKPSCed>cNMGQ*qa6Z?EtF+j>KO`=hKfA+1k6*r2fGM?rNVHLw-7k4 zz`W8&>YfAK_>oB6kjR8x`wdaR9MqFA_*4y3r4%Ou@YnOhe?$In#LAGmH;4dcvHeQT PXTGQVPPd#bA&LJ5(3|z_ literal 5292 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 diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png index 88cfd48dff1169879ba46840804b412fe02fefd6..60608776011c321c2a71521c52557addc3f17960 100644 GIT binary patch literal 9587 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 diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ios/Classes/LivekitClientPlugin.h b/ios/Classes/LivekitClientPlugin.h new file mode 100644 index 0000000..1219c0d --- /dev/null +++ b/ios/Classes/LivekitClientPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface LiveKitClientPlugin : NSObject +@end diff --git a/ios/Classes/LivekitClientPlugin.m b/ios/Classes/LivekitClientPlugin.m new file mode 100644 index 0000000..670eb81 --- /dev/null +++ b/ios/Classes/LivekitClientPlugin.m @@ -0,0 +1,15 @@ +#import "LiveKitClientPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "livekit_client-Swift.h" +#endif + +@implementation LiveKitClientPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftLiveKitClientPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/ios/Classes/SwiftLiveKitClientPlugin.swift b/ios/Classes/SwiftLiveKitClientPlugin.swift new file mode 100644 index 0000000..14aa2be --- /dev/null +++ b/ios/Classes/SwiftLiveKitClientPlugin.swift @@ -0,0 +1,114 @@ +import Flutter +import UIKit +import WebRTC + +public class SwiftLiveKitClientPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "livekit_client", binaryMessenger: registrar.messenger()) + let instance = SwiftLiveKitClientPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + // https://developer.apple.com/documentation/avfaudio/avaudiosession/category + let categoryMap: [String: AVAudioSession.Category] = [ + "ambient": .ambient, + "multiRoute": .multiRoute, + "playAndRecord": .playAndRecord, + "playback": .playback, + "record": .record, + "soloAmbient": .soloAmbient, + ] + + // https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions + let categoryOptionsMap: [String: AVAudioSession.CategoryOptions] = [ + "mixWithOthers": .mixWithOthers, + "duckOthers": .duckOthers, + "interruptSpokenAudioAndMixWithOthers": .interruptSpokenAudioAndMixWithOthers, + "allowBluetooth": .allowBluetooth, + "allowBluetoothA2DP": .allowBluetoothA2DP, + "allowAirPlay": .allowAirPlay, + "defaultToSpeaker": .defaultToSpeaker, +// @available(iOS 14.5, *) +// "overrideMutedMicrophoneInterruption": .overrideMutedMicrophoneInterruption, + ] + + // https://developer.apple.com/documentation/avfaudio/avaudiosession/mode + let modeMap: [String: AVAudioSession.Mode] = [ + "default": .default, + "gameChat": .gameChat, + "measurement": .measurement, + "moviePlayback": .moviePlayback, + "spokenAudio": .spokenAudio, + "videoChat": .videoChat, + "videoRecording": .videoRecording, + "voiceChat": .voiceChat, + "voicePrompt": .voicePrompt, + ] + + private func categoryOptions(fromFlutter options: [String]) -> AVAudioSession.CategoryOptions { + var result: AVAudioSession.CategoryOptions = [] + for option in categoryOptionsMap { + if (options.contains(option.key)) { + result.insert(option.value) + } + } + return result + } + + public func handleConfigureNativeAudio(args: [String: Any?], result: @escaping FlutterResult) { + + let configuration = RTCAudioSessionConfiguration.webRTC() + + // Category + if let string = args["appleAudioCategory"] as? String, + let category = categoryMap[string] { + configuration.category = category.rawValue + print("[LiveKit] Configuring category: ", configuration.category) + } + + // CategoryOptions + if let strings = args["appleAudioCategoryOptions"] as? [String] { + configuration.categoryOptions = categoryOptions(fromFlutter: strings) + print("[LiveKit] Configuring categoryOptions: ", strings) + } + + // Mode + if let string = args["appleAudioMode"] as? String, + let mode = modeMap[string] { + configuration.mode = mode.rawValue + print("[LiveKit] Configuring mode: ", configuration.mode) + } + + let session = RTCAudioSession.sharedInstance() + session.lockForConfiguration() + defer { + session.unlockForConfiguration() + } + + do { + try session.setConfiguration(configuration, active: true) + print("[LiveKit] Configure success") + result(true) + } catch let error { + print("[LiveKit] Configure audio error: ", error) + result(FlutterError(code: "configure", message: error.localizedDescription, details: nil)) + } + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + guard let args = call.arguments as? [String: Any?] else { + print("[LiveKit] arguments must be a dictionary") + result(FlutterMethodNotImplemented) + return + } + + switch call.method { + case "configureNativeAudio": + handleConfigureNativeAudio(args: args, result: result) + default: + print("[LiveKit] method not found: ", call.method) + result(FlutterMethodNotImplemented) + } + } +} diff --git a/ios/livekit_client.podspec b/ios/livekit_client.podspec new file mode 100644 index 0000000..ddb698c --- /dev/null +++ b/ios/livekit_client.podspec @@ -0,0 +1,26 @@ +# +# LiveKit +# https://livekit.io/ +# https://github.com/livekit +# + +Pod::Spec.new do |s| + s.name = 'livekit_client' + s.version = '0.0.1' + s.summary = 'Open source platform for real-time audio and video.' + s.description = 'Open source platform for real-time audio and video.' + s.homepage = 'https://livekit.io/' + s.license = { :file => '../LICENSE' } + s.author = { 'LiveKit' => 'contact@livekit.io' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.platform = :ios, '12.1' + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + s.static_framework = true + + s.dependency 'Flutter' + s.dependency 'WebRTC-SDK', '92.4515.07' +end diff --git a/lib/livekit_client.dart b/lib/livekit_client.dart index 7c53d2b..3f51f7b 100644 --- a/lib/livekit_client.dart +++ b/lib/livekit_client.dart @@ -1,7 +1,7 @@ /// Flutter Client SDK to LiveKit. library livekit_client; -export 'src/errors.dart'; +export 'src/exceptions.dart'; export 'src/events.dart'; export 'src/livekit.dart'; export 'src/managers/event.dart'; diff --git a/lib/src/classes/change_notifier.dart b/lib/src/classes/change_notifier.dart deleted file mode 100644 index b8b4f53..0000000 --- a/lib/src/classes/change_notifier.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:livekit_client/src/logger.dart'; - -// dispose safe change notifier -abstract class LKChangeNotifier extends ChangeNotifier { - bool _disposed = false; - bool get isDisposed => _disposed; - - @override - void dispose() { - _disposed = true; - super.dispose(); - } - - @override - void addListener(VoidCallback listener) { - if (_disposed) { - logger.warning('calling addListener on a disposed ChangeNotifier'); - return; - } - super.addListener(listener); - } - - @override - void removeListener(VoidCallback listener) { - if (_disposed) { - logger.warning('calling removeListener on a disposed ChangeNotifier'); - return; - } - super.removeListener(listener); - } -} diff --git a/lib/src/events.dart b/lib/src/events.dart index a6510a4..fb0cb3d 100644 --- a/lib/src/events.dart +++ b/lib/src/events.dart @@ -1,6 +1,4 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; -import 'package:livekit_client/livekit_client.dart'; -import 'package:meta/meta.dart'; import 'participant/participant.dart'; import 'participant/remote_participant.dart'; @@ -8,6 +6,7 @@ import 'proto/livekit_models.pb.dart' as lk_models; import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'track/remote_track_publication.dart'; import 'track/track.dart'; +import 'track/track_publication.dart'; import 'types.dart'; abstract class LiveKitEvent {} @@ -110,14 +109,6 @@ class TrackSubscribedEvent with RoomEvent, ParticipantEvent { }); } -@internal -class ParticipantInfoUpdatedEvent with ParticipantEvent { - final RemoteParticipant participant; - const ParticipantInfoUpdatedEvent({ - required this.participant, - }); -} - /// An error has occured during track subscription. /// Emitted by [Room] and [RemoteParticipant]. class TrackSubscriptionExceptionEvent with RoomEvent, ParticipantEvent { @@ -226,12 +217,12 @@ class EngineReconnectedEvent with EngineEvent { const EngineReconnectedEvent(); } -class EngineParticipantUpdateEvent with EngineEvent { - final List participants; - const EngineParticipantUpdateEvent({ - required this.participants, - }); -} +// class EngineParticipantUpdateEvent with EngineEvent { +// final List participants; +// const EngineParticipantUpdateEvent({ +// required this.participants, +// }); +// } class EngineTrackAddedEvent with EngineEvent { final rtc.MediaStreamTrack track; @@ -244,13 +235,6 @@ class EngineTrackAddedEvent with EngineEvent { }); } -class EngineSpeakersUpdateEvent with EngineEvent { - final List speakers; - const EngineSpeakersUpdateEvent({ - required this.speakers, - }); -} - class EngineDataPacketReceivedEvent with EngineEvent { final lk_models.UserPacket packet; final lk_models.DataPacket_Kind kind; @@ -363,10 +347,11 @@ class SignalTrickleEvent with SignalEvent { }); } -class SignalParticipantUpdateEvent with SignalEvent { - final List updates; +// relayed by Engine +class SignalParticipantUpdateEvent with SignalEvent, EngineEvent { + final List participants; const SignalParticipantUpdateEvent({ - required this.updates, + required this.participants, }); } @@ -379,9 +364,19 @@ class SignalLocalTrackPublishedEvent with SignalEvent { }); } -class SignalActiveSpeakersChangedEvent with SignalEvent { +// speaker update received through websocket +// relayed by Engine +class SignalSpeakersChangedEvent with SignalEvent, EngineEvent { final List speakers; - const SignalActiveSpeakersChangedEvent({ + const SignalSpeakersChangedEvent({ + required this.speakers, + }); +} + +// Event received through data channel +class EngineActiveSpeakersUpdateEvent with EngineEvent { + final List speakers; + const EngineActiveSpeakersUpdateEvent({ required this.speakers, }); } diff --git a/lib/src/errors.dart b/lib/src/exceptions.dart similarity index 99% rename from lib/src/errors.dart rename to lib/src/exceptions.dart index 382ac9c..ac312fb 100644 --- a/lib/src/errors.dart +++ b/lib/src/exceptions.dart @@ -1,6 +1,3 @@ -// -// -// class LiveKitException implements Exception { final String message; const LiveKitException._(this.message); diff --git a/lib/src/managers/delay.dart b/lib/src/managers/delay.dart index 8fe30ce..f5026d8 100644 --- a/lib/src/managers/delay.dart +++ b/lib/src/managers/delay.dart @@ -1,9 +1,14 @@ import 'package:async/async.dart'; -class CancelableDelayManager { +import '../support/disposable.dart'; + +class CancelableDelayManager extends Disposable { // final _delays = >[]; + CancelableDelayManager() { + onDispose(() async => await cancelAll()); + } // delay but cancelable Future waitFor( Duration wait, { @@ -19,7 +24,16 @@ class CancelableDelayManager { if (!op.isCanceled) ifNotCancelled?.call(); } - Future dispose() async { + // @override + // Future dispose() async { + // final didDispose = await super.dispose(); + // if (didDispose) { + // await cancelAll(); + // } + // return didDispose; + // } + + Future cancelAll() async { // cancel all delays if (_delays.isEmpty) return; // make a copy so we don't mutate while iterating diff --git a/lib/src/managers/event.dart b/lib/src/managers/event.dart index 022a715..7bc0c7e 100644 --- a/lib/src/managers/event.dart +++ b/lib/src/managers/event.dart @@ -1,13 +1,19 @@ import 'dart:async'; -import 'package:flutter/material.dart'; import 'package:synchronized/synchronized.dart' as sync; -import '../errors.dart'; +import '../exceptions.dart'; import '../extensions.dart'; import '../logger.dart'; +import '../support/disposable.dart'; import '../types.dart'; +mixin EventsEmittable { + final events = EventsEmitter(); + EventsListener createListener({bool synchronized = false}) => + EventsListener(events, synchronized: synchronized); +} + // Type-safe, multi-listenable, dispose safe event handling // TODO: Move to a separate package @@ -17,7 +23,9 @@ class EventsEmitter extends EventsListenable { EventsEmitter({ bool listenSynchronized = false, - }) : super(synchronized: listenSynchronized); + }) : super(synchronized: listenSynchronized) { + onDispose(() async => await streamCtrl.close()); + } @override EventsEmitter get emitter => this; @@ -28,13 +36,6 @@ class EventsEmitter extends EventsListenable { // emit the event streamCtrl.add(event); } - - @override - @mustCallSuper - Future dispose() async { - await streamCtrl.close(); - await super.dispose(); - } } // for listening only @@ -51,7 +52,7 @@ class EventsListener extends EventsListenable { } // ensures all listeners will close on dispose -abstract class EventsListenable { +abstract class EventsListenable extends Disposable { // the emitter to listen to EventsEmitter get emitter; @@ -62,17 +63,34 @@ abstract class EventsListenable { EventsListenable({ required this.synchronized, - }); - - @mustCallSuper - Future dispose() async { - // Stop listening to all events - logger.fine('${objectId} dispose() cancelling ${_listeners.length} event(s)'); - for (final listener in _listeners) { - await listener.cancel(); - } + }) { + onDispose(() async { + if (_listeners.isNotEmpty) { + // Stop listening to all events + logger.fine('${objectId} cancelling ${_listeners.length} listeners(s)'); + for (final listener in _listeners) { + await listener.cancel(); + } + } + }); } + // @override + // @mustCallSuper + // Future dispose() async { + // // mark as disposed + // final didDispose = await super.dispose(); + // if (didDispose && _listeners.isNotEmpty) { + // // Stop listening to all events + // logger.fine('${objectId} dispose() cancelling ${_listeners.length} event(s)'); + // for (final listener in _listeners) { + // await listener.cancel(); + // } + // } + + // return didDispose; + // } + // listens to all events, guaranteed to be cancelled on dispose CancelListenFunc listen(FutureOr Function(T) onEvent) { // diff --git a/lib/src/participant/local_participant.dart b/lib/src/participant/local_participant.dart index 86896de..0d096ba 100644 --- a/lib/src/participant/local_participant.dart +++ b/lib/src/participant/local_participant.dart @@ -1,8 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; +import 'package:meta/meta.dart'; -import '../errors.dart'; import '../events.dart'; +import '../exceptions.dart'; import '../extensions.dart'; import '../logger.dart'; import '../managers/event.dart'; @@ -19,16 +20,17 @@ import 'participant.dart'; /// Represents the current participant in the room. class LocalParticipant extends Participant { - final RTCEngine _engine; + @internal + final RTCEngine engine; + @internal final TrackPublishOptions? defaultPublishOptions; LocalParticipant({ - required RTCEngine engine, + required this.engine, required lk_models.ParticipantInfo info, this.defaultPublishOptions, required EventsEmitter roomEvents, - }) : _engine = engine, - super( + }) : super( info.sid, info.identity, roomEvents: roomEvents, @@ -36,39 +38,43 @@ class LocalParticipant extends Participant { updateFromInfo(info); } - /// for internal use - /// {@nodoc} - RTCEngine get engine => _engine; - /// publish an audio track to the room Future publishAudioTrack(LocalAudioTrack track) async { if (audioTracks.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) { throw TrackPublishException('track already exists'); } - // try { - final trackInfo = await _engine.addTrack( - cid: track.getCid(), - name: track.name, - kind: track.kind, - ); + // await AudioManager().incrementPublishCounter(); - final transceiverInit = rtc.RTCRtpTransceiverInit( - direction: rtc.TransceiverDirection.SendOnly, - ); - // addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web) - track.transceiver = await _engine.publisher?.pc.addTransceiver( - track: track.mediaStreamTrack, - kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeAudio, - init: transceiverInit, - ); - await _engine.negotiate(); + try { + final trackInfo = await engine.addTrack( + cid: track.getCid(), + name: track.name, + kind: track.kind, + ); - final pub = LocalTrackPublication(trackInfo, track, this); - addTrackPublication(pub); - notifyListeners(); + await track.start(); - return pub; + final transceiverInit = rtc.RTCRtpTransceiverInit( + direction: rtc.TransceiverDirection.SendOnly, + ); + // addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web) + track.transceiver = await engine.publisher?.pc.addTransceiver( + track: track.mediaStreamTrack, + kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeAudio, + init: transceiverInit, + ); + await engine.negotiate(); + + final pub = LocalTrackPublication(trackInfo, track, this); + addTrackPublication(pub); + notifyListeners(); + return pub; + } catch (e) { + // In any case there was an exception, revert the count. + // await AudioManager().decrementPublishCounter(); + rethrow; + } } /// Publish a video track to the room @@ -83,13 +89,16 @@ class LocalParticipant extends Participant { // Use default options from `ConnectOptions` if options is null options = options ?? defaultPublishOptions; - final trackInfo = await _engine.addTrack( + final trackInfo = await engine.addTrack( cid: track.getCid(), name: track.name, kind: track.kind, ); + logger.fine('publishVideoTrack addTrack response: ${trackInfo}'); + await track.start(); + // Video encodings and simulcasts // use constraints passed to getUserMedia by default @@ -124,14 +133,14 @@ class LocalParticipant extends Participant { streams: [track.mediaStream], ); - logger.fine('publishVideoTrack publisher: ${_engine.publisher}'); + logger.fine('publishVideoTrack publisher: ${engine.publisher}'); - track.transceiver = await _engine.publisher?.pc.addTransceiver( + track.transceiver = await engine.publisher?.pc.addTransceiver( track: track.mediaStreamTrack, kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeVideo, init: transceiverInit, ); - await _engine.negotiate(); + await engine.negotiate(); final pub = LocalTrackPublication(trackInfo, track, this); addTrackPublication(pub); @@ -156,8 +165,17 @@ class LocalParticipant extends Participant { final sender = track.transceiver?.sender; if (sender != null) { - await engine.publisher?.pc.removeTrack(sender); - await engine.negotiate(); + try { + await engine.publisher?.pc.removeTrack(sender); + } catch (_) { + logger.warning('[$objectId] rtc.removeTrack() did throw ${_}'); + } + + // doesn't make sense to negotiate if already disposed + if (!isDisposed) { + // manual negotiation since track changed + await engine.negotiate(); + } } } } diff --git a/lib/src/participant/participant.dart b/lib/src/participant/participant.dart index 32ed8be..1ede18c 100644 --- a/lib/src/participant/participant.dart +++ b/lib/src/participant/participant.dart @@ -1,13 +1,12 @@ import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:meta/meta.dart'; -import '../classes/change_notifier.dart'; import '../events.dart'; import '../extensions.dart'; import '../logger.dart'; import '../managers/event.dart'; import '../proto/livekit_models.pb.dart' as lk_models; +import '../support/disposable.dart'; import '../track/track_publication.dart'; import 'remote_participant.dart'; @@ -21,7 +20,7 @@ import 'remote_participant.dart'; /// Base for [RemoteParticipant] and [LocalParticipant], /// can not be instantiated directly. -abstract class Participant extends LKChangeNotifier { +abstract class Participant extends DisposableChangeNotifier with EventsEmittable { /// map of track sid => published track final trackPublications = {}; @@ -44,7 +43,6 @@ abstract class Participant extends LKChangeNotifier { bool _isSpeaking = false; // suppport for multiple event listeners - final events = EventsEmitter(); final EventsEmitter roomEvents; /// when the participant joined the room @@ -85,14 +83,11 @@ abstract class Participant extends LKChangeNotifier { logger.fine('[ParticipantEvent] $event, will notifyListeners()'); notifyListeners(); }); - } - @override - @mustCallSuper - Future dispose() async { - logger.fine('$objectId dispose()'); - await events.dispose(); - super.dispose(); + onDispose(() async { + await events.dispose(); + await unpublishAllTracks(); + }); } /// for internal use @@ -106,7 +101,7 @@ abstract class Participant extends LKChangeNotifier { lastSpokeAt = DateTime.now(); } - [events, roomEvents].emit(SpeakingChangedEvent( + events.emit(SpeakingChangedEvent( participant: this, speaking: speaking, )); @@ -145,15 +140,17 @@ abstract class Participant extends LKChangeNotifier { // Must implement Future unpublishTrack(String trackSid, {bool notify = false}); - Future unpublishAllTracks() async { - final _ = List.from(trackPublications.values); - for (final track in _) { - await unpublishTrack(track.sid); + Future unpublishAllTracks({bool notify = false}) async { + final trackSids = trackPublications.keys.toSet(); + for (final trackid in trackSids) { + await unpublishTrack(trackid, notify: notify); } } + // // Equality operators // Object is considered equal when sid is equal + // @override int get hashCode => sid.hashCode; diff --git a/lib/src/participant/remote_participant.dart b/lib/src/participant/remote_participant.dart index fdcf8a1..177a7eb 100644 --- a/lib/src/participant/remote_participant.dart +++ b/lib/src/participant/remote_participant.dart @@ -90,8 +90,10 @@ class RemoteParticipant extends Participant { final Track track; if (pub.kind == lk_models.TrackType.AUDIO) { // audio track + // await AudioManager().incrementSubscriptionCounter(); + final audioTrack = AudioTrack(pub.name, mediaTrack, stream); - audioTrack.start(); + await audioTrack.start(); track = audioTrack; } else { // video track @@ -143,8 +145,7 @@ class RemoteParticipant extends Participant { // unpublish any track that is not in the info final validSids = info.tracks.map((e) => e.sid); - final removeSids = - trackPublications.values.where((e) => !validSids.contains(e.sid)).map((e) => e.sid); + final removeSids = trackPublications.keys.where((e) => !validSids.contains(e)).toSet(); for (final sid in removeSids) { await unpublishTrack(sid, notify: true); } @@ -165,6 +166,10 @@ class RemoteParticipant extends Participant { track: track, publication: pub, )); + + // if (track is AudioTrack) { + // await AudioManager().decrementSubscriptionCounter(); + // } } if (notify) { diff --git a/lib/src/proto/livekit_models.pb.dart b/lib/src/proto/livekit_models.pb.dart index 09f934c..0d4251f 100644 --- a/lib/src/proto/livekit_models.pb.dart +++ b/lib/src/proto/livekit_models.pb.dart @@ -37,6 +37,7 @@ class Room extends $pb.GeneratedMessage { const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'enabledCodecs', $pb.PbFieldType.PM, subBuilder: Codec.create) + ..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'metadata') ..hasRequiredFields = false; Room._() : super(); @@ -48,6 +49,7 @@ class Room extends $pb.GeneratedMessage { $fixnum.Int64? creationTime, $core.String? turnPassword, $core.Iterable? enabledCodecs, + $core.String? metadata, }) { final _result = create(); if (sid != null) { @@ -71,6 +73,9 @@ class Room extends $pb.GeneratedMessage { if (enabledCodecs != null) { _result.enabledCodecs.addAll(enabledCodecs); } + if (metadata != null) { + _result.metadata = metadata; + } return _result; } factory Room.fromBuffer($core.List<$core.int> i, @@ -171,6 +176,18 @@ class Room extends $pb.GeneratedMessage { @$pb.TagNumber(7) $core.List get enabledCodecs => $_getList(6); + + @$pb.TagNumber(8) + $core.String get metadata => $_getSZ(7); + @$pb.TagNumber(8) + set metadata($core.String v) { + $_setString(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasMetadata() => $_has(7); + @$pb.TagNumber(8) + void clearMetadata() => clearField(8); } class Codec extends $pb.GeneratedMessage { @@ -922,3 +939,112 @@ class UserPacket extends $pb.GeneratedMessage { @$pb.TagNumber(3) $core.List<$core.String> get destinationSids => $_getList(2); } + +class RecordingResult extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RecordingResult', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), + createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id') + ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'error') + ..aInt64(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'duration') + ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'location') + ..hasRequiredFields = false; + + RecordingResult._() : super(); + factory RecordingResult({ + $core.String? id, + $core.String? error, + $fixnum.Int64? duration, + $core.String? location, + }) { + final _result = create(); + if (id != null) { + _result.id = id; + } + if (error != null) { + _result.error = error; + } + if (duration != null) { + _result.duration = duration; + } + if (location != null) { + _result.location = location; + } + return _result; + } + factory RecordingResult.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory RecordingResult.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RecordingResult clone() => RecordingResult()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RecordingResult copyWith(void Function(RecordingResult) updates) => + super.copyWith((message) => updates(message as RecordingResult)) + as RecordingResult; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RecordingResult create() => RecordingResult._(); + RecordingResult createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RecordingResult getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static RecordingResult? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get id => $_getSZ(0); + @$pb.TagNumber(1) + set id($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get error => $_getSZ(1); + @$pb.TagNumber(2) + set error($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasError() => $_has(1); + @$pb.TagNumber(2) + void clearError() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get duration => $_getI64(2); + @$pb.TagNumber(3) + set duration($fixnum.Int64 v) { + $_setInt64(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasDuration() => $_has(2); + @$pb.TagNumber(3) + void clearDuration() => clearField(3); + + @$pb.TagNumber(4) + $core.String get location => $_getSZ(3); + @$pb.TagNumber(4) + set location($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasLocation() => $_has(3); + @$pb.TagNumber(4) + void clearLocation() => clearField(4); +} diff --git a/lib/src/proto/livekit_models.pbjson.dart b/lib/src/proto/livekit_models.pbjson.dart index c03167f..0ca1adf 100644 --- a/lib/src/proto/livekit_models.pbjson.dart +++ b/lib/src/proto/livekit_models.pbjson.dart @@ -40,12 +40,13 @@ const Room$json = const { '6': '.livekit.Codec', '10': 'enabledCodecs' }, + const {'1': 'metadata', '3': 8, '4': 1, '5': 9, '10': 'metadata'}, ], }; /// Descriptor for `Room`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List roomDescriptor = $convert.base64Decode( - 'CgRSb29tEhAKA3NpZBgBIAEoCVIDc2lkEhIKBG5hbWUYAiABKAlSBG5hbWUSIwoNZW1wdHlfdGltZW91dBgDIAEoDVIMZW1wdHlUaW1lb3V0EikKEG1heF9wYXJ0aWNpcGFudHMYBCABKA1SD21heFBhcnRpY2lwYW50cxIjCg1jcmVhdGlvbl90aW1lGAUgASgDUgxjcmVhdGlvblRpbWUSIwoNdHVybl9wYXNzd29yZBgGIAEoCVIMdHVyblBhc3N3b3JkEjUKDmVuYWJsZWRfY29kZWNzGAcgAygLMg4ubGl2ZWtpdC5Db2RlY1INZW5hYmxlZENvZGVjcw=='); + 'CgRSb29tEhAKA3NpZBgBIAEoCVIDc2lkEhIKBG5hbWUYAiABKAlSBG5hbWUSIwoNZW1wdHlfdGltZW91dBgDIAEoDVIMZW1wdHlUaW1lb3V0EikKEG1heF9wYXJ0aWNpcGFudHMYBCABKA1SD21heFBhcnRpY2lwYW50cxIjCg1jcmVhdGlvbl90aW1lGAUgASgDUgxjcmVhdGlvblRpbWUSIwoNdHVybl9wYXNzd29yZBgGIAEoCVIMdHVyblBhc3N3b3JkEjUKDmVuYWJsZWRfY29kZWNzGAcgAygLMg4ubGl2ZWtpdC5Db2RlY1INZW5hYmxlZENvZGVjcxIaCghtZXRhZGF0YRgIIAEoCVIIbWV0YWRhdGE='); @$core.Deprecated('Use codecDescriptor instead') const Codec$json = const { '1': 'Codec', @@ -182,3 +183,17 @@ const UserPacket$json = const { /// Descriptor for `UserPacket`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List userPacketDescriptor = $convert.base64Decode( 'CgpVc2VyUGFja2V0EicKD3BhcnRpY2lwYW50X3NpZBgBIAEoCVIOcGFydGljaXBhbnRTaWQSGAoHcGF5bG9hZBgCIAEoDFIHcGF5bG9hZBIpChBkZXN0aW5hdGlvbl9zaWRzGAMgAygJUg9kZXN0aW5hdGlvblNpZHM='); +@$core.Deprecated('Use recordingResultDescriptor instead') +const RecordingResult$json = const { + '1': 'RecordingResult', + '2': const [ + const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'}, + const {'1': 'error', '3': 2, '4': 1, '5': 9, '10': 'error'}, + const {'1': 'duration', '3': 3, '4': 1, '5': 3, '10': 'duration'}, + const {'1': 'location', '3': 4, '4': 1, '5': 9, '10': 'location'}, + ], +}; + +/// Descriptor for `RecordingResult`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List recordingResultDescriptor = $convert.base64Decode( + 'Cg9SZWNvcmRpbmdSZXN1bHQSDgoCaWQYASABKAlSAmlkEhQKBWVycm9yGAIgASgJUgVlcnJvchIaCghkdXJhdGlvbhgDIAEoA1IIZHVyYXRpb24SGgoIbG9jYXRpb24YBCABKAlSCGxvY2F0aW9u'); diff --git a/lib/src/proto/livekit_rtc.pb.dart b/lib/src/proto/livekit_rtc.pb.dart index 42dab15..d6ceb95 100644 --- a/lib/src/proto/livekit_rtc.pb.dart +++ b/lib/src/proto/livekit_rtc.pb.dart @@ -282,9 +282,10 @@ enum SignalResponse_Message { trickle, update, trackPublished, - speaker, leave, mute, + speakersChanged, + roomUpdate, notSet } @@ -296,9 +297,10 @@ class SignalResponse extends $pb.GeneratedMessage { 4: SignalResponse_Message.trickle, 5: SignalResponse_Message.update, 6: SignalResponse_Message.trackPublished, - 7: SignalResponse_Message.speaker, 8: SignalResponse_Message.leave, 9: SignalResponse_Message.mute, + 10: SignalResponse_Message.speakersChanged, + 11: SignalResponse_Message.roomUpdate, 0: SignalResponse_Message.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -306,7 +308,7 @@ class SignalResponse extends $pb.GeneratedMessage { package: const $pb.PackageName( const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + ..oo(0, [1, 2, 3, 4, 5, 6, 8, 9, 10, 11]) ..aOM( 1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join', subBuilder: JoinResponse.create) @@ -325,15 +327,18 @@ class SignalResponse extends $pb.GeneratedMessage { ..aOM( 6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished', subBuilder: TrackPublishedResponse.create) - ..aOM<$0.ActiveSpeakerUpdate>( - 7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker', - subBuilder: $0.ActiveSpeakerUpdate.create) ..aOM( 8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', subBuilder: LeaveRequest.create) ..aOM( 9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute', subBuilder: MuteTrackRequest.create) + ..aOM( + 10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speakersChanged', + subBuilder: SpeakersChanged.create) + ..aOM( + 11, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'roomUpdate', + subBuilder: RoomUpdate.create) ..hasRequiredFields = false; SignalResponse._() : super(); @@ -344,9 +349,10 @@ class SignalResponse extends $pb.GeneratedMessage { TrickleRequest? trickle, ParticipantUpdate? update, TrackPublishedResponse? trackPublished, - $0.ActiveSpeakerUpdate? speaker, LeaveRequest? leave, MuteTrackRequest? mute, + SpeakersChanged? speakersChanged, + RoomUpdate? roomUpdate, }) { final _result = create(); if (join != null) { @@ -367,15 +373,18 @@ class SignalResponse extends $pb.GeneratedMessage { if (trackPublished != null) { _result.trackPublished = trackPublished; } - if (speaker != null) { - _result.speaker = speaker; - } if (leave != null) { _result.leave = leave; } if (mute != null) { _result.mute = mute; } + if (speakersChanged != null) { + _result.speakersChanged = speakersChanged; + } + if (roomUpdate != null) { + _result.roomUpdate = roomUpdate; + } return _result; } factory SignalResponse.fromBuffer($core.List<$core.int> i, @@ -491,47 +500,61 @@ class SignalResponse extends $pb.GeneratedMessage { @$pb.TagNumber(6) TrackPublishedResponse ensureTrackPublished() => $_ensure(5); - @$pb.TagNumber(7) - $0.ActiveSpeakerUpdate get speaker => $_getN(6); - @$pb.TagNumber(7) - set speaker($0.ActiveSpeakerUpdate v) { - setField(7, v); - } - - @$pb.TagNumber(7) - $core.bool hasSpeaker() => $_has(6); - @$pb.TagNumber(7) - void clearSpeaker() => clearField(7); - @$pb.TagNumber(7) - $0.ActiveSpeakerUpdate ensureSpeaker() => $_ensure(6); - @$pb.TagNumber(8) - LeaveRequest get leave => $_getN(7); + LeaveRequest get leave => $_getN(6); @$pb.TagNumber(8) set leave(LeaveRequest v) { setField(8, v); } @$pb.TagNumber(8) - $core.bool hasLeave() => $_has(7); + $core.bool hasLeave() => $_has(6); @$pb.TagNumber(8) void clearLeave() => clearField(8); @$pb.TagNumber(8) - LeaveRequest ensureLeave() => $_ensure(7); + LeaveRequest ensureLeave() => $_ensure(6); @$pb.TagNumber(9) - MuteTrackRequest get mute => $_getN(8); + MuteTrackRequest get mute => $_getN(7); @$pb.TagNumber(9) set mute(MuteTrackRequest v) { setField(9, v); } @$pb.TagNumber(9) - $core.bool hasMute() => $_has(8); + $core.bool hasMute() => $_has(7); @$pb.TagNumber(9) void clearMute() => clearField(9); @$pb.TagNumber(9) - MuteTrackRequest ensureMute() => $_ensure(8); + MuteTrackRequest ensureMute() => $_ensure(7); + + @$pb.TagNumber(10) + SpeakersChanged get speakersChanged => $_getN(8); + @$pb.TagNumber(10) + set speakersChanged(SpeakersChanged v) { + setField(10, v); + } + + @$pb.TagNumber(10) + $core.bool hasSpeakersChanged() => $_has(8); + @$pb.TagNumber(10) + void clearSpeakersChanged() => clearField(10); + @$pb.TagNumber(10) + SpeakersChanged ensureSpeakersChanged() => $_ensure(8); + + @$pb.TagNumber(11) + RoomUpdate get roomUpdate => $_getN(9); + @$pb.TagNumber(11) + set roomUpdate(RoomUpdate v) { + setField(11, v); + } + + @$pb.TagNumber(11) + $core.bool hasRoomUpdate() => $_has(9); + @$pb.TagNumber(11) + void clearRoomUpdate() => clearField(11); + @$pb.TagNumber(11) + RoomUpdate ensureRoomUpdate() => $_ensure(9); } class AddTrackRequest extends $pb.GeneratedMessage { @@ -1559,3 +1582,117 @@ class ICEServer extends $pb.GeneratedMessage { @$pb.TagNumber(3) void clearCredential() => clearField(3); } + +class SpeakersChanged extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SpeakersChanged', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), + createEmptyInstance: create) + ..pc<$0.SpeakerInfo>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speakers', + $pb.PbFieldType.PM, + subBuilder: $0.SpeakerInfo.create) + ..hasRequiredFields = false; + + SpeakersChanged._() : super(); + factory SpeakersChanged({ + $core.Iterable<$0.SpeakerInfo>? speakers, + }) { + final _result = create(); + if (speakers != null) { + _result.speakers.addAll(speakers); + } + return _result; + } + factory SpeakersChanged.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory SpeakersChanged.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpeakersChanged clone() => SpeakersChanged()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpeakersChanged copyWith(void Function(SpeakersChanged) updates) => + super.copyWith((message) => updates(message as SpeakersChanged)) + as SpeakersChanged; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static SpeakersChanged create() => SpeakersChanged._(); + SpeakersChanged createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpeakersChanged getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpeakersChanged? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$0.SpeakerInfo> get speakers => $_getList(0); +} + +class RoomUpdate extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RoomUpdate', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), + createEmptyInstance: create) + ..aOM<$0.Room>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'room', + subBuilder: $0.Room.create) + ..hasRequiredFields = false; + + RoomUpdate._() : super(); + factory RoomUpdate({ + $0.Room? room, + }) { + final _result = create(); + if (room != null) { + _result.room = room; + } + return _result; + } + factory RoomUpdate.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory RoomUpdate.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RoomUpdate clone() => RoomUpdate()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RoomUpdate copyWith(void Function(RoomUpdate) updates) => + super.copyWith((message) => updates(message as RoomUpdate)) + as RoomUpdate; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RoomUpdate create() => RoomUpdate._(); + RoomUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RoomUpdate getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static RoomUpdate? _defaultInstance; + + @$pb.TagNumber(1) + $0.Room get room => $_getN(0); + @$pb.TagNumber(1) + set room($0.Room v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasRoom() => $_has(0); + @$pb.TagNumber(1) + void clearRoom() => clearField(1); + @$pb.TagNumber(1) + $0.Room ensureRoom() => $_ensure(0); +} diff --git a/lib/src/proto/livekit_rtc.pbjson.dart b/lib/src/proto/livekit_rtc.pbjson.dart index 36b572d..4833792 100644 --- a/lib/src/proto/livekit_rtc.pbjson.dart +++ b/lib/src/proto/livekit_rtc.pbjson.dart @@ -186,15 +186,6 @@ const SignalResponse$json = const { '9': 0, '10': 'trackPublished' }, - const { - '1': 'speaker', - '3': 7, - '4': 1, - '5': 11, - '6': '.livekit.ActiveSpeakerUpdate', - '9': 0, - '10': 'speaker' - }, const { '1': 'leave', '3': 8, @@ -213,6 +204,24 @@ const SignalResponse$json = const { '9': 0, '10': 'mute' }, + const { + '1': 'speakers_changed', + '3': 10, + '4': 1, + '5': 11, + '6': '.livekit.SpeakersChanged', + '9': 0, + '10': 'speakersChanged' + }, + const { + '1': 'room_update', + '3': 11, + '4': 1, + '5': 11, + '6': '.livekit.RoomUpdate', + '9': 0, + '10': 'roomUpdate' + }, ], '8': const [ const {'1': 'message'}, @@ -221,7 +230,7 @@ const SignalResponse$json = const { /// Descriptor for `SignalResponse`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List signalResponseDescriptor = $convert.base64Decode( - 'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBI4CgdzcGVha2VyGAcgASgLMhwubGl2ZWtpdC5BY3RpdmVTcGVha2VyVXBkYXRlSABSB3NwZWFrZXISLQoFbGVhdmUYCCABKAsyFS5saXZla2l0LkxlYXZlUmVxdWVzdEgAUgVsZWF2ZRIvCgRtdXRlGAkgASgLMhkubGl2ZWtpdC5NdXRlVHJhY2tSZXF1ZXN0SABSBG11dGVCCQoHbWVzc2FnZQ=='); + 'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBItCgVsZWF2ZRgIIAEoCzIVLmxpdmVraXQuTGVhdmVSZXF1ZXN0SABSBWxlYXZlEi8KBG11dGUYCSABKAsyGS5saXZla2l0Lk11dGVUcmFja1JlcXVlc3RIAFIEbXV0ZRJFChBzcGVha2Vyc19jaGFuZ2VkGAogASgLMhgubGl2ZWtpdC5TcGVha2Vyc0NoYW5nZWRIAFIPc3BlYWtlcnNDaGFuZ2VkEjYKC3Jvb21fdXBkYXRlGAsgASgLMhMubGl2ZWtpdC5Sb29tVXBkYXRlSABSCnJvb21VcGRhdGVCCQoHbWVzc2FnZQ=='); @$core.Deprecated('Use addTrackRequestDescriptor instead') const AddTrackRequest$json = const { '1': 'AddTrackRequest', @@ -402,3 +411,25 @@ const ICEServer$json = const { /// Descriptor for `ICEServer`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List iCEServerDescriptor = $convert.base64Decode( 'CglJQ0VTZXJ2ZXISEgoEdXJscxgBIAMoCVIEdXJscxIaCgh1c2VybmFtZRgCIAEoCVIIdXNlcm5hbWUSHgoKY3JlZGVudGlhbBgDIAEoCVIKY3JlZGVudGlhbA=='); +@$core.Deprecated('Use speakersChangedDescriptor instead') +const SpeakersChanged$json = const { + '1': 'SpeakersChanged', + '2': const [ + const {'1': 'speakers', '3': 1, '4': 3, '5': 11, '6': '.livekit.SpeakerInfo', '10': 'speakers'}, + ], +}; + +/// Descriptor for `SpeakersChanged`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List speakersChangedDescriptor = $convert.base64Decode( + 'Cg9TcGVha2Vyc0NoYW5nZWQSMAoIc3BlYWtlcnMYASADKAsyFC5saXZla2l0LlNwZWFrZXJJbmZvUghzcGVha2Vycw=='); +@$core.Deprecated('Use roomUpdateDescriptor instead') +const RoomUpdate$json = const { + '1': 'RoomUpdate', + '2': const [ + const {'1': 'room', '3': 1, '4': 1, '5': 11, '6': '.livekit.Room', '10': 'room'}, + ], +}; + +/// Descriptor for `RoomUpdate`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List roomUpdateDescriptor = + $convert.base64Decode('CgpSb29tVXBkYXRlEiEKBHJvb20YASABKAsyDS5saXZla2l0LlJvb21SBHJvb20='); diff --git a/lib/src/room.dart b/lib/src/room.dart index 5a0852e..5672b3f 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -1,10 +1,11 @@ -import 'dart:async'; import 'dart:collection'; -import 'classes/change_notifier.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; + import 'constants.dart'; -import 'errors.dart'; import 'events.dart'; +import 'exceptions.dart'; import 'extensions.dart'; import 'logger.dart'; import 'managers/event.dart'; @@ -15,9 +16,8 @@ import 'participant/remote_participant.dart'; import 'proto/livekit_models.pb.dart' as lk_models; import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'rtc_engine.dart'; -import 'signal_client.dart'; +import 'support/disposable.dart'; import 'track/track.dart'; -import 'track/track_publication.dart'; import 'types.dart'; /// Room is the primary construct for LiveKit conferences. It contains a @@ -29,14 +29,14 @@ import 'types.dart'; /// * participant membership changes /// * active speakers are different /// {@category Room} -class Room extends LKChangeNotifier { +class Room extends DisposableChangeNotifier with EventsEmittable { // Room is only instantiated if connected, so defaults to connected. ConnectionState _connectionState = ConnectionState.connected; /// connection state of the room ConnectionState get connectionState => _connectionState; - final Map _participants = {}; + final _participants = {}; /// map of SID to RemoteParticipant UnmodifiableMapView get participants => @@ -46,10 +46,10 @@ class Room extends LKChangeNotifier { late final LocalParticipant localParticipant; /// name of the room - late final String name; + final String name; /// sid of the room - late final String sid; + final String sid; List _activeSpeakers = []; @@ -60,8 +60,7 @@ class Room extends LKChangeNotifier { final RTCEngine engine; // suppport for multiple event listeners - final events = EventsEmitter(); - late final _engineListener = EventsListener(engine.events); + late final _engineListener = engine.createListener(); /// internal use /// {@nodoc} @@ -69,7 +68,8 @@ class Room extends LKChangeNotifier { required this.engine, required lk_rtc.JoinResponse joinResponse, ConnectOptions? connectOptions, - }) { + }) : sid = joinResponse.room.sid, + name = joinResponse.room.name { // _setUpListeners(); @@ -80,9 +80,6 @@ class Room extends LKChangeNotifier { roomEvents: events, ); - sid = joinResponse.room.sid; - name = joinResponse.room.name; - for (final info in joinResponse.otherParticipants) { _getOrCreateRemoteParticipant(info.sid, info); } @@ -92,20 +89,17 @@ class Room extends LKChangeNotifier { logger.fine('[RoomEvent] $event, will notifyListeners()'); notifyListeners(); }); - } - @override - Future dispose() async { - // dispose local participant - await localParticipant.dispose(); - // dispose Room's events emitter - await events.dispose(); - // dispose all listeners for RTCEngine - await _engineListener.dispose(); - // dispose the engine - await engine.dispose(); - - super.dispose(); + onDispose(() async { + // dispose events + await events.dispose(); + // dispose local participant + await localParticipant.dispose(); + // dispose all listeners for RTCEngine + await _engineListener.dispose(); + // dispose the engine + await engine.dispose(); + }); } static Future connect( @@ -116,7 +110,6 @@ class Room extends LKChangeNotifier { }) async { // final engine = RTCEngine( - SignalClient(), rtcConfig, ); @@ -167,16 +160,16 @@ class Room extends LKChangeNotifier { ..on((event) async { _connectionState = ConnectionState.connected; events.emit(const RoomReconnectedEvent()); - notifyListeners(); }) ..on((event) async { _connectionState = ConnectionState.reconnecting; events.emit(const RoomReconnectingEvent()); - notifyListeners(); }) - ..on((event) => _onDisconnectedEvent()) - ..on((event) => _onParticipantUpdateEvent(event.participants)) - ..on((event) => _onSpeakerUpdateEvent(event.speakers)) + ..on((event) => _handleClose()) + ..on((event) => _onParticipantUpdateEvent(event.participants)) + ..on( + (event) => _onEngineActiveSpeakersUpdateEvent(event.speakers)) + ..on((event) => _onSignalSpeakersChangedEvent(event.speakers)) ..on(_onDataMessageEvent) ..on((event) async { final track = localParticipant.trackPublications[event.sid]; @@ -210,8 +203,10 @@ class Room extends LKChangeNotifier { /// Disconnects from the room, notifying server of disconnection. Future disconnect() async { - engine.signalClient.sendLeave(); - await _onDisconnectedEvent(); + if (_connectionState != ConnectionState.disconnected) { + engine.signalClient.sendLeave(); + } + await _handleClose(); } Future reconnect() async { @@ -244,42 +239,36 @@ class Room extends LKChangeNotifier { return participant; } - Future _onDisconnectedEvent() async { + // there should be no problem calling this method multiple times + Future _handleClose() async { + logger.fine('[$objectId] _handleClose()'); if (_connectionState == ConnectionState.disconnected) { - logger.fine('$objectId: _handleDisconnect() already disconnected'); - return; + logger.warning('[$objectId]: close() already disconnected'); } - // we need to flag room as disconnected immediately to avoid - // this method firing multiple times since the following code - // is being awaited - _connectionState = ConnectionState.disconnected; // clean up RemoteParticipants - for (final _ in _participants.values) { + for (final _ in _participants.values.toList()) { // RemoteParticipant is responsible for disposing resources - await _.unpublishAllTracks(); await _.dispose(); } _participants.clear(); // clean up LocalParticipant - // for (final pub in localParticipant.tracks.values) { - // await pub.track?.stop(); - // } await localParticipant.unpublishAllTracks(); - // await localParticipant.dispose(); - // localParticipant = null; - + // clean up engine await engine.close(); _activeSpeakers.clear(); - notifyListeners(); - events.emit(const RoomDisconnectedEvent()); + // only notify if was not disconnected + if (_connectionState != ConnectionState.disconnected) { + _connectionState = ConnectionState.disconnected; + events.emit(const RoomDisconnectedEvent()); + } } - void _onParticipantUpdateEvent(List updates) async { + Future _onParticipantUpdateEvent(List updates) async { // trigger change notifier only if list of participants membership is changed var hasChanged = false; for (final info in updates) { @@ -290,7 +279,7 @@ class Room extends LKChangeNotifier { if (info.state == lk_models.ParticipantInfo_State.DISCONNECTED) { hasChanged = true; - _handleParticipantDisconnect(info.sid); + await _handleParticipantDisconnect(info.sid); continue; } @@ -310,43 +299,63 @@ class Room extends LKChangeNotifier { } } - void _onSpeakerUpdateEvent(List speakers) { - final seenSids = {}; - List newSpeakers = []; - for (final info in speakers) { - seenSids.add(info.sid); + void _onSignalSpeakersChangedEvent(List speakers) { + // + final lastSpeakers = { + for (final p in _activeSpeakers) p.sid: p, + }; - if (info.sid == localParticipant.sid) { - localParticipant.audioLevel = info.level; - localParticipant.isSpeaking = true; - newSpeakers.add(localParticipant); - continue; - } + for (final speaker in speakers) { + Participant? p = _participants[speaker.sid]; + if (speaker.sid == localParticipant.sid) p = localParticipant; + if (p == null) continue; - final participant = participants[info.sid]; - if (participant != null) { - participant.audioLevel = info.level; - participant.isSpeaking = true; - newSpeakers.add(participant); + p.audioLevel = speaker.level; + p.isSpeaking = speaker.active; + if (speaker.active) { + lastSpeakers[speaker.sid] = p; + } else { + lastSpeakers.remove(speaker.sid); } } - // clear previous speakers - if (seenSids.contains(localParticipant.sid)) { - localParticipant.audioLevel = 0; - localParticipant.isSpeaking = false; - } - for (final participant in _participants.values) { - if (!seenSids.contains(participant.sid)) { - participant.audioLevel = 0; - participant.isSpeaking = false; + final activeSpeakers = lastSpeakers.values.toList(); + activeSpeakers.sort((a, b) => b.audioLevel.compareTo(a.audioLevel)); + _activeSpeakers = activeSpeakers; + events.emit(ActiveSpeakersChangedEvent(speakers: activeSpeakers)); + } + + // from data channel + // updates are sent only when there's a change to speaker ordering + void _onEngineActiveSpeakersUpdateEvent(List speakers) { + List activeSpeakers = []; + + // localParticipant & remote participants + final allParticipants = { + localParticipant.sid: localParticipant, + ..._participants, + }; + + for (final speaker in speakers) { + final p = allParticipants[speaker.sid]; + if (p != null) { + p.audioLevel = speaker.level; + p.isSpeaking = true; + activeSpeakers.add(p); } } - events.emit(ActiveSpeakersChangedEvent(speakers: newSpeakers)); + // clear if not in the speakers list + final speakerSids = speakers.map((e) => e.sid).toSet(); + for (final p in allParticipants.values) { + if (!speakerSids.contains(p.sid)) { + p.audioLevel = 0; + p.isSpeaking = false; + } + } - _activeSpeakers = newSpeakers; - notifyListeners(); + _activeSpeakers = activeSpeakers; + events.emit(ActiveSpeakersChangedEvent(speakers: activeSpeakers)); } void _onDataMessageEvent(EngineDataPacketReceivedEvent dataPacketEvent) { @@ -368,16 +377,13 @@ class Room extends LKChangeNotifier { events.emit(event); } - void _handleParticipantDisconnect(String sid) { + Future _handleParticipantDisconnect(String sid) async { final participant = _participants.remove(sid); if (participant == null) { return; } - final toRemove = List.from(participant.trackPublications.values); - for (final track in toRemove) { - participant.unpublishTrack(track.sid, notify: true); - } + await participant.unpublishAllTracks(notify: true); events.emit(ParticipantDisconnectedEvent(participant: participant)); } diff --git a/lib/src/rtc_engine.dart b/lib/src/rtc_engine.dart index 53adb1a..882935c 100644 --- a/lib/src/rtc_engine.dart +++ b/lib/src/rtc_engine.dart @@ -3,10 +3,11 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; +import 'package:meta/meta.dart'; import 'constants.dart'; -import 'errors.dart'; import 'events.dart'; +import 'exceptions.dart'; import 'extensions.dart'; import 'logger.dart'; import 'managers/delay.dart'; @@ -15,10 +16,11 @@ import 'options.dart'; import 'proto/livekit_models.pb.dart' as lk_models; import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'signal_client.dart'; +import 'support/disposable.dart'; import 'transport.dart'; import 'types.dart'; -class RTCEngine { +class RTCEngine extends Disposable with EventsEmittable { static const _lossyDCLabel = '_lossy'; static const _reliableDCLabel = '_reliable'; static const _maxReconnectAttempts = 5; @@ -27,19 +29,25 @@ class RTCEngine { // config for RTCPeerConnection final RTCConfiguration? rtcConfig; + @internal PCTransport? publisher; + + @internal PCTransport? subscriber; + + @internal PCTransport? get primary => _subscriberPrimary ? subscriber : publisher; - // used for ice state notifications - CancelListenFunc? _primaryIceStateListener; - // data channels for packets - rtc.RTCDataChannel? reliableDC; - rtc.RTCDataChannel? lossyDC; - bool iceConnected = false; - bool isReconnecting = false; - bool isClosed = true; + rtc.RTCDataChannel? _reliableDC; + rtc.RTCDataChannel? _lossyDC; + bool _iceConnected = false; + + ConnectionState _connectionState = ConnectionState.disconnected; + + /// connection state of the room + ConnectionState get connectionState => _connectionState; + // true if publisher connection has already been established. // this is helpful to know if we need to restart ICE on the publisher connection bool _hasPublished = false; @@ -55,41 +63,29 @@ class RTCEngine { // internal int _reconnectAttempts = 0; - final events = EventsEmitter(); - late final _signalListener = EventsListener(signalClient.events, synchronized: true); + late final _signalListener = signalClient.createListener(synchronized: true); final delays = CancelableDelayManager(); - // late final Timer _statsTimer; - RTCEngine( - this.signalClient, - this.rtcConfig, - ) { + this.rtcConfig, { + SignalClient? signalClient, + }) : signalClient = signalClient ?? SignalClient() { if (kDebugMode) { // log all EngineEvents events.listen((event) => logger.fine('[EngineEvent] $objectId ${event.runtimeType}')); } _setUpListeners(); - // _statsTimer = Timer.periodic(const Duration(seconds: 1), _onStatTimer); + + onDispose(() async { + await events.dispose(); + await delays.dispose(); + await close(); + await _signalListener.dispose(); + }); } - Future dispose() async { - await events.dispose(); - await _signalListener.dispose(); - } - - // void _onStatTimer(Timer _) async { - // // - // final stats = await publisher?.pc.getStats(); - // if (stats == null || stats.isEmpty) return; - - // for (final s in stats) { - // logger.fine('STATS ${s.values}'); - // } - // } - Future join( String url, String token, { @@ -110,22 +106,15 @@ class RTCEngine { return event.response; } + // there is no side-effect calling this method multiple times Future close() async { - logger.fine('${objectId} close()'); - if (isClosed) { - logger.fine('${objectId} close() already closed'); - return; + logger.fine('[$objectId] close()'); + if (_connectionState == ConnectionState.disconnected) { + logger.warning('[$objectId]: close() already disconnected'); } - isClosed = true; - // _statsTimer.cancel(); - - // cancel events - await _primaryIceStateListener?.call(); - _primaryIceStateListener = null; - // cancel all ongoing delays - await delays.dispose(); + await delays.cancelAll(); // PCTransport is responsible for disposing RTCPeerConnection await publisher?.dispose(); @@ -134,7 +123,10 @@ class RTCEngine { await subscriber?.dispose(); subscriber = null; - signalClient.close(); + await signalClient.close(); + + _connectionState = ConnectionState.disconnected; + // notifyListeners(); } Future addTrack({ @@ -174,10 +166,10 @@ class RTCEngine { final dcMessage = rtc.RTCDataChannelMessage.fromBinary(packet.writeToBuffer()); - if (packet.kind == lk_models.DataPacket_Kind.LOSSY && lossyDC != null) { - await lossyDC?.send(dcMessage); - } else if (packet.kind == lk_models.DataPacket_Kind.RELIABLE && reliableDC != null) { - await reliableDC?.send(dcMessage); + if (packet.kind == lk_models.DataPacket_Kind.LOSSY && _lossyDC != null) { + await _lossyDC?.send(dcMessage); + } else if (packet.kind == lk_models.DataPacket_Kind.RELIABLE && _reliableDC != null) { + await _reliableDC?.send(dcMessage); } } @@ -188,7 +180,7 @@ class RTCEngine { } if (publisher?.pc.iceConnectionState?.isConnected() == true) { - logger.warning('publisher is already connected'); + logger.warning('[$objectId] publisher is already connected'); return; } @@ -207,7 +199,7 @@ class RTCEngine { } Future reconnect() async { - if (isClosed) { + if (_connectionState == ConnectionState.disconnected) { logger.fine('$objectId reconnect() already closed'); return; } @@ -225,7 +217,8 @@ class RTCEngine { _reconnectAttempts++; try { - isReconnecting = true; + // isReconnecting = true; + _connectionState = ConnectionState.reconnecting; await signalClient.reconnect(url, token); if (publisher == null || subscriber == null) { @@ -256,7 +249,8 @@ class RTCEngine { // don't catch and pass up any exception } finally { // always set reconnecting to false - isReconnecting = false; + // isReconnecting = false; + _connectionState = ConnectionState.disconnected; } } @@ -296,31 +290,25 @@ class RTCEngine { subscriber?.pc.onDataChannel = _onDataChannel; } - // logger.fine('subscriber.pc: ${subscriber?.pc}'); - subscriber?.pc.onIceConnectionState = (state) { - // - events.emit(EngineSubscriberIceStateUpdatedEvent( - state: state, - isPrimary: _subscriberPrimary, - )); - }; + subscriber?.pc.onIceConnectionState = + (state) => events.emit(EngineSubscriberIceStateUpdatedEvent( + state: state, + isPrimary: _subscriberPrimary, + )); - publisher?.pc.onIceConnectionState = (state) { - // - events.emit(EnginePublisherIceStateUpdatedEvent( - state: state, - isPrimary: !_subscriberPrimary, - )); - }; + publisher?.pc.onIceConnectionState = (state) => events.emit(EnginePublisherIceStateUpdatedEvent( + state: state, + isPrimary: !_subscriberPrimary, + )); - _primaryIceStateListener ??= events.on((event) { + events.on((event) { // only listen to primary ice events if (!event.isPrimary) return; if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateConnected) { - if (!iceConnected) { - iceConnected = true; - if (isReconnecting) { + if (!_iceConnected) { + _iceConnected = true; + if (_connectionState == ConnectionState.reconnecting) { events.emit(const EngineReconnectedEvent()); } else { events.emit(const EngineConnectedEvent()); @@ -328,8 +316,8 @@ class RTCEngine { } } else if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateFailed) { // trigger reconnect sequence - if (iceConnected) { - iceConnected = false; + if (_iceConnected) { + _iceConnected = false; _onDisconnected('peerconnection'); } } @@ -350,34 +338,40 @@ class RTCEngine { )); }; - // data channels - final lossyInit = rtc.RTCDataChannelInit() - ..binaryType = 'binary' - ..ordered = true - ..maxRetransmits = 0; - lossyDC = await publisher?.pc.createDataChannel(_lossyDCLabel, lossyInit); - - final reliableInit = rtc.RTCDataChannelInit() - ..binaryType = 'binary' - ..ordered = true; - reliableDC = await publisher?.pc.createDataChannel(_reliableDCLabel, reliableInit); - // also handle messages over the pub channel, for backwards compatibility - lossyDC?.onMessage = _onDCMessage; - reliableDC?.onMessage = _onDCMessage; + try { + final lossyInit = rtc.RTCDataChannelInit() + ..binaryType = 'binary' + ..ordered = true + ..maxRetransmits = 0; + _lossyDC = await publisher?.pc.createDataChannel(_lossyDCLabel, lossyInit); + _lossyDC?.onMessage = _onDCMessage; + } catch (_) { + logger.severe('[$objectId] createDataChannel() did throw $_'); + } + + try { + final reliableInit = rtc.RTCDataChannelInit() + ..binaryType = 'binary' + ..ordered = true; + _reliableDC = await publisher?.pc.createDataChannel(_reliableDCLabel, reliableInit); + _reliableDC?.onMessage = _onDCMessage; + } catch (_) { + logger.severe('[$objectId] createDataChannel() did throw $_'); + } } void _onDataChannel(rtc.RTCDataChannel dc) { switch (dc.label) { case _reliableDCLabel: logger.fine('Server opened DC label: ${dc.label}'); - reliableDC = dc; - reliableDC?.onMessage = _onDCMessage; + _reliableDC = dc; + _reliableDC?.onMessage = _onDCMessage; break; case _lossyDCLabel: logger.fine('Server opened DC label: ${dc.label}'); - lossyDC = dc; - lossyDC?.onMessage = _onDCMessage; + _lossyDC = dc; + _lossyDC?.onMessage = _onDCMessage; break; default: logger.warning('Unknown DC label: ${dc.label}'); @@ -395,7 +389,7 @@ class RTCEngine { final dp = lk_models.DataPacket.fromBuffer(message.binary); if (dp.whichValue() == lk_models.DataPacket_Value.speaker) { // Speaker packet - events.emit(EngineSpeakersUpdateEvent(speakers: dp.speaker.speakers)); + events.emit(EngineActiveSpeakersUpdateEvent(speakers: dp.speaker.speakers)); } else if (dp.whichValue() == lk_models.DataPacket_Value.user) { // User packet events.emit(EngineDataPacketReceivedEvent( @@ -406,11 +400,15 @@ class RTCEngine { } Future _onDisconnected(String reason) async { - if (isClosed) return; + if (_connectionState == ConnectionState.disconnected) { + logger.fine('[$objectId] Already disconnected $reason'); + return; + } + + logger.fine('[$objectId] Disconnected $reason'); - logger.fine('disconnected $reason'); if (_reconnectAttempts >= _maxReconnectAttempts) { - logger.info('could not connect after $_reconnectAttempts, giving up'); + logger.info('[$objectId] Could not connect after ${_reconnectAttempts} attempts, giving up'); await close(); events.emit(const EngineDisconnectedEvent()); return; @@ -432,12 +430,10 @@ class RTCEngine { }); } - //------------------ SignalClient Delegate methods -------------------------// - void _setUpListeners() => _signalListener ..on((event) async { // create peer connections - isClosed = false; + _connectionState = ConnectionState.connected; _subscriberPrimary = event.response.subscriberPrimary; _providedIceServers = event.response.iceServers; @@ -451,28 +447,30 @@ class RTCEngine { // for subscriberPrimary, we negotiate when necessary (lazy) await negotiate(); } - - // _joinCompleter?.complete(Future.value(event.response)); - // _joinCompleter = null; }) ..on((_) async { await _onDisconnected('signal'); }) ..on((event) async { if (subscriber == null) { + logger.warning('[$objectId] subscriber is null'); return; } - logger.fine('received server offer(type: ${event.sd.type}, ' + logger.fine('[$objectId] Received server offer(type: ${event.sd.type}, ' '${subscriber!.pc.signalingState})'); await subscriber!.setRemoteDescription(event.sd); - final answer = await subscriber!.pc.createAnswer(); - logger.fine('Created answer'); - logger.finer('sdp: ${answer.sdp}'); - await subscriber!.pc.setLocalDescription(answer); - signalClient.sendAnswer(answer); + try { + final answer = await subscriber!.pc.createAnswer(); + logger.fine('Created answer'); + logger.finer('sdp: ${answer.sdp}'); + await subscriber!.pc.setLocalDescription(answer); + signalClient.sendAnswer(answer); + } catch (_) { + logger.severe('[$objectId] Failed to createAnswer()'); + } }) ..on((event) async { if (publisher == null) { @@ -494,16 +492,10 @@ class RTCEngine { await publisher!.addIceCandidate(event.candidate); } }) - ..on((event) async { - events.emit(EngineParticipantUpdateEvent(participants: event.updates)); - }) - // ..on((event) async { - // final completer = _pendingTrackResolvers.remove(event.cid); - // completer?.complete(event.track); - // }) - ..on((event) async { - events.emit(EngineSpeakersUpdateEvent(speakers: event.speakers)); - }) + // relay + ..on((event) => events.emit(event)) + // relay + ..on((event) => events.emit(event)) ..on((event) async { await close(); events.emit(const EngineDisconnectedEvent()); diff --git a/lib/src/signal_client.dart b/lib/src/signal_client.dart index 2740219..d7e6fc2 100644 --- a/lib/src/signal_client.dart +++ b/lib/src/signal_client.dart @@ -1,23 +1,23 @@ import 'dart:async'; -import 'dart:convert'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:http/http.dart' as http; -import 'errors.dart'; import 'events.dart'; +import 'exceptions.dart'; import 'extensions.dart'; import 'logger.dart'; import 'managers/event.dart'; import 'options.dart'; import 'proto/livekit_models.pb.dart' as lk_models; import 'proto/livekit_rtc.pb.dart' as lk_rtc; +import 'support/disposable.dart'; +import 'support/websocket.dart'; import 'types.dart'; import 'utils.dart'; -import 'ws/interface.dart'; -class SignalClient { - final events = EventsEmitter(); +class SignalClient extends Disposable with EventsEmittable { + // final ProtocolVersion protocol; bool _connected = false; @@ -29,6 +29,11 @@ class SignalClient { events.listen((event) { logger.fine('[SignalEvent] $event'); }); + + onDispose(() async { + await events.dispose(); + await close(); + }); } bool get connected => _connected; @@ -51,7 +56,7 @@ class SignalClient { try { _ws = await LiveKitWebSocket.connect( rtcUri, - WebSocketOptions( + WebSocketEventHandlers( onData: _onSocketData, onDispose: _onSocketDone, onError: _handleError, @@ -71,8 +76,7 @@ class SignalClient { // Attempt Validation try { final validateResponse = await http.get(validateUri); - if (validateResponse.statusCode != 200) - throw ConnectException(validateResponse.body); + if (validateResponse.statusCode != 200) throw ConnectException(validateResponse.body); throw ConnectException(); } catch (error) { // Pass it up if it's already a `ConnectError` @@ -88,7 +92,7 @@ class SignalClient { String token, ) async { _connected = false; - _ws?.dispose(); + await _ws?.dispose(); _ws = null; final rtcUri = Utils.buildUri( @@ -100,7 +104,7 @@ class SignalClient { _ws = await LiveKitWebSocket.connect( rtcUri, - WebSocketOptions( + WebSocketEventHandlers( onData: _onSocketData, onDispose: _onSocketDone, onError: _handleError, @@ -110,24 +114,27 @@ class SignalClient { _connected = true; } - void close() { + Future close() async { _connected = false; - _ws?.dispose(); + await _ws?.dispose(); } - void sendOffer(rtc.RTCSessionDescription offer) => - _sendRequest(lk_rtc.SignalRequest( + // @override + // Future dispose() async { + // super.dispose(); + // await events.dispose(); + // await close(); + // } + + void sendOffer(rtc.RTCSessionDescription offer) => _sendRequest(lk_rtc.SignalRequest( offer: offer.toSDKType(), )); - void sendAnswer(rtc.RTCSessionDescription answer) => - _sendRequest(lk_rtc.SignalRequest( + void sendAnswer(rtc.RTCSessionDescription answer) => _sendRequest(lk_rtc.SignalRequest( answer: answer.toSDKType(), )); - void sendIceCandidate( - rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) => - _sendRequest( + void sendIceCandidate(rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) => _sendRequest( lk_rtc.SignalRequest( trickle: lk_rtc.TrickleRequest( candidateInit: candidate.toJson(), @@ -136,8 +143,7 @@ class SignalClient { ), ); - void sendMuteTrack(String trackSid, bool muted) => - _sendRequest(lk_rtc.SignalRequest( + void sendMuteTrack(String trackSid, bool muted) => _sendRequest(lk_rtc.SignalRequest( mute: lk_rtc.MuteTrackRequest( sid: trackSid, muted: muted, @@ -174,8 +180,7 @@ class SignalClient { subscription: subscription, )); - void sendSetSimulcastLayers( - String trackSid, List layers) => + void sendSetSimulcastLayers(String trackSid, List layers) => _sendRequest(lk_rtc.SignalRequest( simulcast: lk_rtc.SetSimulcastLayers( trackSid: trackSid, @@ -188,8 +193,8 @@ class SignalClient { )); void _sendRequest(lk_rtc.SignalRequest req) { - if (_ws == null) { - logger.warning('could not send message, not connected'); + if (_ws == null || isDisposed) { + logger.warning('[$objectId] Could not send message, not connected or already disposed'); return; } @@ -221,8 +226,7 @@ class SignalClient { )); break; case lk_rtc.SignalResponse_Message.update: - events.emit( - SignalParticipantUpdateEvent(updates: msg.update.participants)); + events.emit(SignalParticipantUpdateEvent(participants: msg.update.participants)); break; case lk_rtc.SignalResponse_Message.trackPublished: events.emit(SignalLocalTrackPublishedEvent( @@ -230,9 +234,8 @@ class SignalClient { track: msg.trackPublished.track, )); break; - case lk_rtc.SignalResponse_Message.speaker: - events.emit( - SignalActiveSpeakersChangedEvent(speakers: msg.speaker.speakers)); + case lk_rtc.SignalResponse_Message.speakersChanged: + events.emit(SignalSpeakersChangedEvent(speakers: msg.speakersChanged.speakers)); break; case lk_rtc.SignalResponse_Message.leave: events.emit(SignalLeaveEvent(canReconnect: msg.leave.canReconnect)); diff --git a/lib/src/support/disposable.dart b/lib/src/support/disposable.dart new file mode 100644 index 0000000..2795e3e --- /dev/null +++ b/lib/src/support/disposable.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; + +import '../extensions.dart'; +import '../logger.dart'; + +typedef OnDisposeFunc = Future Function(); + +mixin _Disposer { + // + final _disposeFuncs = []; + bool _isDisposed = false; + bool get isDisposed => _isDisposed; + + // last added func will be called first when disposing + void onDispose(OnDisposeFunc func) => _disposeFuncs.add(func); + + Future _dispose() async { + if (!_isDisposed) { + logger.fine('[${objectId}] dispose()'); + _isDisposed = true; + if (_disposeFuncs.isNotEmpty) { + logger.fine('[$objectId] running ${_disposeFuncs.length} dispose funcs...'); + // call dispose funcs in reverse order + for (final _func in _disposeFuncs.reversed) { + await _func(); + } + _disposeFuncs.clear(); + logger.fine('[$objectId] dispose complete.'); + } + return true; + } else { + logger.warning('[$objectId] unnecessary dispose() called.'); + return false; + } + } +} + +abstract class Disposable with _Disposer { + @mustCallSuper + Future dispose() async { + return await _dispose(); + } +} + +abstract class DisposableChangeNotifier extends ChangeNotifier with _Disposer { + @override + Future dispose() async { + if (!isDisposed) super.dispose(); + return await super._dispose(); + } + + @override + bool get hasListeners { + if (isDisposed) { + logger.warning('called hasListeners on a disposed ChangeNotifier'); + return false; + } + return super.hasListeners; + } + + @override + void addListener(VoidCallback listener) { + if (isDisposed) { + logger.warning('called addListener() on a disposed ChangeNotifier'); + return; + } + super.addListener(listener); + } + + @override + void notifyListeners() { + if (isDisposed) { + logger.warning('called notifyListeners() on a disposed ChangeNotifier'); + return; + } + super.notifyListeners(); + } + + @override + void removeListener(VoidCallback listener) { + if (isDisposed) { + logger.warning('called removeListener() on a disposed ChangeNotifier'); + return; + } + super.removeListener(listener); + } +} diff --git a/lib/src/support/native_audio.dart b/lib/src/support/native_audio.dart new file mode 100644 index 0000000..0af4224 --- /dev/null +++ b/lib/src/support/native_audio.dart @@ -0,0 +1,122 @@ +// https://developer.apple.com/documentation/avfaudio/avaudiosession/category +import 'package:flutter/services.dart'; + +import '../logger.dart'; + +enum AppleAudioCategory { + soloAmbient, + playback, + record, + playAndRecord, + multiRoute, +} + +// https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions +enum AppleAudioCategoryOption { + mixWithOthers, // Only playAndRecord, playback, or multiRoute. + duckOthers, // Only playAndRecord, playback, or multiRoute. + interruptSpokenAudioAndMixWithOthers, + allowBluetooth, // Only playAndRecord or record. + allowBluetoothA2DP, + allowAirPlay, + defaultToSpeaker, +} + +// https://developer.apple.com/documentation/avfaudio/avaudiosession/mode +enum AppleAudioMode { + default_, + gameChat, + measurement, + moviePlayback, + spokenAudio, + videoChat, + videoRecording, + voiceChat, + voicePrompt, +} + +extension AppleAudioCategoryExt on AppleAudioCategory { + String toStringValue() => { + AppleAudioCategory.soloAmbient: 'soloAmbient', + AppleAudioCategory.playback: 'playback', + AppleAudioCategory.record: 'record', + AppleAudioCategory.playAndRecord: 'playAndRecord', + AppleAudioCategory.multiRoute: 'multiRoute', + }[this]!; +} + +extension AppleAudioCategoryOptionExt on AppleAudioCategoryOption { + String toStringValue() => { + AppleAudioCategoryOption.mixWithOthers: 'mixWithOthers', + AppleAudioCategoryOption.duckOthers: 'duckOthers', + AppleAudioCategoryOption.interruptSpokenAudioAndMixWithOthers: + 'interruptSpokenAudioAndMixWithOthers', + AppleAudioCategoryOption.allowBluetooth: 'allowBluetooth', + AppleAudioCategoryOption.allowBluetoothA2DP: 'allowBluetoothA2DP', + AppleAudioCategoryOption.allowAirPlay: 'allowAirPlay', + AppleAudioCategoryOption.defaultToSpeaker: 'defaultToSpeaker', + }[this]!; +} + +extension AppleAudioModeExt on AppleAudioMode { + String toStringValue() => { + AppleAudioMode.default_: 'default', + AppleAudioMode.gameChat: 'gameChat', + AppleAudioMode.measurement: 'measurement', + AppleAudioMode.moviePlayback: 'moviePlayback', + AppleAudioMode.spokenAudio: 'spokenAudio', + AppleAudioMode.videoChat: 'videoChat', + AppleAudioMode.videoRecording: 'videoRecording', + AppleAudioMode.voiceChat: 'voiceChat', + AppleAudioMode.voicePrompt: 'voicePrompt', + }[this]!; +} + +class NativeAudioConfiguration { + final AppleAudioCategory? appleAudioCategory; + final Set? appleAudioCategoryOptions; + final AppleAudioMode? appleAudioMode; + + NativeAudioConfiguration({ + // for iOS / Mac + this.appleAudioCategory, + this.appleAudioCategoryOptions, + this.appleAudioMode, + // Android options + // ... + }); + + Map toMap() => { + if (appleAudioCategory != null) 'appleAudioCategory': appleAudioCategory!.toStringValue(), + if (appleAudioCategoryOptions != null) + 'appleAudioCategoryOptions': + appleAudioCategoryOptions!.map((e) => e.toStringValue()).toList(), + if (appleAudioMode != null) 'appleAudioMode': appleAudioMode!.toStringValue(), + }; + + NativeAudioConfiguration copyWith({ + AppleAudioCategory? appleAudioCategory, + Set? appleAudioCategoryOptions, + AppleAudioMode? appleAudioMode, + }) => + NativeAudioConfiguration( + appleAudioCategory: appleAudioCategory ?? this.appleAudioCategory, + appleAudioCategoryOptions: appleAudioCategoryOptions ?? this.appleAudioCategoryOptions, + appleAudioMode: appleAudioMode ?? this.appleAudioMode, + ); +} + +const _lkMethodChannel = MethodChannel('livekit_client'); + +Future configureNativeAudio(NativeAudioConfiguration configuration) async { + try { + final result = await _lkMethodChannel.invokeMethod( + 'configureNativeAudio', + configuration.toMap(), + ); + return result == true; + } catch (_) { + logger.warning('configureAudioSession did throw $_'); + return false; + } +} diff --git a/lib/src/ws/platform/io.dart b/lib/src/support/platforms/io.dart similarity index 54% rename from lib/src/ws/platform/io.dart rename to lib/src/support/platforms/io.dart index 417569e..7d6e7c9 100644 --- a/lib/src/ws/platform/io.dart +++ b/lib/src/support/platforms/io.dart @@ -2,17 +2,18 @@ import 'dart:async'; import 'dart:io' as io; import '../../logger.dart'; -import '../interface.dart'; +import '../websocket.dart'; +import '../../extensions.dart'; Future lkWebSocketConnect( Uri uri, [ - WebSocketOptions? options, + WebSocketEventHandlers? options, ]) => LiveKitWebSocketIO.connect(uri, options); class LiveKitWebSocketIO implements LiveKitWebSocket { final io.WebSocket _ws; - final WebSocketOptions? options; + final WebSocketEventHandlers? options; late final StreamSubscription _subscription; LiveKitWebSocketIO._( @@ -26,38 +27,38 @@ class LiveKitWebSocketIO implements LiveKitWebSocket { } @override - void dispose() { + Future dispose() async { + await _subscription.cancel(); + await _ws.close(); options?.onDispose?.call(); - _subscription.cancel(); - _ws.close(); } @override void send(List data) { - // 0 CONNECTING - // 1 OPEN - // 2 CLOSING - // 3 CLOSED - if (_ws.readyState == 1) { - try { - _ws.add(data); - } catch (e) { - // - } + // 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED + if (_ws.readyState != 1) { + logger.fine('[$objectId] Tried to send data (readyState: ${_ws.readyState})'); + return; + } + + try { + _ws.add(data); + } catch (_) { + logger.fine('[$objectId] send did throw ${_}'); } } static Future connect( Uri uri, [ - WebSocketOptions? options, + WebSocketEventHandlers? options, ]) async { - logger.fine('WebSocketIO connect (uri: ${uri.toString()})'); + logger.fine('[WebSocketIO] Connecting(uri: ${uri.toString()})...'); try { final ws = await io.WebSocket.connect(uri.toString()); - logger.fine('WebSocketIO connected'); + logger.fine('[WebSocketIO] Connected'); return LiveKitWebSocketIO._(ws, options); } catch (_) { - logger.severe('WebSocketIO error ${_}'); + logger.severe('[WebSocketIO] did throw ${_}'); throw WebSocketException.connect(); } } diff --git a/lib/src/ws/platform/web.dart b/lib/src/support/platforms/web.dart similarity index 83% rename from lib/src/ws/platform/web.dart rename to lib/src/support/platforms/web.dart index b1da3d7..953c8e7 100644 --- a/lib/src/ws/platform/web.dart +++ b/lib/src/support/platforms/web.dart @@ -4,17 +4,17 @@ import 'dart:async'; import 'dart:html' as html; import 'dart:typed_data'; -import '../interface.dart'; +import '../websocket.dart'; Future lkWebSocketConnect( Uri uri, [ - WebSocketOptions? options, + WebSocketEventHandlers? options, ]) => LiveKitWebSocketWeb.connect(uri, options); class LiveKitWebSocketWeb implements LiveKitWebSocket { final html.WebSocket _ws; - final WebSocketOptions? options; + final WebSocketEventHandlers? options; late final StreamSubscription _messageSubscription; late final StreamSubscription _closeSubscription; @@ -34,16 +34,16 @@ class LiveKitWebSocketWeb implements LiveKitWebSocket { void send(List data) => _ws.send(data); @override - void dispose() { + Future dispose() async { options?.onDispose?.call(); - _messageSubscription.cancel(); - _closeSubscription.cancel(); + await _messageSubscription.cancel(); + await _closeSubscription.cancel(); _ws.close(); } static Future connect( Uri uri, [ - WebSocketOptions? options, + WebSocketEventHandlers? options, ]) async { final completer = Completer(); final ws = html.WebSocket(uri.toString()); diff --git a/lib/src/ws/interface.dart b/lib/src/support/websocket.dart similarity index 82% rename from lib/src/ws/interface.dart rename to lib/src/support/websocket.dart index f82edf8..8021abf 100644 --- a/lib/src/ws/interface.dart +++ b/lib/src/support/websocket.dart @@ -1,4 +1,4 @@ -import 'platform/io.dart' if (dart.library.html) 'platform/web.dart'; +import 'platforms/io.dart' if (dart.library.html) 'platforms/web.dart'; class WebSocketException implements Exception { final int code; @@ -18,11 +18,11 @@ typedef WebSocketOnData = Function(dynamic data); typedef WebSocketOnError = Function(dynamic error); typedef WebSocketOnDispose = Function(); -class WebSocketOptions { +class WebSocketEventHandlers { final WebSocketOnData? onData; final WebSocketOnError? onError; final WebSocketOnDispose? onDispose; - const WebSocketOptions({ + const WebSocketEventHandlers({ this.onData, this.onError, this.onDispose, @@ -31,11 +31,11 @@ class WebSocketOptions { abstract class LiveKitWebSocket { void send(List data); - void dispose(); + Future dispose(); static Future connect( Uri uri, [ - WebSocketOptions? options, + WebSocketEventHandlers? options, ]) => lkWebSocketConnect(uri, options); } diff --git a/lib/src/track/audio_track.dart b/lib/src/track/audio_track.dart index 057b3d6..68fc5ff 100644 --- a/lib/src/track/audio_track.dart +++ b/lib/src/track/audio_track.dart @@ -1,29 +1,174 @@ -import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; +// import 'package:audio_session/audio_session.dart' as _as; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; +import 'package:synchronized/synchronized.dart' as sync; + +import '../logger.dart'; import '../proto/livekit_models.pb.dart' as lk_models; +import '../support/native_audio.dart'; import '_audio_api.dart' if (dart.library.html) '_audio_html.dart' as audio; import 'local_audio_track.dart'; import 'track.dart'; +enum AudioTrackState { + none, + remoteOnly, + localOnly, + localAndRemote, +} + +typedef ConfigureNativeAudioFunc = Future Function(AudioTrackState state); + class AudioTrack extends Track { + // it's possible to set custom function here to customize audio session configuration + static ConfigureNativeAudioFunc nativeAudioConfigurationForAudioTrackState = + defaultNativeAudioConfigurationFunc; + + static final _trackCounterLock = sync.Lock(); + static AudioTrackState audioTrackState = AudioTrackState.none; + static int _localTrackCount = 0; + static int _remoteTrackCount = 0; + rtc.MediaStream? mediaStream; - AudioTrack(String name, rtc.MediaStreamTrack track, this.mediaStream) - : super(lk_models.TrackType.AUDIO, name, track); + AudioTrack( + String name, + rtc.MediaStreamTrack track, + this.mediaStream, + ) : super( + lk_models.TrackType.AUDIO, + name, + track, + ); /// Start playing audio track. On web platform, create an audio element and /// start playback - void start() { - if (this is! LocalAudioTrack) { - audio.startAudio(getCid(), mediaStreamTrack); + @override + Future start() async { + final didStart = await super.start(); + if (didStart) { + if (this is! LocalAudioTrack) { + audio.startAudio(getCid(), mediaStreamTrack); + } + + // update counter + await _trackCounterLock.synchronized(() async { + if (this is LocalAudioTrack) { + _localTrackCount++; + } else if (this is! LocalAudioTrack) { + _remoteTrackCount++; + } + await _onAudioTrackCountDidChange(); + }); } + + return didStart; } @override - Future stop() async { - await mediaStream?.dispose(); - mediaStream = null; - audio.stopAudio(getCid()); - await super.stop(); + Future stop() async { + final didStop = await super.stop(); + if (didStop) { + await mediaStream?.dispose(); + mediaStream = null; + audio.stopAudio(getCid()); + + // update counter + await _trackCounterLock.synchronized(() async { + if (this is LocalAudioTrack) { + _localTrackCount--; + } else if (this is! LocalAudioTrack) { + _remoteTrackCount--; + } + await _onAudioTrackCountDidChange(); + }); + } + + return didStop; + } + + Future _onAudioTrackCountDidChange() async { + logger.fine('[$runtimeType] onAudioTrackCountDidChange: ' + 'local: $_localTrackCount, remote: $_remoteTrackCount'); + + final newState = _computeAudioTrackState(); + + if (audioTrackState != newState) { + audioTrackState = newState; + logger.fine('[$runtimeType] didUpdateSate: $audioTrackState'); + + NativeAudioConfiguration? config; + if (!kIsWeb && Platform.isIOS) { + // Only iOS for now... + config = await nativeAudioConfigurationForAudioTrackState.call(audioTrackState); + } + + if (config != null) { + logger.fine('[$runtimeType] configuring for ${audioTrackState} using ${config}...'); + try { + await configureNativeAudio(config); + } catch (error) { + logger.warning('[$runtimeType] Failed to configure ${error}'); + } + } + } + } + + static AudioTrackState _computeAudioTrackState() { + if (_localTrackCount > 0 && _remoteTrackCount == 0) { + return AudioTrackState.localOnly; + } else if (_localTrackCount == 0 && _remoteTrackCount > 0) { + return AudioTrackState.remoteOnly; + } else if (_localTrackCount > 0 && _remoteTrackCount > 0) { + return AudioTrackState.localAndRemote; + } + // Default + return AudioTrackState.none; } } + +Future defaultNativeAudioConfigurationFunc(AudioTrackState state) async { + // + if (state == AudioTrackState.remoteOnly) { + return NativeAudioConfiguration( + appleAudioCategory: AppleAudioCategory.playback, + appleAudioCategoryOptions: { + AppleAudioCategoryOption.mixWithOthers, + // IosAudioCategoryOption.duckOthers, + }, + appleAudioMode: AppleAudioMode.spokenAudio, + ); + } else if ([ + AudioTrackState.localOnly, + AudioTrackState.localAndRemote, + ].contains(state)) { + return NativeAudioConfiguration( + appleAudioCategory: AppleAudioCategory.playAndRecord, + appleAudioCategoryOptions: { + AppleAudioCategoryOption.allowBluetooth, + AppleAudioCategoryOption.mixWithOthers, + // IosAudioCategoryOption.duckOthers, + }, + appleAudioMode: AppleAudioMode.voiceChat, + ); + } + + // TODO: .record category causes exception in WebRTC lib for unknown reason + // if (this == AudioTrackState.localOnly) { + // return NativeAudioConfiguration( + // iosCategory: IosAudioCategory.record, + // iosCategoryOptions: { + // // IosAudioCategoryOption.allowBluetooth, + // }, + // iosMode: IosAudioMode.spokenAudio, + // ); + // } + + return NativeAudioConfiguration( + appleAudioCategory: AppleAudioCategory.soloAmbient, + appleAudioCategoryOptions: {}, + appleAudioMode: AppleAudioMode.default_, + ); +} diff --git a/lib/src/track/local_audio_track.dart b/lib/src/track/local_audio_track.dart index 830090c..75b80b0 100644 --- a/lib/src/track/local_audio_track.dart +++ b/lib/src/track/local_audio_track.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; -import '../errors.dart'; +import '../exceptions.dart'; import 'audio_track.dart'; import 'options.dart'; @@ -15,8 +15,12 @@ class LocalAudioTrack extends AudioTrack { /// Creates a new audio track from the default audio input device. static Future create([LocalAudioTrackOptions? options]) async { - // try { + // TODO: have back up incase the options fail final stream = await rtc.navigator.mediaDevices.getUserMedia({ + // 'audio': { + // 'echoCancellation': true, + // 'noiseSuppression': true, + // }, 'audio': true, 'video': false, }); diff --git a/lib/src/track/local_video_track.dart b/lib/src/track/local_video_track.dart index ae8e106..c7197f6 100644 --- a/lib/src/track/local_video_track.dart +++ b/lib/src/track/local_video_track.dart @@ -1,6 +1,6 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; -import '../errors.dart'; +import '../exceptions.dart'; import '../logger.dart'; import 'options.dart'; import 'track.dart'; diff --git a/lib/src/track/remote_track_publication.dart b/lib/src/track/remote_track_publication.dart index bd0792e..fef7ba6 100644 --- a/lib/src/track/remote_track_publication.dart +++ b/lib/src/track/remote_track_publication.dart @@ -1,11 +1,9 @@ -import 'package:livekit_client/livekit_client.dart'; - +import '../events.dart'; +import '../extensions.dart'; import '../participant/remote_participant.dart'; import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_rtc.pb.dart' as lk_rtc; import 'track.dart'; -import '../extensions.dart'; - import 'track_publication.dart'; /// Represents a track publication from a RemoteParticipant. Provides methods to diff --git a/lib/src/track/track.dart b/lib/src/track/track.dart index af7ea5b..ceddb3e 100644 --- a/lib/src/track/track.dart +++ b/lib/src/track/track.dart @@ -1,13 +1,17 @@ +import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; -import 'package:livekit_client/src/classes/change_notifier.dart'; +import 'package:meta/meta.dart'; import 'package:uuid/uuid.dart'; +import '../extensions.dart'; +import '../logger.dart'; import '../proto/livekit_models.pb.dart' as lk_models; +import '../support/disposable.dart'; /// Wrapper around a MediaStreamTrack with additional metadata. /// Base for [AudioTrack] and [VideoTrack], /// can not be instantiated directly. -abstract class Track extends LKChangeNotifier { +abstract class Track extends DisposableChangeNotifier { static const cameraName = 'camera'; static const screenShareName = 'screen'; @@ -19,6 +23,10 @@ abstract class Track extends LKChangeNotifier { rtc.RTCRtpTransceiver? transceiver; String? _cid; + // started / stopped + bool _active = false; + bool get isActive => _active; + Track( this.kind, this.name, @@ -50,7 +58,33 @@ abstract class Track extends LKChangeNotifier { return cid; } - Future stop() async { - await mediaStreamTrack.stop(); + // returns true if started, false if already started + @mustCallSuper + Future start() async { + if (_active) { + // already started + return false; + } + + _active = true; + return true; + } + + // returns true if stopped, false if already stopped + @mustCallSuper + Future stop() async { + if (!_active) { + // already stopped + return false; + } + + try { + await mediaStreamTrack.stop(); + } catch (_) { + logger.warning('[$objectId] rtc.mediaStreamTrack.stop() did throw ${_}'); + } + + _active = false; + return true; } } diff --git a/lib/src/track/track_publication.dart b/lib/src/track/track_publication.dart index 4b51838..a476aad 100644 --- a/lib/src/track/track_publication.dart +++ b/lib/src/track/track_publication.dart @@ -1,3 +1,4 @@ +import '../support/disposable.dart'; import '../proto/livekit_models.pb.dart' as lk_models; import '../types.dart'; import 'track.dart'; @@ -8,9 +9,9 @@ import 'track.dart'; /// Base for [RemoteTrackPublication] and [LocalTrackPublication], /// can not be instantiated directly. -abstract class TrackPublication { - final String name; +abstract class TrackPublication extends Disposable { final String sid; + final String name; final lk_models.TrackType kind; Track? track; diff --git a/lib/src/track/video_track.dart b/lib/src/track/video_track.dart index 5da706b..12fe003 100644 --- a/lib/src/track/video_track.dart +++ b/lib/src/track/video_track.dart @@ -5,6 +5,7 @@ import 'track.dart'; /// A video track will notify when its mediaTrack has changed. class VideoTrack extends Track { + // rtc.MediaStream _mediaStream; VideoTrack( @@ -27,9 +28,12 @@ class VideoTrack extends Track { } @override - Future stop() async { - await super.stop(); - await _mediaStream.dispose(); + Future stop() async { + final didStop = await super.stop(); + if (didStop) { + await _mediaStream.dispose(); + } // _mediaStream = null; + return didStop; } } diff --git a/lib/src/transport.dart b/lib/src/transport.dart index e31f79d..f21cad4 100644 --- a/lib/src/transport.dart +++ b/lib/src/transport.dart @@ -5,13 +5,14 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'constants.dart'; import 'extensions.dart'; import 'logger.dart'; +import 'support/disposable.dart'; import 'types.dart'; import 'utils.dart'; typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer); /// a wrapper around PeerConnection -class PCTransport { +class PCTransport extends Disposable { final rtc.RTCPeerConnection pc; final List _pendingCandidates = []; bool restartingIce = false; @@ -20,11 +21,42 @@ class PCTransport { Function? _cancelDebounce; // private constructor - PCTransport._(this.pc); + PCTransport._(this.pc) { + // + onDispose(() async { + _cancelDebounce?.call(); + _cancelDebounce = null; + + // Ensure callbacks won't fire any more + pc.onRenegotiationNeeded = null; + pc.onIceCandidate = null; + pc.onIceConnectionState = null; + pc.onTrack = null; + + // Remove all senders + List senders = []; + try { + senders = await pc.getSenders(); + } catch (_) { + logger.warning('getSenders() failed with error: $_'); + } + + for (final e in senders) { + try { + await pc.removeTrack(e); + } catch (_) { + logger.warning('removeTrack() failed with error: $_'); + } + } + + await pc.close(); + await pc.dispose(); + }); + } static Future create([RTCConfiguration? rtcConfig]) async { rtcConfig ??= const RTCConfiguration(); - logger.fine('PCTransport creating ${rtcConfig.toMap()}'); + logger.fine('[PCTransport] creating ${rtcConfig.toMap()}'); final _ = await rtc.createPeerConnection(rtcConfig.toMap()); return PCTransport._(_); } @@ -35,39 +67,19 @@ class PCTransport { wait: Timeouts.debounce, ); - Future dispose() async { - logger.fine('${objectId} dispose()'); - // Ensure debounce won't fire - _cancelDebounce?.call(); - _cancelDebounce = null; + // @override + // Future dispose() async { + // super.dispose(); + // // Ensure debounce won't fire - // Ensure callbacks won't fire any more - pc.onRenegotiationNeeded = null; - pc.onIceCandidate = null; - pc.onIceConnectionState = null; - pc.onTrack = null; - - // Remove all senders - List senders = []; - try { - senders = await pc.getSenders(); - } catch (_) { - logger.warning('getSenders() failed with error: $_'); - } - - for (final e in senders) { - try { - await pc.removeTrack(e); - } catch (_) { - logger.warning('removeTrack() failed with error: $_'); - } - } - - await pc.close(); - await pc.dispose(); - } + // } Future setRemoteDescription(rtc.RTCSessionDescription sd) async { + if (isDisposed) { + logger.warning('[$objectId] setRemoteDescription() already disposed'); + return; + } + await pc.setRemoteDescription(sd); for (final candidate in _pendingCandidates) { @@ -84,6 +96,11 @@ class PCTransport { } Future createAndSendOffer([RTCOfferOptions? options]) async { + if (isDisposed) { + logger.warning('[$objectId] createAndSendOffer() already disposed'); + return; + } + if (onOffer == null) { logger.warning('onOffer is null'); return; @@ -116,6 +133,11 @@ class PCTransport { } Future addIceCandidate(rtc.RTCIceCandidate candidate) async { + if (isDisposed) { + logger.warning('[$objectId] addIceCandidate() already disposed'); + return; + } + final desc = await getRemoteDescription(); if (desc != null && !restartingIce) { @@ -127,6 +149,11 @@ class PCTransport { } Future getRemoteDescription() async { + if (isDisposed) { + logger.warning('[$objectId] getRemoteDescription() already disposed'); + return null; + } + // Checking agains null doesn't work as intended // if (pc.iceConnectionState == null) return null; diff --git a/pubspec.lock b/pubspec.lock index 0d7a855..d1a0e23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -99,11 +99,11 @@ packages: dependency: "direct main" description: path: "." - ref: use-custom-webrtc-build - resolved-ref: "4942e7faec2e5775d35c42e22c2929ca6ca53769" - url: "https://github.com/livekit/flutter-webrtc" + ref: master + resolved-ref: "32d83d85fa2faebcd37a19534f8a84273b12cbce" + url: "https://github.com/flutter-webrtc/flutter-webrtc" source: git - version: "0.6.7" + version: "0.6.8" http: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5eb9b4a..0ace219 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,28 +16,21 @@ dependencies: uuid: ^3.0.4 synchronized: ^3.0.0 protobuf: ^2.0.0 - - flutter_webrtc: - git: - url: https://github.com/livekit/flutter-webrtc - ref: use-custom-webrtc-build - - # ^0.6.7 - # path: ../../repos_livekit/flutter-webrtc/ + + flutter_webrtc: # This will use custom webrtc build from # https://github.com/webrtc-sdk/Specs/releases - - # protobuf: - # git: - # url: https://github.com/google/protobuf.dart.git - # ref: master - # path: protobuf/ - - # WebSocketChannel has design flaws - # https://github.com/dart-lang/web_socket_channel/issues/25 - # web_socket_channel: ^2.1.0 + git: + url: https://github.com/flutter-webrtc/flutter-webrtc + ref: master dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.4 + +flutter: + plugin: + platforms: + ios: + pluginClass: LiveKitClientPlugin