From 3471fcf78bf6cec629b96984c8a4f6f8196cd63f Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 18 Apr 2020 13:50:38 +0200 Subject: [PATCH] Add player skeleton and playback service This introduces a dependency on audio_service and implements the playback service using that package. The UI was adapted to the new interface. --- android/app/build.gradle | 2 + android/app/src/main/AndroidManifest.xml | 15 ++ .../res/drawable-hdpi/ic_notification.png | Bin 0 -> 674 bytes .../src/main/res/drawable-hdpi/ic_pause.png | Bin 0 -> 140 bytes .../src/main/res/drawable-hdpi/ic_play.png | Bin 0 -> 272 bytes .../src/main/res/drawable-hdpi/ic_stop.png | Bin 0 -> 102 bytes .../res/drawable-mdpi/ic_notification.png | Bin 0 -> 485 bytes .../src/main/res/drawable-mdpi/ic_pause.png | Bin 0 -> 108 bytes .../src/main/res/drawable-mdpi/ic_play.png | Bin 0 -> 159 bytes .../src/main/res/drawable-mdpi/ic_stop.png | Bin 0 -> 92 bytes .../res/drawable-xhdpi/ic_notification.png | Bin 0 -> 1053 bytes .../src/main/res/drawable-xhdpi/ic_pause.png | Bin 0 -> 162 bytes .../src/main/res/drawable-xhdpi/ic_play.png | Bin 0 -> 288 bytes .../src/main/res/drawable-xhdpi/ic_stop.png | Bin 0 -> 114 bytes .../res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 1470 bytes .../src/main/res/drawable-xxhdpi/ic_pause.png | Bin 0 -> 202 bytes .../src/main/res/drawable-xxhdpi/ic_play.png | Bin 0 -> 547 bytes .../src/main/res/drawable-xxhdpi/ic_stop.png | Bin 0 -> 196 bytes .../res/drawable-xxxhdpi/ic_notification.png | Bin 0 -> 2317 bytes .../main/res/drawable-xxxhdpi/ic_pause.png | Bin 0 -> 244 bytes .../src/main/res/drawable-xxxhdpi/ic_play.png | Bin 0 -> 488 bytes .../src/main/res/drawable-xxxhdpi/ic_stop.png | Bin 0 -> 244 bytes lib/app.dart | 4 +- lib/backend.dart | 35 +-- lib/main.dart | 7 +- lib/player.dart | 249 ++++++++++++++++++ lib/screens/home.dart | 2 +- lib/screens/program.dart | 42 ++- lib/widgets/play_pause_button.dart | 6 +- lib/widgets/player_bar.dart | 2 +- pubspec.yaml | 1 + 31 files changed, 321 insertions(+), 44 deletions(-) create mode 100644 android/app/src/main/res/drawable-hdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-hdpi/ic_pause.png create mode 100644 android/app/src/main/res/drawable-hdpi/ic_play.png create mode 100644 android/app/src/main/res/drawable-hdpi/ic_stop.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_pause.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_play.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_stop.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_pause.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_play.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_stop.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_pause.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_play.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_stop.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_pause.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_play.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_stop.png create mode 100644 lib/player.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index b0d2158..5909b5d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -50,6 +50,8 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug + // See https://github.com/ryanheise/audio_service/blob/master/README.md#android-setup + shrinkResources false } } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8ec7429..e544927 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..a989e378d21ee1a11af5ab299b75ef328bb82dff GIT binary patch literal 674 zcmV;T0$u%yP)*4*XgkZZDj>o ziVD{MrM?|~m{(Sy6Ea*!(=fGdcpm!IWrP`tyJ7ef0!?1!Y!_;3* z0_`s&&}6*xCuY~E?ab*;mJw)9?99Eycw3FyGx(C|&X*CW17FdV7=K3d5V|S&j!lW~ zP8orE@iGqg!^FIBE};pRaT8->hv8MdVVc6IC)S;b`Rze;^Y9HDV*M&KztB=opowAB z5$nc^4>SCe{6?CAO&e|*A!QBR<2cv7W46+fcE z1&oaPIXtL#a1G3(PZ(EA^VXCY2lyv$74Una!OB=qlg+1{1^m~+|B4w!A)2s8HfQ=D z=XdDF{8%3;(01Zu|DncN*(XNTmoJ`%U!UY5NeL}D6@)x0cpKM?3mPgOMLHFBecuj#)Dx`4S^jpay|0-Cs?Av8n|0)`2pn)8I1AX&=B6u5A%K!iX07*qo IM6N<$g5uplmH+?% literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-hdpi/ic_pause.png b/android/app/src/main/res/drawable-hdpi/ic_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..55f33b25319b5aef092caeb23207bdc8637fcd08 GIT binary patch literal 140 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBf<0XvLn2z=UUTGPP~cz(#q8sy1`p(u}#?|$tO`(ZHsRK+bh;Pn| kzu}R0%jWwN<{!rx1O3`=oI3ka3urupr>mdKI;Vst0Ape?I{*Lx literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-hdpi/ic_play.png b/android/app/src/main/res/drawable-hdpi/ic_play.png new file mode 100644 index 0000000000000000000000000000000000000000..326a6aabc9ab1dae84f0a3521c76793a7203bdf4 GIT binary patch literal 272 zcmV+r0q_2aP)yRb1%qJYwDO)=8Z(6|k1|%Oc%$NbBbUj@hLn2z=p4-UFz`${M!@cuY z|1``vFm01MW5SYYMkyyIC_1zopr0E?L* Ap#T5? literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..cb6f9d70142a29f739cce25895dec3e1f219737c GIT binary patch literal 485 zcmVJVP!7u*>vU`svC*Wo+nvGotQO?Zb#*j*37QG5-O<2jfTMgsE`z4Z`W z#S%VYAqUfex%icD@`U6O+zE9Lu#CP8%n-(L10OI{wE3F&>|Gec=TKTsVA?Q+9-I&S zMU;vV>_s)!2k{~Xc_e{376h%RsKrE~^MKQM9P1J$@g<%cB0qzHSZ@!4Ti9HL;2I_p z?|bkK_plQu!uQQlFNYcKqCMTL1>NYw0c^t}&ZTfSaTy!Kl9g2bHJ+m@nQX=-yow=J zaUXB7Kh6a1dd=D9ppv zxs*X_0Rz{8U`DCV!vdQ^lNYpTdz4LDJ#WU7zbisEEb`{cn=@ll)2B4&`641QGcGP+ zi7(^vbgazgzBT2+>Bf)Kn=-nSocq>gzPYeyz0$uoTK3NwqrxTJjcCpDi pxPo=DSWlD#H@DkC4Fx_1hB+sE7cO8c{|Hph;OXk;vd$@?2>`@N7=8c% literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..3ef9bc59611ed434589d63d777fc38c5967acce2 GIT binary patch literal 1053 zcmV+&1mgRNP)V0|K62h! z6lWzzi}6!*_7_eofO8Rkjhs&v5oa}LXQSjWChZWLGJJ0r5oa}Lt4zeFIC&C$%kX=K zZ%2{w<~h7M5ipKh(0&wL-FPf=l(8REnh|^1=|_&lW}J8wT+8rv4qsn0VsFC~9Ksm( zWE}p9GaBUT!jO;%4kQB3YnFf>;Q&93jXA4O_clTwMz9liCtNo+OTdaa+xOwjoTaZj zrDAR58p4GM&y&p(@ErcY9m193)x`1p^{fKsVi&ezApXy$=FgfDd^>jIXIv56rY8c% z&|e4FGeYKhx-2+ymvN{W0_F$@cLx?`+TY4JF5!7}RN+}DWS$>!YNq|GWHJi7XO$fc zU@wNV+XR;jnW>EJII99jhj3sI3i&_F^IjrgNznpsh>kx<+IvjQNaMJ^0>|n2GOpD8 zq;Eqa;F2PPZx;@539se!-F;%9MO~eY z=@9PwqZr8HxGb@Myhs7PcqbB{RAJ2eB!Dq=W%`HW_Id!*a_pB1TeOVVF|{ZG=ioaW zs4}KI*+vf~{l7(yRTbfwfip$(BNrSj1?%iK%*l!Zf$sUg)@8W+-AZzBStkNN0qVEjCx_AHOT zm6%)0Abl35*lrRZuKpF^T%n-7D;%6H!WkKCWCs6p2;Y5X3KhC)-fp3V7FuZGe}}&T Xr-1!s)jEs<00000NkvXXu0mjfBlo1`?|x>U_dm*KlFIf!rg|S_i!51Z0xf0mboFyt=akR{0O1BU@&Et; literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_play.png b/android/app/src/main/res/drawable-xhdpi/ic_play.png new file mode 100644 index 0000000000000000000000000000000000000000..62d2067cbda361605dcc65ac2536f3a934599c68 GIT binary patch literal 288 zcmV+*0pI?KP)tjG*Oq5r}mI zRznAm^dS&i?h%*{8^1q+YRlb7z(BzDE9CYgcHKtZ{>a>p60V0ix8t3XSm mdscCM*m8eb6ha6gL|Z zxd+=7CONQYFfkl((M>1k4ElFd;F417Cjxw_7~BmEEwTQ$ivIPb)V2cH%Rp`~q4sSN?S-Y%wg%{nnA}_< z-C@9vh;~aUw50R8|PilBRKf^OMH^aB7c zPcRB_zh})V;Hw19**-O$0d7@Hbs5Zl?pbpx;w>4+!#*{g0bW*2HOCm9;YqI=_(I0) z2OeSYYaZZrA9|Sqwo5~MfJ)$?p+N0;c;y34O3W?5IN-2GZ@Z8DvYKrGdZnRt^4MjD zU$h4}+k@s?O3a&q5kRL#ZwFA9JUR%Wo!kk)9Ca8jXv^p2KZPpG$nHGK|_H$fZCKgmB1!>eGX`joPXA!)#M{j zXMkT6Gs7e28WE3zj#SeRV1{z-_nPwy3_1^G&CwZPD1|kk{f0I#7>w@%hNaM2sa(5A zbAGBp=Z!2mW&)U-WCR$6n2SZ}2d=Qxxq!k@Vy~{Sh&N|+!4Aqfs|Q|FjND{sQvrNn z_;ovgY73ow2b=@$jA^&epp&Y<^MXwLfMQ}yOxp_$#vx&G9q|}<%eLLX*qFAa$?I8J za&-bRywAUK4_UO|WH7%z=9&s%z0&sH1dVqM8l``eAv(nq(!fs^?dQkbMcO?q;C!Xc zP796YhWO1%*Lnw-jp!2qKCx(DkG``8@n}?Ou6a>u(`uoy#1Q{6*F2p8o>B~~w!G#R zrKl>O5$UWEtCYv&`7UDmIOQ7C^yU@UJe>hPRSZ0CdCf=$&$@eoOEqoVlr~RT#=pi; zpbyjLmkGoSx>qr9U5aa0Ma-`?v{ToV&b5p^-B6(2w*G8Hsu6Ffi5Qj&7`WsBFfm2^ zMhcG(z?GUd%arr)VY$c#oMtFcA6dQ=kj1zM_=v%y(N3ThmzUY5eE8IIYk8IO93RP#nF8iRl@@OUm(W4!Uz?yg>svU@?V`JaylJ zd5Pt&w1LI|n=#L}O`HIJQ~qD!kVHN34)Tsvj*n;HuE-pR*y_Mir7ca6+L%CTaoROS zGY12nR^o1TjB8pw*oy6j0phXp#eA1U48GLK<~+w3_ZfV)6S-sKf<(Wi$-*p`L^PgS zm6$!G=$s>Qt}^)CkIkD580)b(0Oz?R67e*e!nh|#!lX1I)-?3}>Tzs}j+jIFB8SBk zz{1mwO3Y4DtTbw3O~Je+oxHP4{Mt-fI>@+FikN=EECFF7 Y-tKfdBvi literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_pause.png b/android/app/src/main/res/drawable-xxhdpi/ic_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..8ac598d28b8b89a4108e0a0c664a26a65bae125f GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!6FprVLn`LHyX)fztF77kYKrImW<<2zpAblrB^ zCk2$MiyPu=`s43_`q;z^T*jVyt+m6bfKo_ZJhH>TrvC*rTwibzdn@%=)g%G+4OY7q zoT<}hJ(C8Me!I`OX$Q?s(*!ge)H58z?(!{{HA_ILI=GD^#k(%3NkE%e$6H(|-FPWY z0vfIj+`-|p?dLbIfcp4}H@JoaB?pnyJOfJA!xJ1WIF4*`1eEG4u3}%Y(PWS_pfoo< z#raYLnz!5mrPZ#3yB2V6DiP3dP%m-H0&Or$29&Cc`#4@;e2vuzsBf?(c#kXjhuBQb zfIb*chcJ>DM@CfeznFE%R%5AeQ-mDmIE+)Hhtmp<<)SAV)y!1}Bb71;>$1 z9CwT{&jYi2w#ipBtm%>K4}8R>(v6oAhxTL4dhPQZC+&dyv1u<3J6N_rJK?6{>UoSw z0$MdVme^mZ$Evn^{?fx^@%&|EIPrk$w^2{$cNA@eFv36`T+c l$wLSsgb+dqA%qax;1}a#b8*acw$T6p002ovPDHLkV1k7r|C;~+ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stop.png b/android/app/src/main/res/drawable-xxhdpi/ic_stop.png new file mode 100644 index 0000000000000000000000000000000000000000..17da4a363780a5c933e8bec01e68b0e9f9a88fc4 GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!-JULvAr*7pUO33fpuoUv_`EzT z{l#imp&hL6WPs8P4QHQ*QCtuOTu>C;g0hxGjNppM76sx~ zL|i~w0tARcg32aEHd|VXLTRC;Z~K1y&tM`p#-`YC(0a%truf;qVrblIA~oxk*xhc%1po$m@#N=Suhh1r*z(r-3P6!C$Kec%uK*AzAtdIGP-sx}hF|Zk98$6Sv@zg=<>j|7wFaa=o3~0QlK;MOf*4GpGeaQq|i;V`T zZx8&pNZ+8e^^9Ow|GQ)Yp1|7%q;CSwmA^;9y@SwJ3xWw+#%jzl0KW_N#Jwf@p6$1; z+Q6(1CIBA7^ge5wgwJC|2RF#3{nS<)$~WM2C;MuWVoTiDp}z5X7!!Oco$^n8T!!$; z3fhk8m#*5t`dV%4r5$wixISrnh4!cOJkIWiu38ad0O>(L4~t4V`hY%|q(*yZe^a7s z{F$PdjpeCM9Ei`Abac6f4`$c`|4OyF6t5D@;XLLwsi_eJ&*D(hfrZ#x2u(g#(9vPc z)ld`)>m)b9i%EN{h1kTpxVc2vdQE6*1i{4nc_N%(ZS0)v}` zI5hd~k^)Ue^p6X$EpAJR|DzrzlKg&p$pjoyr=CW^ z3_O?;f4O8?5gzyROFH`~wys&z6x@(%<2lSq{eD!5rsM0>(@6je@EE4npy!@coy)Ojn{wMIHbw8jw!LWS3xbpD{*>?suz60|{k0y} zvMj@=uy&VPMl7G_QgvRT&1oA`aaDnqhp<;SI{J!`1@U4^_$@^PXmrX-nqge8ON&T| zN6fY6JhDh|k&n`Y4e;#(El(N`rYPQ(5&nac@*h;}lYbV6cd6ywcs^Cvb@HFa2{^4l z$A63muc*;6fRjtg?}|GMIu4hq=%f=YtzV|thf2wIQ&WX!!b z`)W;$ZO0{;P*Tr^S}m)U+8hLPuAOf5lauCe*uK*~^&Mfe zOj9WXS~#H=@i_J=spAMdox0}F*rvnwr{Yfq?JvTvRr=I7g!TXL8Nsv4uWYGf23{%{ zfEB`qy>8C+Q-!5$WcgW&6Lq+83Z`gLcyb8)Rq0dT5FXzJ8Nro~?t;_o7X<@AcuKa) zxqgOLhuWQ(R#C(E6bFpPX0|jA%XEIB>bCpZc(44YV5x$01%8q$JA|{k=*L$S??q=+ zt+Ou(vDWm!-L8w+uHco-+X(N$b~)F*8cR}TpVmdTo!~nv@=yJ&YMqTEn2|r!YWiKF z3-_(Cm(0=rVD{;P>#oa`J-^Go9HP~imF2J9)Y~Z79KX#7{kkqsjd;D$FH9`Rs}FC| zx^6GiWiUrb*eyyTd!d_pJ85{LU`0$5Qb-OK0vWTDyJ8>gpuH=15Bw!1a#(P(b>&7* z>ik`*z9Bg=S*#}n*VAlV+vh+rLt9TJ1bCz{zCI zPnBJQgL~98Go|A$Y>`v$P_6GEEYL$*i(raot+_0Rb>Fkr#x@o*6dqD+f2SE1{5|*K ztAZc>Ls~6|NAzefCj)V#upyqH%?Vh@KmESe<>xXzrjz6Dk}>#VFZGW!GEF%34VN@l z#uNBM=6`o!ml`x4k_^UfIlu3s^~qa|qiSfkC^80D_1eHRVU6u|@bj3fhHoqgUo1Hi z>*V}?R;JyX)!-}>5{Zj=P`CS=uJhKidml5 zZL$C^P@I6&D?|G583N&)k!C(NsHyIEl=N|#ZoYLPceVG z9TbwZidc>hbW`!^ijUcKutbOc_ihmE{)@C)KJ(;wNV{xfqmn+Z#Mi1;ItAA>FyL7h zWB{A)!S4cWCq%fOQq4MAEv$`qYsd`zzd~4&<_H0rC*>z5 zy(r{IUV)DouOBiRiwI7x9fVlyEF6bZ@a5$1vp8JvM8<*d2SUEjgyb)-@sUU*5{X12 nkw_#Gi9{liNF)-8j2-?5?(y+1ktg=r00000NkvXXu0mjfXc&L= literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_pause.png b/android/app/src/main/res/drawable-xxxhdpi/ic_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..43435028fb0b360f82113f2795388eb8933877b6 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoyFFbTLn`LHy|aTPv4n7St t7z`Pcn2yj`;Kq}bC7Y*0+`|y_nmOgd0j4?6z7>K*JYD@<);T3K0RV2!OEv%i literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_play.png b/android/app/src/main/res/drawable-xxxhdpi/ic_play.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f92814f0b5a291a1fdaff85dcf0673429fe940 GIT binary patch literal 488 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U|jF%;uumf=j|OseHKTVwvXyU zQd(YFb_`ixOsulDObL0%#rb)qX!;~&OUpO!cIVBD|MWinz3;nwx97@EQ1P4uMJw&6 z9|)@w)Sb;F{J>&QUz%^@C&Tgser1+#XYwy#&_5u#VOmUx_dd3O=ZrVF*R7W9X3{vI zUBH-gBfg!xSrSs;I*2o#3BUF=ar*!NrJ@Z}zfRx9w;=4A_L?=`FO?g9o~p9AG4)Sy zEzmwO?blAuECCxf>6wHul)l@zsm`c`p*1qq_V_8T6rjx=Q%$8D4xWAwFL8uV*WSZ%3nSu`U*u+N}>?G!{X cuU%5UFVdQ&MBb@0Mo$L?EnA( literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stop.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stop.png new file mode 100644 index 0000000000000000000000000000000000000000..20ee1b7bca560084d90d681473ea48ea7f0cd1b4 GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoyFFbTLn`LHy?BuGfB^&Z#xwU< zT07gDx~uG9eNzmShk$+6kDjh&FLBtj=W7FB$Mo%T7Z^W&y2fm!5O=T2f&IwoX#N)s u6_u|UWd-!}Z5J@pO7VlQ>@XKG{I6nMUv#ATdu@KbLh*2~7aec3kNI literal 0 HcmV?d00001 diff --git a/lib/app.dart b/lib/app.dart index 6936c52..fc77eb2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -93,13 +93,13 @@ class _ContentState extends State with SingleTickerProviderStateMixin { super.didChangeDependencies(); backend = Backend.of(context); - playerBarAnimation.value = backend.playerActive.value ? 1.0 : 0.0; + playerBarAnimation.value = backend.player.active.value ? 1.0 : 0.0; if (playerActiveSubscription != null) { playerActiveSubscription.cancel(); } - playerActiveSubscription = backend.playerActive.listen((active) => + playerActiveSubscription = backend.player.active.listen((active) => active ? playerBarAnimation.forward() : playerBarAnimation.reverse()); } diff --git a/lib/backend.dart b/lib/backend.dart index 8d8141c..b36a4fb 100644 --- a/lib/backend.dart +++ b/lib/backend.dart @@ -8,10 +8,10 @@ import 'package:moor/moor.dart'; import 'package:moor_ffi/moor_ffi.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart' as pp; -import 'package:rxdart/rxdart.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'database.dart'; +import 'player.dart'; // The following code was taken from // https://moor.simonbinder.eu/docs/advanced-features/isolates/ and just @@ -81,9 +81,7 @@ class Backend extends StatefulWidget { class BackendState extends State { static const _platform = MethodChannel('de.johrpan.musicus/platform'); - final playerActive = BehaviorSubject.seeded(false); - final playing = BehaviorSubject.seeded(false); - final position = BehaviorSubject.seeded(0.0); + final player = Player(); BackendStatus status = BackendStatus.loading; Database db; @@ -109,11 +107,12 @@ class BackendState extends State { Future _load() async { _moorIsolate = await _createMoorIsolate(); final dbConnection = await _moorIsolate.connect(); + player.setup(); db = Database.connect(dbConnection); _shPref = await SharedPreferences.getInstance(); musicLibraryUri = _shPref.getString('musicLibraryUri'); - + _loadMusicLibrary(); } @@ -142,32 +141,6 @@ class BackendState extends State { } } - void startPlayer() { - playerActive.add(true); - } - - void playPause() { - playing.add(!playing.value); - if (playing.value) { - simulatePlay(); - } - } - - void seekTo(double pos) { - position.add(pos); - } - - Future simulatePlay() async { - while (playing.value) { - await Future.delayed(Duration(milliseconds: 200)); - if (position.value >= 0.99) { - position.add(0.0); - } else { - position.add(position.value + 0.01); - } - } - } - @override void dispose() { super.dispose(); diff --git a/lib/main.dart b/lib/main.dart index a09a429..9b389c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,13 @@ +import 'package:audio_service/audio_service.dart'; import 'package:flutter/widgets.dart'; import 'app.dart'; import 'backend.dart'; void main() { - runApp(Backend( - child: App(), + runApp(AudioServiceWidget( + child: Backend( + child: App(), + ), )); } diff --git a/lib/player.dart b/lib/player.dart new file mode 100644 index 0000000..0c4ac46 --- /dev/null +++ b/lib/player.dart @@ -0,0 +1,249 @@ +import 'dart:async'; + +import 'package:audio_service/audio_service.dart'; +import 'package:rxdart/rxdart.dart'; + +/// Entrypoint for the playback service. +void _playbackServiceEntrypoint() { + AudioServiceBackground.run(() => _PlaybackService()); +} + +class Player { + /// The interval between playback position updates in milliseconds. + static const positionUpdateInterval = 250; + + /// Whether the player is active. + /// + /// This means, that there is at least one item in the queue and the playback + /// service is ready to play. + final active = BehaviorSubject.seeded(false); + + /// Whether we are currently playing or not. + /// + /// This will be false, if the player is not active. + final playing = BehaviorSubject.seeded(false); + + /// Current playback position. + /// + /// If the player is not active, this will default to zero. + final position = BehaviorSubject.seeded(const Duration()); + + /// Duration of the current track. + /// + /// If the player is not active, the duration will default to 1 s. + final duration = BehaviorSubject.seeded(const Duration(seconds: 1)); + + /// Playback position normalized to the range from zero to one. + final normalizedPosition = BehaviorSubject.seeded(0.0); + + /// The current position in milliseconds. + int _positionMs = 0; + + StreamSubscription _stateStreamSubscription; + StreamSubscription _mediaItemStreamSubscription; + + /// Update [position] and [normalizedPosition] according to [_positionMs]. + void _updatePosition() { + position.add(Duration(milliseconds: _positionMs)); + normalizedPosition.add(_positionMs / duration.value.inMilliseconds); + } + + /// Set everything to its default because the playback service was stopped. + void _stop() { + active.add(false); + playing.add(false); + position.add(const Duration()); + duration.add(const Duration(seconds: 1)); + normalizedPosition.add(0.0); + _positionMs = 0; + _stateStreamSubscription.cancel(); + _mediaItemStreamSubscription.cancel(); + } + + /// Start playback service. + Future start() async { + if (!AudioService.running) { + await AudioService.start( + backgroundTaskEntrypoint: _playbackServiceEntrypoint, + androidNotificationChannelName: 'Musicus playback', + androidNotificationChannelDescription: + 'Keeps Musicus playing in the background', + androidNotificationIcon: 'drawable/ic_notification', + ); + + setup(); + } + } + + /// Connect listeners and initialize streams. + void setup() { + if (AudioService.running) { + active.add(true); + + _stateStreamSubscription = + AudioService.playbackStateStream.listen((playbackState) { + if (playbackState != null) { + if (playbackState.basicState == BasicPlaybackState.stopped) { + _stop(); + } else { + if (playbackState.basicState == BasicPlaybackState.playing) { + playing.add(true); + _play(); + } else { + playing.add(false); + } + + _positionMs = playbackState.currentPosition; + _updatePosition(); + } + } + }); + + _mediaItemStreamSubscription = + AudioService.currentMediaItemStream.listen((mediaItem) { + if (mediaItem?.duration != null) { + duration.add(Duration(milliseconds: mediaItem.duration)); + } + }); + } + } + + /// Toggle whether the player is playing or paused. + /// + /// If the player is not active, this will do nothing. + Future playPause() async { + if (active.value) { + if (playing.value) { + await AudioService.pause(); + } else { + await AudioService.play(); + } + } + } + + /// Regularly update [_positionMs] while playing. + // TODO: Maybe find a better approach on handling this. + Future _play() async { + while (playing.value) { + await Future.delayed(Duration(milliseconds: positionUpdateInterval)); + _positionMs += positionUpdateInterval; + _updatePosition(); + } + } + + /// Seek to [pos], which is a value between (and including) zero and one. + /// + /// If the player is not active or an invalid value is provided, this will do + /// nothing. + Future seekTo(double pos) async { + if (active.value && pos >= 0.0 && pos <= 1.0) { + final durationMs = duration.value.inMilliseconds; + await AudioService.seekTo((pos * durationMs).floor()); + } + } + + /// Tidy up. + void dispose() { + _stateStreamSubscription.cancel(); + _mediaItemStreamSubscription.cancel(); + + active.close(); + playing.close(); + position.close(); + duration.close(); + normalizedPosition.close(); + } +} + +class _PlaybackService extends BackgroundAudioTask { + static const playControl = MediaControl( + androidIcon: 'drawable/ic_play', + label: 'Play', + action: MediaAction.play, + ); + + static const pauseControl = MediaControl( + androidIcon: 'drawable/ic_pause', + label: 'Pause', + action: MediaAction.pause, + ); + + static const stopControl = MediaControl( + androidIcon: 'drawable/ic_stop', + label: 'Stop', + action: MediaAction.stop, + ); + + static const dummyMediaItem = MediaItem( + id: 'dummy', + album: 'Johannes Brahms', + title: 'Symphony No. 1 in C minor, Op. 68: 1. Un poco sostenuto — Allegro', + duration: 10000, + ); + + final _completer = Completer(); + + int _position; + int _updateTime; + bool _playing = false; + + void _setPosition(int position) { + _position = position; + _updateTime = DateTime.now().millisecondsSinceEpoch; + } + + void _setState() { + AudioServiceBackground.setState( + controls: + _playing ? [pauseControl, stopControl] : [playControl, stopControl], + basicState: + _playing ? BasicPlaybackState.playing : BasicPlaybackState.paused, + position: _position, + updateTime: _updateTime, + ); + + AudioServiceBackground.setMediaItem(dummyMediaItem); + } + + @override + Future onStart() async { + _setPosition(0); + _setState(); + await _completer.future; + } + + @override + void onPlay() { + super.onPlay(); + + _playing = true; + _setState(); + } + + @override + void onPause() { + super.onPause(); + + _playing = false; + _setState(); + } + + @override + void onSeekTo(int position) { + super.onSeekTo(position); + + _setPosition(position); + _setState(); + } + + @override + void onStop() { + AudioServiceBackground.setState( + controls: [], + basicState: BasicPlaybackState.stopped, + ); + + // This will end onStart. + _completer.complete(); + } +} diff --git a/lib/screens/home.dart b/lib/screens/home.dart index ecf5a23..c7ebf2f 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -34,7 +34,7 @@ class HomeScreen extends StatelessWidget { ], onSelected: (selected) { if (selected == 0) { - backend.startPlayer(); + backend.player.start(); } else if (selected == 1) { Navigator.push( context, diff --git a/lib/screens/program.dart b/lib/screens/program.dart index 5eed825..6d0fb97 100644 --- a/lib/screens/program.dart +++ b/lib/screens/program.dart @@ -26,7 +26,7 @@ class _ProgramScreenState extends State { positionSubscription.cancel(); } - positionSubscription = backend.position.listen((pos) { + positionSubscription = backend.player.normalizedPosition.listen((pos) { if (!seeking) { setState(() { position = pos; @@ -56,7 +56,7 @@ class _ProgramScreenState extends State { }, onChangeEnd: (pos) { seeking = false; - backend.seekTo(pos); + backend.player.seekTo(pos); }, onChanged: (pos) { setState(() { @@ -68,7 +68,16 @@ class _ProgramScreenState extends State { children: [ Padding( padding: const EdgeInsets.only(left: 24.0), - child: Text('4:00'), + child: StreamBuilder( + stream: backend.player.position, + builder: (context, snapshot) { + if (snapshot.hasData) { + return DurationText(snapshot.data); + } else { + return Container(); + } + }, + ), ), Spacer(), IconButton( @@ -83,7 +92,16 @@ class _ProgramScreenState extends State { Spacer(), Padding( padding: const EdgeInsets.only(right: 20.0), - child: Text('10:30'), + child: StreamBuilder( + stream: backend.player.duration, + builder: (context, snapshot) { + if (snapshot.hasData) { + return DurationText(snapshot.data); + } else { + return Container(); + } + }, + ), ), ], ), @@ -99,3 +117,19 @@ class _ProgramScreenState extends State { positionSubscription.cancel(); } } + +class DurationText extends StatelessWidget { + final Duration duration; + + DurationText(this.duration); + + @override + Widget build(BuildContext context) { + final minutes = duration.inMinutes; + final seconds = (duration - Duration(minutes: minutes)).inSeconds; + + final secondsString = seconds >= 10 ? seconds.toString() : '0$seconds'; + + return Text('$minutes:$secondsString'); + } +} diff --git a/lib/widgets/play_pause_button.dart b/lib/widgets/play_pause_button.dart index 84703d8..9202201 100644 --- a/lib/widgets/play_pause_button.dart +++ b/lib/widgets/play_pause_button.dart @@ -30,13 +30,13 @@ class _PlayPauseButtonState extends State super.didChangeDependencies(); backend = Backend.of(context); - playPauseAnimation.value = backend.playing.value ? 1.0 : 0.0; + playPauseAnimation.value = backend.player.playing.value ? 1.0 : 0.0; if (playingSubscription != null) { playingSubscription.cancel(); } - playingSubscription = backend.playing.listen((playing) => + playingSubscription = backend.player.playing.listen((playing) => playing ? playPauseAnimation.forward() : playPauseAnimation.reverse()); } @@ -47,7 +47,7 @@ class _PlayPauseButtonState extends State icon: AnimatedIcons.play_pause, progress: playPauseAnimation, ), - onPressed: backend.playPause, + onPressed: backend.player.playPause, ); } diff --git a/lib/widgets/player_bar.dart b/lib/widgets/player_bar.dart index 014555a..b800390 100644 --- a/lib/widgets/player_bar.dart +++ b/lib/widgets/player_bar.dart @@ -16,7 +16,7 @@ class PlayerBar extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ StreamBuilder( - stream: backend.position, + stream: backend.player.normalizedPosition, builder: (context, snapshot) => LinearProgressIndicator( value: snapshot.data, ), diff --git a/pubspec.yaml b/pubspec.yaml index 2f56cb5..061dfc9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: sdk: ">=2.3.0 <3.0.0" dependencies: + audio_service: flutter: sdk: flutter moor: