From 4fc91cec3e0de9b2673b81cb572f4ed7b7da8f41 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 19 Oct 2026 15:22:34 +0100 Subject: [PATCH] feat: add authenticated live podcasts and simplify native video playback Include podcast capture, ingestion and replay, dependency and installer updates, and native video positioning after navigation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 17 +- Directory.Packages.props | 58 +- INSTALL.md | 64 +++ NuGet.config | 1 + ROADMAP.md | 12 +- contrib/blog-edit.png | Bin 61533 -> 0 bytes contrib/blog-view.png | Bin 65768 -> 0 bytes contrib/{Windows => mswin}/postit.nsi | 0 doc/architecture/postit.md | 63 ++- doc/testing.md | 24 + src/PostIt/Directory.Packages.props | 10 +- .../PostIt.Tests/PodcastNativeCaptureTests.cs | 166 ++++++ src/PostIt/PostIt.Tests/PodcastTests.cs | 163 ++++++ .../Helpers/ServiceCollectionHelpers.cs | 4 + src/PostIt/PostIt/PostIt.csproj | 1 + src/PostIt/PostIt/Services/CameraStreamer.cs | 23 +- .../PostIt/Services/HlsCapturePlaylist.cs | 43 ++ .../PostIt/Services/PodcastBroadcaster.cs | 231 ++++++++ .../PostIt/Services/PodcastMediaInput.cs | 78 +++ src/PostIt/PostIt/Services/YavscApiClient.cs | 7 +- src/PostIt/PostIt/ViewLocator.cs | 1 + .../ViewModels/Blogs/VideoPageViewModel.cs | 7 +- .../ViewModels/Layout/HomePageViewModel.cs | 9 + .../ViewModels/PodcastsPageViewModel.cs | 276 ++++++++++ src/PostIt/PostIt/Views/Blogs/VideoPage.axaml | 4 +- .../PostIt/Views/Blogs/VideoPage.axaml.cs | 87 +-- src/PostIt/PostIt/Views/Layout/HomePage.axaml | 5 + src/PostIt/PostIt/Views/PodcastsPage.axaml | 55 ++ src/PostIt/PostIt/Views/PodcastsPage.axaml.cs | 25 + src/Yavsc.Abstract/Streaming/Podcast.cs | 28 + src/Yavsc.Api.Client/PodcastApiClient.cs | 51 ++ .../Controllers/Musical/PodcastController.cs | 106 +++- .../Middleware/PodcastExceptionFilter.cs | 21 + src/Yavsc.Api/Podcast.http | 49 ++ src/Yavsc.Api/Program.cs | 1 + .../PodcastServiceCollectionExtensions.cs | 14 + .../Controllers/FileSystemStreamController.cs | 3 +- src/Yavsc.Server/Interfaces/ILiveProcessor.cs | 9 + src/Yavsc.Server/Services/LiveProcessor.cs | 510 ++++++++++++------ src/Yavsc.Server/Services/PodcastException.cs | 6 + src/Yavsc.Server/Settings/PodcastOptions.cs | 11 + .../ViewModels/LiveCastHandler.cs | 110 +--- .../Fixtures/ApiWebServerFixture.cs | 11 +- .../Yavsc.Api.Tests/LiveProcessorTests.cs | 267 +++++++++ .../Yavsc.Api.Tests/PodcastControllerTests.cs | 101 ++++ 45 files changed, 2342 insertions(+), 390 deletions(-) delete mode 100644 contrib/blog-edit.png delete mode 100644 contrib/blog-view.png rename contrib/{Windows => mswin}/postit.nsi (100%) create mode 100644 src/PostIt/PostIt.Tests/PodcastNativeCaptureTests.cs create mode 100644 src/PostIt/PostIt.Tests/PodcastTests.cs create mode 100644 src/PostIt/PostIt/Services/HlsCapturePlaylist.cs create mode 100644 src/PostIt/PostIt/Services/PodcastBroadcaster.cs create mode 100644 src/PostIt/PostIt/Services/PodcastMediaInput.cs create mode 100644 src/PostIt/PostIt/ViewModels/PodcastsPageViewModel.cs create mode 100644 src/PostIt/PostIt/Views/PodcastsPage.axaml create mode 100644 src/PostIt/PostIt/Views/PodcastsPage.axaml.cs create mode 100644 src/Yavsc.Abstract/Streaming/Podcast.cs create mode 100644 src/Yavsc.Api.Client/PodcastApiClient.cs create mode 100644 src/Yavsc.Api/Middleware/PodcastExceptionFilter.cs create mode 100644 src/Yavsc.Api/Podcast.http create mode 100644 src/Yavsc.Api/Services/PodcastServiceCollectionExtensions.cs create mode 100644 src/Yavsc.Server/Services/PodcastException.cs create mode 100644 src/Yavsc.Server/Settings/PodcastOptions.cs create mode 100644 src/test/Yavsc.Api.Tests/LiveProcessorTests.cs create mode 100644 src/test/Yavsc.Api.Tests/PodcastControllerTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 970efdda5..7fd31746e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog -## [1.0.8-rc27] - unstable +## [1.0.8-rc28] - unstable + ### Added @@ -10,6 +11,14 @@ L'auteur peut terminer ou supprimer ses emissions ; stockage prive sur disque, quotas dedies et conservation des segments recus apres interruption. +### Changed + +### Fixed + +## [1.0.8-rc27] - unstable + +### Added + * [PostIt.Desktop] Installeur NSIS Windows x64 par utilisateur, sans elevation : publication autonome, raccourci du menu Demarrer, protocole `postit://` pour la connexion et desinstallation via Windows. @@ -52,12 +61,6 @@ ### Fixed -* [PostIt] Le playback Linux revient sur un chemin logiciel sûr quand - VAAPI/VDPAU n'est pas validé, sans bloquer la lecture si le flux reste - lisible. Le fallback navigateur reste actif pour les médias impossibles - à lire dans le contrôle VLC, sans se baser sur l'absence de `vainfo` - ou de pilotes VAAPI. - * [Yavsc.Server] Reception WebSocket historique : correction du tampon reutilise, suppression de la file non synchronisee et liberation des connexions. Les podcasts utilisent desormais une ingestion HTTP bornee. diff --git a/Directory.Packages.props b/Directory.Packages.props index 1e3577c72..12028b161 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,8 @@ - - + + @@ -15,35 +15,35 @@ - - - - + + + + - + - - - + + + - - - - - + + + + + - - + + - - - + + + - - - - + + + + @@ -53,15 +53,15 @@ - - - + + + - + - + diff --git a/INSTALL.md b/INSTALL.md index a836c6611..62b7c15b6 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -76,6 +76,70 @@ login callback, updating and uninstallation on Windows with a standard (non-administrator) account. Installer compilation alone does not validate these Windows runtime behaviours. +## Podcast live broadcasts and replays + +PostIt Desktop's home page provides **Podcasts : directs et replays**. +Install LibVLC 3 with the `livehttp`, MPEG-TS, x264 and AAC plugins on the +Desktop machine. Windows capture uses DirectShow; Linux uses V4L2 and the +default PulseAudio microphone. FFmpeg is **not** a runtime dependency. +Select a camera and optionally a microphone, enter a title, then start. +Stopping publishes the retained replay; navigating away or logging out +stops capture too. A second signed-in PostIt instance can watch the live +broadcast while it is being captured. + +Both upload and playback require a bearer token with the `api` scope. +All authenticated API users can watch the catalogue, live broadcasts and +replays; only the broadcaster can upload, complete or delete their podcast. +Media is not anonymous and no access token is included in a playback URL. + +Configure Yavsc.Api's `Podcast` section (or environment variables such as +`Podcast__RootPath`). No database migration or server-side encoder is needed: + +```json +{ + "Podcast": { + "RootPath": "/srv/yavsc-private/podcasts", + "MaxSegmentBytes": 8388608, + "MaxOwnerBytes": 1073741824, + "MaxSegments": 7200, + "MaxOwnerPodcasts": 100, + "IdleTimeoutSeconds": 60 + } +} +``` + +`RootPath` must be absolute, writable by Yavsc.Api and **outside any static +file directory**. The default is `DataDir/podcasts` under the API application +base directory. Use a persistent volume and back up this entire directory +(indexes and segments together). This implementation supports **one API +process per storage root**, not multiple writers or replicas. + +Defaults allow 8 MiB per segment (also the HTTP endpoint's hard limit), +1 GiB retained per owner, 7,200 segments per episode and 100 episodes per +owner. At the target two-second segment duration, this is roughly four hours +per episode; the byte quota can be reached earlier. Podcast quotas are +separate from the legacy user-files `DiskQuota`. Deleting an ended podcast +releases its podcast quota. Reverse proxies must allow an 8 MiB PUT body +and forward Authorization on every API request. + +A broadcast without a new committed segment for 60 seconds, or present +when the API restarts, becomes an interrupted replay on the next catalogue, +manifest or upload request. Already committed media remains playable. +The broadcaster retries transient upload errors twice using the same +sequence and content; it stops if capture fails or the pending local media +exceeds 64 MiB. Unacknowledged media is retained under +`%LOCALAPPDATA%\PostIt\podcast-capture` (Linux: the corresponding local +application data directory), with `podcast.json` and the capture playlist +for manual recovery. There is no automatic resumption after an application +restart; these private local recordings may be removed manually when no +longer needed. + +The replay is **HLS/MPEG-TS**, not an MP4 download. PostIt's authenticated +player joins a live broadcast up to three segments behind the edge and +reads a replay from its beginning; seeking is not implemented. Native +camera/microphone permissions, drivers and codecs must still be verified +on each target Windows/Linux machine. + ## Resources System resources needed to install. diff --git a/NuGet.config b/NuGet.config index c601d09b5..3f45ad414 100644 --- a/NuGet.config +++ b/NuGet.config @@ -19,5 +19,6 @@ + diff --git a/ROADMAP.md b/ROADMAP.md index 364b98bda..0a46fff5a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -109,14 +109,14 @@ Sous-jalons du `TODO.fr.md.archive` repris ici : Repris du `TODO.fr.md.archive`, jalon 1 (sélection) + jalon 2 (interface) : -- ☐ **Gestion du profil utilisateur** Simple & Prestataire, sur **Android** (Web ✔) -- ☐ **Gestion de l'espace de stockage personnel** (Web, API web, Mobile) -- ☐ **Création d'une annonce** +- ✔ **Gestion du profil utilisateur** Simple & Prestataire, sur **Android** (Web ✔) +- ✔ **Gestion de l'espace de stockage personnel** (Web, API web, Mobile) +- ◐ **Création d'une annonce** - ☐ **Transformation annonce ↔ demande nominative** -- ☐ **Sélection d'une star** depuis le **mobile** (Web ✔) +- ◐ **Sélection d'une star** depuis le **mobile** (Web ✔) - ☐ **Sélection DJ / chanteur / formation** (recherche par critères : date, lieu, tendance, taille, répertoire) depuis le **mobile** - ☐ **Géocodeur de lieu** côté mobile -- ☐ **Géocodeur d'adresse postale** (Web + Mobile) +- ◐ **Géocodeur d'adresse postale** (Web + Mobile) - ☐ **Évaluation des trajets** - ☐ **Interface de gestion des collaborateurs** (sous-traitance — déjà amorcée dans `doc/Architecture.md`) - ☐ **Migration de l'identifiant utilisateur** : `string` GUID → `long` auto-incrémenté (Npgsql) @@ -163,7 +163,7 @@ Repris du `TODO.fr.md.archive`, jalon 1 (sélection) + jalon 2 (interface) : - ☐ **Aide à la rencontre physique** : carte interactive, geofencing, alertes - ☐ **Activités secondaires** : spécialisation d'une activité, activités filles, *activité principale* (code APE) - ☐ **Streaming vidéo pair-à-pair** -- ☐ **Streaming vidéo public** +- ✔ **Streaming vidéo public** **Critère de sortie** : un événement géolocalisé notifie ses participants confirmés à l'approche du point de rendez-vous. diff --git a/contrib/blog-edit.png b/contrib/blog-edit.png deleted file mode 100644 index 4175f51532e105ded21befef169015b1bcc9e0b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61533 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 diff --git a/contrib/blog-view.png b/contrib/blog-view.png deleted file mode 100644 index 04625dbc9d405bcd1a40207d53c49442a381a72b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65768 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 sv)0f7E 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⪼IlaI5Fr}OdJ04oJJ1+<45L6q3 z0iBY~x}NecJ)f;o1+2?Ys&N~Rlq`jyKv`WVDf%5!u+6lfgVTjS#e+r2%^G6Mk6pr( zi(D6m2#La}95$NYLPG%qT0TrU&WIDKHU44O226n4?KsP?cBB|75U;)>*7Fp6h4E!4 zD&lwj(D&%Vb`DsT1!4Vesp!;wMvkCBkmtOr+E8HOMyz@LU`tSVG8_EMuD@iNV;HEMp)0jLeu}wrA-6-_Pf{ z?>Enj=lAZJ7c!LA zeDi0y79l$seu;ADOR&zt(@DL{l~JeyO?(9RnlQoYDF35xxE_M>&w2moaN zd^nbSWSm4s8!4T+01mrIzuxqw;4=T6#W9xuX3I}9wS9P=Cmj(vXFh9W9FfH7$PYBm z@?Y%->-jl^ZoZo%%1?oyaI#eW9Pjl1HzcJk*Gi-?>i(`^ze1q*Li(1-s{s zKdpAeB&zbUM7>@&VhoyYWE|piAMSa&Q3U{x=BT+PPto%|q&pMd#b8I3+zqrvxZuRsL;E~DM z?+pO35aU;XkZAMomQ!HLE$!Tedv!Dw|9*3V`+Clsc>9OI^GrT75gho*NhD;|SMYx& zA-I3Ct+x$TPw+bUzw#TBBUsZPIHVb*&r1V)X$8NonEx+v8_v0zn*F@$o*n+S^=$gy??ts>dxEG1v*ZA|084pk?rPB zzRvzCNB7*P-0HlsGSzi!8~@=-upU>>n=icD0HFWvK4DdUPj4yc%K2E=AtPl;Cert^XMJSs;epMpI}lAl*#bzPxfm<2_v@ zHAF)<=f|*h-gWWdhER^vF$;@oB|bdPIRB-&kraJV%*tzdmqv=QDDb(WE^(__pypk} z?Ei#&ea)%hFx$zeA6Hzl(h|T!^{Nf~sgVmir|0vJh1kSX-AqNqQd6GX4KR zt3R$Ga)%Th!0p4UvVf|w&@KMMg;_$Y8&aQ+r%w>2olA#^t~@-o@8+Idydt)JsoUe_ zqIBXy&a%!!RaLuJA2Vb>xp3RL2R-fu9)2h=Xl>*lapxFq59I6jSX+};09-yZWsOV@ zPI+#-TArIcnS~3~D0T-PMHW5cy?Q05chJn_Wa`%yi<2P-&b_y`VSQ6rTRrqpCQZk* zx%_3`t4_RGZ9XgDpJGyya|_P|oWAeH0!ICW|AhiNu)6~GE{My2?v=#wHLUa$(r>%} zzVlq^`#-Q=UG%e*a@Dv$^AWSJaXn8?RO1|zxxPyeHovZ{6Z^lZ{REU8?_4sUXeFy= zMtT4s=d9;EXJ2)HD%+Q<=fnOidl&e0-1_?Etf@cliiNCGBMs8>$u6VsMfdD)*TK-%sML9V=7(pzGd{8*PCGg8;bvml|)qiu^`CkOB z$CFHHibAl<(zmT%AN7r9=C%(&%~!>3k>WO%Oy0FKv_NUp^tm!qsG$R0Zvby$5#7^x zVFJuoo)2Ox3n{m}_5tS?ooD1WJ(N@+rhAA@D!wFRAd zRpx*{Vo2dP6Qg~D3KlW+G}GM}8(HRzZMRnGJA3TynE>J2ONpGcm5t< zo!xVVBf{FD`-6pxj*R|lu}Fb+;Ya)<^&zLG$dLN2n6sCh6`XCeHt#y+FcIJna{U>z zw-wYrFj3!rS@T=}z-|?IxH)+EwL(S4US0)FZrkSbkmXO4&hdyoA4A%F#0ry_Z=L5w zm0_vxe=zlK7j<*pvOs-ExuBCf5%=kWVCdxFlRy>OzGYB#vB+re!+H~t0*hni9Cdr8 z`^qb@0GzK8Qg)tZ(cKk$ThoBuqM zaG7lo#lwX7BQFOkTuf&AD@Usg{z=^2kTx2YZ_R4<5sZBpqoY}H*X@hTOOV?%7qaLj z8?#UKv3C@DC!8EiPWg+a%{C-K)llor6w(Kl zu9r1JuUACOPTrkEAIwc3xY>vlJ?U?V7;{)qh`Dm-TC;f`CAynlT;L_)?BJ4f;^-x* zhhbB;L0$%bjPbOuTr$0fCNF$*f*Ov4W$abOlCI=BWAJ?RdUC%4vlxN{B7RB6w*&`{Vft$ z3cCO+x2bk+CnYGo+mG`sz5YswZk&P$@z6=QRy|l(#i)>3WF%hGhMv>N-x|07q-^Li zZrPKkgK8ewO9!yp-TM0N%e;}W=Kcff&mjlexpTRrzYF!rB>N;*vC)J7UqjRO+HrH2 zyB_TH;ok$-DQS|u-z@%*tp6u-=EA{?(FXf}#O}A_8z`C_b=&|y;vzm3eN?RRq872I+88EHMr213A96FBL@%P~-`v0o(H+ErFAWC?OJq)3dt}M3o zFt9x`<8TV5Y;qHZ%gDHh)nZHyG+;DST^ACXp!5+V98Dc~7O{b&!>8^8!n@fy!+z3R zCZ}PScHm7y$*`?720q6$9@k*w{%QhA|5I4b_(<{RWDy4SSN}asDuMK-OIq!|_c^aY zhlNgOQGDIXPW@~DAK!wC$20Mn9E?ZW#BL5{@OCOXlp(d=$Dq~v6`Exs7Bt}s@L!>+ z&rb75K{kW941+>9pC(V`qhMEM%ax6)X2)^gGRS5-&X_tY>$%QrK0_59*6wSb#+^Wm5~A zGNFbkbkClSJeHZM1xO@Wr_Oh?>jmEwqgAH0I7~9)%j2bWJ~vCPulRC+Nxbx?HhoWn z=*=J!QBu|x8NpE`1VjBIp*QG8p~I2=`FtU{!1r(*6`NHv4TADMQ&Cj)->iyp*j@Y7 z-d%&h%+UFpb1Np?*t!`Leh30x&}fktp$QK=9qInFO}7G@_q_jCD|7x#i3!tJDVFj( zCHTs+!gShw;5W$r8$l8H9QnrnSb8uoW0}y=upH*rZn+9`t0lI2chEqc%M)e{#1`?V zi*SA$$b>J$$ZxaNtqJ-~XPy3ue7QRwx=2`q(H1a~8KduzJHOiyTpJEI3}4f64qpqM zQ$09Fv41e+IlBZ4MO$m?Sd!cLCCc29FouJvTH_zha9jjvsewgDW2bYqhE&#zx?q%` zSgjr?v3!WLvObcUwVvhx-@UULzd%}q`TL?adQ>)rZI7|0T2i~!3vB+Yds=;6L!X{* zJzX`;U^k5*W`#A2YS2o0)az4`1VZQNi+-QjQ+$!g?M;d=;5ei1#z%u;H0=@iq;j{3 z?f1HFb28=)>XEqSF`xpU9u%Vv^QKCDOB8}R&-Cvkes2eT`x8@nHs6$Ex=irkCQ&hq zKClT@pcf(sSiiSGBc1ykK9h+*9S{xP!}%$&BDN(>PzcEEdQzFeoS@yA_DD{g>GXfkyi|7RvE zA%L&@VBJN0dqxWmquL#RVs8@Vq^ZpbEM>$Lcy?i{tUXwx|?lq$rWeE zSt&5ii1@`LgcO}OIWTuH+ZD;cz+&STS#6uCn_sRx`Y3pLf5lSS5QT8;g8szE`I=-> zE>6NfPgD-{hwKEkE`UxaKUTQp%Wj}kPwvpAV)9#{9w`y3lW!e5(j}& z2EhvZs$bfbW;9i!g<9wh)9~v1}ZHF=t;QzkilPeJ8h@g;4fd+48D_lvcFxqPP~LiH*@LG_4H7hSRDzL zIxX*(0Q}DhInR2d^r8f45E*RG5{2$TU?AZm5ugqD<8cQo^1}s(DaSA;sQFo z0C^DFvld<#EKRnee%Z2&M{UrqUdtfskZ?%Hk6|sAfa4kJ^)t3_>gt`De~h!){!)%V z!X;tEkQLe}Ce9uOlJaBpZxditS+gI$ZVUFiXDo_mva!!C?^kl9cr{Y^I2a2}Dl|5> z2&oDr=M`#(&t^QVjjEBI6v1!^N1_cf``uiN8$5=M*Fwc2($hW~a#|&Y(*XTcBG56>HHep70CQj#9 zNl%g|jndk|bd+Yg#}pbF_P}v?f@mt@MB4db_eZ9NF%#p)$5bf7-!|aFzl!1e@#8!E z(FTJZcxMsm7BE+?PMv-_+X!NF*e!Y0U=%jCqH!QN&1HB-u*QPiO^#F?ohX1j%f)i zzKH_+N&TcNlW;G=;eqoDp5T}*-&neGiBoHkT59*}_N^bYW>8X$6sQON^%R_^ab$<% zoZW37+~Y3j{-Pp6ig^e3*uGBr(BS6S+6s@!)7S|zR*Ms(pApabi&Snan!?M>oO|y& zXQ{~7x<<|)VysX3DivCy!RTWpmhDuSvqSnR`2}aDC?EwBaM9!bhs1mM6P_M^a#C zSL4s%4(?lf#xeOj0>gLS+>iEP5+670lF?Z#CrBHz=qzWC{7LecF=usYIuYY3&(qv3 z(`1lQuj$@@xHSSNl~zUFq^R7y;OC3`n7*(k0x&-{ufLCDeBTJJj-j-+p%wXHYP}Sk zOyWC1u)Me>o%(J}1(;W~r7@yUO&4)V1^DZOLkH(qIeadATH-(ps8XB^7DTccyH0g@SbI_WwY z)!w{-A=Qcn(bs>;mZv7ChxwTw*lX+3Yob0k%RhHyto87Pe^+Ko_gS_7jupZMUUKb! zQ_mK$s}Y@qRt6_uicaZ)Yene9SL*Grn?czF*u4Io`{p& zNL>zwRxi7yT*L?A;-j6%mGId%GSk?iwO*e1SF&H@-{E!{3?LHwHq^R5*Cn zh-tWUgZ`E7`6^sG6SC9bpdNATV(bsy{WAW)c?+rlJ09BGAk%FxGjlO+*SZXyN_u|g zde9Z)^UAdr-&Ou~r@vVPu9?Eg$>}x6dE@OdZg$+Wk4_J~-ba}}I?m3;9D{my!DGjc z2?=4Q0y0$GOwJrlQ*rC@bTE6_rEr9qpr*Y1Rzp~RZ>S0a;=(-=g=8Ebc$G%Mj+~C! z@8nzC|F@?xeL!@t!1wQQT2!%(iU1J<)|^##CNjZvzDi$u1TSYMZWN8y*~e7sE?xSu zKid8tVKB6jG42o^*!%H=c+;74=RbH5KbAikZ4YP~opo0fbZ{TjM&_?kQ<0*bGw?V^#$CbByE#qzl0E( z+xngd`D}eRLsQHD*LdLkAa-ENgdVRtM0)nRR?0L>eOc_1Sc+ZvTzfS*v!yKM4efxn z=cV4ta*M*A1lq$QN)gf-iCApg5Oa8O@VML4=7(>>0_U>6k3y?o7jhGx!_Zq7!^{*s%h?O6 z&xZ!Ar*NR{Zf5C1@W-l@8G~ZO=^@+Hr2(+TLKfLa4@(?(sY&%-+77(nHBeM^Yr5%m z&4^~b3GWkYk78(GS;OI3i@Rmz%EEFf$6lE}d4tTWoVr8$v96J_XL+3$1EK!5-alto z6FIYP+ns=nC~Dk)%^HG3S_vys+Q{9|4SFpPPr0~Y*}GSig(fb`ci=p8btO4e7NbwV z1&$oKWn;gY8P7SuWvDo{we$-snTe2?5TM&pKwwqkR9BG?=Bpq`G9$rb6$>9)P#RrVNx$CD`r5a7$VFkVs4 z-FpGAa)+1^gNfa{Re+;A@w|1%_@KF?lm8<*<N^?)0jP}56%_rF3!%nZfx&XWr&1`Dk&@*}z$x^IZ^UITc zy-DugXIC!KZgkKyFYHO?THPygv)W}reJiFc1%FyB)g1hE>O!ipeAwc2&)cb|;mf;* zD9)4AzM?z@B9s}B{8C%J0%j~epx1&ro8`a3pja{8^2o*IAxtipADHj3wxOulYbrB_ zjY^0Uq9b-_R2d7f{Ii*pGi0%V^58gU8^*^-l*+1Q7pN-^Z%EJMAT=gkG)Fw7ZKanS zpG@YPNenhcZ7HY(Z0eEc#R9yVrfh3w$Gzv(f&4@rTAoZw8>jaotu^dpBb-e*u=WcY zva^lvZ$B=`+eE8F$kLoD4~fITjB#@pIpIrw1V>t$3tB~x@7R@l!Ejn(>eP$OPUB}T z!%tljQYAF-{Yid+shW71eF|U2?)MqxnTOr4Qxw3G_Q9LK(`Mx&WC<6B8}YpIyL6Mq zN*8~fXOQDlj3F*#ByW3h#cT(A!PhTJB;Vla!K7b8e|FqCoSAx#A zbav(5^f|@F(FX;;O=s)7WW~gX0{>(45wk4EUezK89 zF6mg?&}>yBc5b3&XZ}Vc>Ue5aR0;#xZ?c%Dh8EPXb$)lJPXO~en^mcBO61sDqA8KU z+eMecL~LMrOg{s_Vn5+&;$ut&{%dd8jW?W@!yAzcMf%LZIflkH{tLPQ3tb%CFIk)B zG-n_RoCu;t3&G3kumdXE+HN{cSX|9oq>lkw$N>>90=Tk#&#PfWmpgof92@V`6N6L6 z1OTAeX*V4-G9sU@?Y~Qqg50s?=3EYO+5L#?pHV4%cE8>!Vcim4s>lmp)|ODTt)Zu3 zE!cqhvZ<}Vei%?Xt*=Nj@*J#h7aQpnb_`(e$vn~mD0we-Ekk&J<~?vMj)w@62c%-M z$sAxLxhrV-{>(c@S78iFO^4Ch;|lOTqedPjMH`5pWIP_Py8v6diB5xD z9&^@om_X`4mPzoeWsS48ieXfQrG5A*bu;N>{ac*`F?B7VMb>3O10p#V`!+ zEol>PE7Fz{198;j03bir{}sC|=)ky(Cmn)g@)$|$(}|M8B+8UoOCjkljlA&Gw4Yy; zzlWLgPjl-X$W_A^Wof6f{!yneqcB8Y} z?;mF_!Je`9$S1K?>)qJt*-f)9Q%PIep+<&D{lTp`7%80CqX+=IvMHrIidA(BF?=jQ zOA|N(S9H1+z^iNWSssg=FDoe`u>yD3bQ;|PszYv5vdn-tTt7xU)R9M6qo~AJnWm6V zt~sTyY{IX3$9Bf$5~!pzWnO!H~B_^`yMhXiFsC1Bi0b98E=9RwKx-Qn_!MFZYf^ipn&3 zfr_=F)SyRFy=>>)Sb_PLv_(-rijuC^`j4P47ZCCwlyMw5p-ycG9q1vkWE0$LJZWF! zZOQ9+0WnMP+a@rzJkyZQ4G;|6P$`CL@?rU}r|Lrc@Dv_Zh!;Z~h*xhW&$bdcmtb@G zSgzCvEDPDwZZZ-8@`qReU!6+H4DZ^jugYGsMxFAM2+HLJ?gF}f ze^deFcb~mkyY4cx>yqzXPb4M)co4407$h0~ShdmbO!wQF>7p_+ECxoKG*rdpTLR&S zfL1-X#Gl|_pU*Y84I3KT2w>EtpgO7*^^c}*M416fTHcGVZiU-G@hvG(L9p46wH?yD zJ#}&%`U{OEiU&K8;bO~w!!Oe5MFT=aP?pZxAY5Zh6KZHSAXQ#d=7$QBWDs3}53~j9 z1nLQCC^is?GBf1mE>+TQ3gffWQb&4E*+xy2{~QrMnvqwv&7Hu*$ILi*OdddkTd_0b zd}*g(VOMOV#I%hI;mbsHKM{;`-~#}Jw??h^lycOCkx$PcI+^74c#in{+xZ;uDTePF zmWpci!&x2ho~PMI@1)S%V4CCxM%dyYtVUgfqVWq z+Ye@+86Bt>2&z(-f<1deJT5bA^JT4eByv#~_4;GVg_(PwDlTQPN4>$*Z+2y!96wBm zdwMjg_*o58+kv(qSK>EG)xb8XY8Gi|0H=>ElQxmu2B!es+b`004Rmi%WnAD%*TQ*1 z&d|j!C9(p)Zx?Yo!x9b^vh_;kn(J2z9#vVqw$#Xf!^~nxoar~1f4cTwHteH~JPH|9)c3A*-S*)vfjY3?9)3 z=V*cRVS9Rv9?uLgF=13{j`#M+35dFoBA-lV*DhfI<&9pu5``B-uL)08(rb;C@){=H zYe8Pfi0L`AoIqAoC~YR-5h{AgCZXocFVw;A)o;9Ar}M&rnfI?WA(KtbzPHEXIf#jn z%>yKZNi$>1otFt_cE*MEKz48d4TaYJN?&d@wEtZ&im00D^sa*uusJhiH`(84W2>;> zC-j&TM(~o2XR|}I&!PTz4Z=0>%cuPKdB4H zh(SYI_CnY0#xOy;gr`9MQ#(i|a@!aHTBW%p4tqmqmG;I_ttvOvX9F*lILLti)M%R> zLeoBf$vytZA-&>DF*({PYLvR1G}|(PUw)CCU8Of;m9V7B&tMg?7;r#01xrVUDw8}& zud+xivHNWFD{y`5~U12f|5&QqtO2BiR7_5#{ znqS^VGL~a$1*~VZcGG)Dz-G~5!$-2?-C?r|nIuWpqP5Pr2Wjn*r z-7U+(=Eg`_K;9Y&x5^P+3eh~jOxulT0tDxHbj1R7EHB6HkTL-6Ox$6Ogl0yS0_ZLP ztNK%;=MY|rT~`C{!7A4AAmXYJ%lD=>(mXmA4UQ|<=hs1oi@2f{tPTK#xdxH-i4tr1 z(y}~lwl)y_YpKIWu3OY4Q{DpvsGCpf*ePhZnrR5@Jvns3;YDuSPnb@Fm=FNQJ z=(P%LP(LL_`4JA_W)4m(78)1nnX&FtCMl?!g= z0rYW{X}Cz}C5(V8c$ZFBsI+}Fx=Hxeo^rY1=7-{$R-{Hi2z|veE94z}RQ{?%&zWVp`7$M`qTuBgb53l(wGok~x} zd4_jprxy-&vqk-$j+5vn-dvMKO%-+ptSr`>i|UoK1E;X{VsN)@W1M!39_3M1j^>Jp zU4Dn{VWp!-raZokkFo;+)d4FbtB+Dvj4N3)9+!>x9|F87|)??#d zmR9VH$(2@GZ~;jEESw-=ePTH1jOW8mZ1c5z=u?@SQxbfywv z8_OMbw7)i{z&vl9Ni~vsr9Sz`R6V@5Yto)Z0Lj$aMo*yFwAYLRUVsW)!_kF;XsJkQ z$nS0*#?1Oth=&M!R5BBkZ{9X>eulAAj5*z150$UB@=dj?Yb1$@y^1GN?*EQSaGMHg z(g-YE)Qn5~65`!dl#@@rMkLu^vs&jFM<b%ik-FdaTKt^PQDWc1R==!qb* z6O@969Y@Woziu=mPJC1VGqzfx??xIwRfLYcT$~gtsDHvdEiKzThALqvWm5O0j!6dl zV9MhwVaR3~(Q%%6Vp>Y-litZi8m$h(JYkLSC-+);!`#y!KCp9xqA9nd^CmZ;T+H*t zI(H79oT9msPQ3oQHxZ2W_4V7@EW>thGG;Y-7;`}owZRxr@zO+eR#2$2{f~5tE~5FS zpbia=gI~WH706im6uFciFGc(k+_>kWLK$icB_W|>rxW7iVo0yyq9iZfAvRDaiZhO5 zDt)bcywXFgz;0j`s$Kszm`w1&74>q<7aTjjYqP8G2hLhqxn2~$fsqgM9$wCZe1f!Q zF}ejooqxeu;bGyuSjF9KWouP=s_Wa92SRBR9L;t_$G$aFH1Xy`(KaU;4%fb*l)e+e zu!u)jB;r#e+{Pwm2Bt6>B3Yxemd{9$+Rbh6mG6r-*IYwpu+L~iac%EfR#N*BY}!z` zIJ~WxBc)r6K6(BA#jTZO5n*9|eV_ieW?&CFuea)6IuMHe2Aws!nT@G47;5Z1Oz`AB z?MGwF>OQcW5Ttu<4;9z^C*3273Vg(-VU7UPY0PI87%xmMzys^Muo$mUlj(m}p2I|LHU%Tz`ZgSF7&s=|d zX7X@Aqrjv1B!p+Zm`)7a$!98ETXsz2)9U)w%Xj(f$C%Litz!W;779mSoH4cXn%jfk vOQZhZ>)a|+T99Fw_H+B8@RRTI8Ia;zKboWp6JIkc0j}$tTrIoe_~icr2)_D6 diff --git a/contrib/Windows/postit.nsi b/contrib/mswin/postit.nsi similarity index 100% rename from contrib/Windows/postit.nsi rename to contrib/mswin/postit.nsi diff --git a/doc/architecture/postit.md b/doc/architecture/postit.md index 2ba6cb812..625cc1875 100644 --- a/doc/architecture/postit.md +++ b/doc/architecture/postit.md @@ -175,10 +175,19 @@ affichées dans la barre de statut, sans les masquer par une déduction MIME. Les types `video/*` ouvrent `VideoPage` via `App.PushPageAsync` sur desktop. Cette page contient un `LibVLCSharp.Avalonia.VideoView`, lecture/pause et une action d'ouverture externe. Le lecteur et ses ressources natives sont -arrêtés et libérés à la sortie de la page. **LibVLC doit être installé** +arrêtés et libérés à la sortie de la page. La lecture utilise directement +`new LibVLC()`, `new MediaPlayer(libVlc)` et `Play(media)`, sans options +spécifiques à Linux ni détection VAAPI/VDPAU. Le média est libéré après +`Play` comme dans l'exemple officiel ; le lecteur conserve sa référence native. +**LibVLC doit être installé** sur le poste (par exemple `libvlc5` et les plugins VLC sur Linux ; les bibliothèques natives adaptées sont nécessaires sous Windows/macOS). Une absence de LibVLC ou une erreur de décodage est signalée dans la page. +Le navigateur ne s'ouvre que sur demande explicite, jamais en repli automatique. +Après la navigation, `TryUpdateNativeControlPosition()` synchronise la surface +native à la priorité `Loaded`, une fois la mise en page effectuée : +les transformations de transition ne déclenchent pas de changement de `Bounds`. +Le rappel est ignoré si le lecteur a été libéré ou remplacé entre-temps. Sur Android et Browser, où ce contrôle natif desktop n'est pas disponible, les vidéos sont ouvertes dans le navigateur, comme les autres fichiers (images liées, PDF, téléchargements) et les liens externes. @@ -231,6 +240,58 @@ faut *deux* enregistrements : la page en `AddTransient` (ou de `ViewLocator.Build`. Si l'un manque, l'app affiche "No view for X" sans crash. +## Podcasts : capture, direct et replay + +`PodcastsPageViewModel` est accessible depuis l'accueil Desktop Windows/Linux. +`CameraStreamer` detecte les cameras DirectShow/V4L2 et les microphones +(DirectShow sous Windows, source PulseAudio par defaut sous Linux). +`PodcastBroadcaster` utilise uniquement LibVLC : encodage H.264/AAC, sortie +`livehttp`, segments MPEG-TS d'environ deux secondes. Un segment n'est +envoye que lorsque l'index local LibVLC le declare complet. + +`PodcastApiClient` reutilise le transport bearer et son rafraichissement +des jetons. Les PUT sont numerotes a partir de zero ; une nouvelle tentative +doit avoir exactement les memes octets et la meme duree. Le client supprime +un segment local seulement apres acquittement, et envoie le dernier segment +ferme par LibVLC avant de terminer le direct. La navigation et la deconnexion +arretent la camera. Les erreurs restent visibles ; les fichiers non envoyes +restent disponibles localement pour recuperation manuelle. + +`ILiveProcessor` est un singleton dans Yavsc.Api. Il stocke les metadonnees +et empreintes SHA-256 dans un index JSON remplace atomiquement, avec des +segments immuables accessibles uniquement via les endpoints authentifies. +Deux receptions au plus sont tamponnees en parallele ; les validations de +sequence, de proprietaire et de quota sont serialisees avant commit. +Un index ne reference jamais un segment en cours d'ecriture. + +| Route sous `api/v1/podcasts` | Role | +| --- | --- | +| `GET /`, `POST /` | Catalogue ou creation d'une emission | +| `GET /{id}` | Metadonnees et segments ordonnes | +| `PUT /{id}/segments/{sequence}?duration=...` | Ingestion `video/mp2t`, auteur uniquement | +| `POST /{id}/complete` | Finalisation idempotente du replay | +| `GET /{id}/index.m3u8` | Playlist HLS EVENT, ENDLIST apres fin/interruption | +| `GET /{id}/segments/{sequence}.ts` | Segment engage, lecture authentifiee | +| `DELETE /{id}` | Suppression d'une emission terminee, auteur uniquement | + +Toutes les routes requierent la politique `ApiScope`. La lecture est ouverte +a tous les utilisateurs authentifies avec ce scope, pas seulement a l'auteur. +`PodcastMediaInput` alimente LibVLC en octets MPEG-TS via les requetes +authentifiees existantes, sans transmettre de jeton dans une URL ou un +processus externe. Les rappels natifs ne laissent echapper aucune exception : +l'erreur est remontee a la page de lecture. + +Un direct inactif devient `Interrupted` lors de la prochaine lecture de +l'index ; au redemarrage, tous les anciens directs sont interrompus. +Les segments engages constituent le replay, sans transcodage serveur. +Le stockage exige un seul processus par racine et les quotas podcast sont +independants de ceux des fichiers utilisateur. Voir +[INSTALL.md](../../INSTALL.md#podcast-live-broadcasts-and-replays) pour +les limites et le deploiement ; [Podcast.http](../../src/Yavsc.Api/Podcast.http) +pour les requetes. +Le document OpenAPI dedie est `/openapi/podcasts.json`, accessible en +developpement aux administrateurs comme les autres documents de l'API. + ## Composition root (`App.axaml.cs`) `App.OnFrameworkInitializationCompleted` est le seul endroit où diff --git a/doc/testing.md b/doc/testing.md index ef80cda9b..8b5e8eff1 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -73,6 +73,30 @@ HTTP, on stub `IAuthorizationService` directement (cf. `BlogspotController` dans `Yavsc.Org.Tests/NonRegression/`) pour éviter de monter un host complet. +## Tests cibles des podcasts + +Avec le runner Microsoft.Testing.Platform du depot (.NET 10, xUnit v3) : + +```bash +dotnet test --project src/test/Yavsc.Api.Tests/Yavsc.Api.Tests.csproj \ + --filter-class '*LiveProcessorTests' --filter-class '*PodcastControllerTests' +dotnet test --project src/PostIt/PostIt.Tests/PostIt.Tests.csproj \ + --filter-class '*PodcastTests' --filter-class '*PodcastNativeCaptureTests' +``` + +Les tests serveur couvrent HTTP/authentification, proprietaire, sequences, +reprises idempotentes, limites exactes, concurrence et persistance des replays +apres interruption/redemarrage, dans des dossiers temporaires isoles. + +Les tests natifs sont explicitement ignores sans `POSTIT_TEST_MEDIA`. +Pour les activer, installer LibVLC avec ses encodeurs H.264/AAC et definir +cette variable sur un fichier video local d'environ huit secondes contenant +une piste audio. Ils exercent la segmentation LibVLC reelle, l'envoi et la +reprise apres acquittement perdu, le dernier segment apres arret et le +decodage authentifie du replay. Aucun FFmpeg n'est requis par ces tests ni +par l'application (le fichier d'entree peut provenir de n'importe quelle source). +Les tests materiels camera/microphone Windows/Linux restent manuels. + ## Quand ne PAS écrire de test Un test qui ne détecte rien n'est pas un test. Si l'invariant diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index fbfaa1f38..3b23fc8fb 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -17,16 +17,16 @@ - + - + - + @@ -36,6 +36,6 @@ - + - \ No newline at end of file + diff --git a/src/PostIt/PostIt.Tests/PodcastNativeCaptureTests.cs b/src/PostIt/PostIt.Tests/PodcastNativeCaptureTests.cs new file mode 100644 index 000000000..8fc0c7cca --- /dev/null +++ b/src/PostIt/PostIt.Tests/PodcastNativeCaptureTests.cs @@ -0,0 +1,166 @@ +using System.Net; +using System.Net.Http; +using LibVLCSharp.Shared; +using PostIt.Services; +using Yavsc.Abstract.Streaming; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +public sealed class PodcastNativeCaptureTests +{ + public static bool HasFixture => File.Exists(Environment.GetEnvironmentVariable("POSTIT_TEST_MEDIA")); + + [Fact(Skip = "Set POSTIT_TEST_MEDIA to a local video with audio and install LibVLC with H.264/AAC encoders.", SkipUnless = nameof(HasFixture))] + public async Task LibVLC_produces_complete_H264_AAC_HLS_segments_without_external_encoder() + { + var root = Path.Combine(Path.GetTempPath(), $"postit native's hls {Guid.NewGuid():N}"); + Directory.CreateDirectory(root); + try + { + Core.Initialize(); + using var vlc = new LibVLC(); + using var media = new Media(vlc, new Uri(Path.GetFullPath(Environment.GetEnvironmentVariable("POSTIT_TEST_MEDIA")!))); + using var player = new MediaPlayer(vlc); + PodcastBroadcaster.ConfigureOutput(media, root); + var ended = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + player.EndReached += (_, _) => ended.TrySetResult(); + player.EncounteredError += (_, _) => ended.TrySetException(new InvalidOperationException("LibVLC encoding failed.")); + Assert.True(player.Play(media)); + await ended.Task.WaitAsync(TimeSpan.FromSeconds(45), TestContext.Current.CancellationToken); + player.Stop(); + var playlist = await File.ReadAllTextAsync(Path.Combine(root, "index.m3u8"), TestContext.Current.CancellationToken); + var segments = HlsCapturePlaylist.Parse(playlist); + Assert.True(segments.Count >= 2, playlist); + Assert.Equal(0, segments[0].Sequence); + foreach (var segment in segments) + { + var bytes = await File.ReadAllBytesAsync(Path.Combine(root, segment.FileName), TestContext.Current.CancellationToken); + Assert.True(bytes.Length > 188); + Assert.Equal(0, bytes.Length % 188); + Assert.InRange(segment.DurationSeconds, 0.001, 15); + for (var i = 0; i < bytes.Length; i += 188) Assert.Equal(0x47, bytes[i]); + } + } + finally { Directory.Delete(root, recursive: true); } + } + + [Fact(Skip = "Set POSTIT_TEST_MEDIA to a local video with audio and install LibVLC with H.264/AAC encoders.", SkipUnless = nameof(HasFixture))] + public async Task Broadcaster_uploads_final_segment_retries_safely_and_replay_decodes_in_LibVLC() + { + var ct = TestContext.Current.CancellationToken; + await using var transport = new CaptureTransport(); + var client = new PodcastApiClient(transport, () => "https://api.example/api/v1/"); + await using (var broadcaster = await PodcastBroadcaster.StartAsync(client, "Native test", + vlc => new Media(vlc, new Uri(Path.GetFullPath(Environment.GetEnvironmentVariable("POSTIT_TEST_MEDIA")!))), ct)) + { + await transport.FirstTwo.Task.WaitAsync(TimeSpan.FromSeconds(45), ct); + var livePlayback = PlayThroughAsync(client, transport.Podcast.Id, live: true, ct); + await transport.ReadStarted.Task.WaitAsync(TimeSpan.FromSeconds(45), ct); + await broadcaster.StopAsync().WaitAsync(TimeSpan.FromSeconds(45), ct); + Assert.True(broadcaster.Completion.IsCompletedSuccessfully); + await livePlayback; + } + Assert.Equal(PodcastState.Completed, transport.Podcast.State); + Assert.True(transport.Uploaded.Count >= 3); + Assert.Equal(2, transport.FirstSegmentAttempts); + await PlayThroughAsync(client, transport.Podcast.Id, live: false, ct); + } + + private static async Task PlayThroughAsync(PodcastApiClient client, Guid id, bool live, CancellationToken ct) + { + using var vlcPlayer = new LibVLC("--vout=dummy", "--aout=dummy", "--no-xlib"); + using var input = new PodcastMediaInput(client, id, live); + using var replay = new Media(vlcPlayer, input); + replay.AddOption(":demux=ts"); + using var player = new MediaPlayer(vlcPlayer); + var ended = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + input.Failed += message => ended.TrySetException(new InvalidOperationException(message)); + player.EndReached += (_, _) => ended.TrySetResult(); + player.EncounteredError += (_, _) => ended.TrySetException(new InvalidOperationException("Replay decode failed.")); + Assert.True(player.Play(replay)); + try + { + await ended.Task.WaitAsync(TimeSpan.FromSeconds(45), ct); + Assert.Contains(replay.Tracks, track => track.TrackType == TrackType.Video && track.Codec == 0x34363268); + Assert.Contains(replay.Tracks, track => track.TrackType == TrackType.Audio && track.Codec == 0x6134706d); + } + finally + { + input.Cancel(); + player.Stop(); + } + } + + private sealed class CaptureTransport : IYavscApiClient + { + private readonly object _sync = new(); + public HttpClient Http { get; } = new(); + public Dictionary Uploaded { get; } = new(); + private readonly List _segments = new(); + public TaskCompletionSource FirstTwo { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public TaskCompletionSource ReadStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + public int FirstSegmentAttempts { get; private set; } + public PodcastResponse Podcast { get; private set; } = new(Guid.NewGuid(), "Test", "alice", "Alice", + DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, PodcastState.Live, 0, 0, 0); + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + lock (_sync) + { + object result; + if (typeof(T) == typeof(PodcastManifestResponse)) + result = new PodcastManifestResponse(Podcast, _segments.ToArray()); + else + { + if (path.EndsWith("/complete", StringComparison.Ordinal)) + Podcast = Podcast with { State = PodcastState.Completed }; + result = Podcast; + } + return Task.FromResult((T)result); + } + } + + public async Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + { + var uri = new Uri(path); + var sequence = int.Parse(uri.Segments[^1], System.Globalization.CultureInfo.InvariantCulture); + var duration = double.Parse(uri.Query["?duration=".Length..], System.Globalization.CultureInfo.InvariantCulture); + using var content = contentFactory(); + var bytes = await content.ReadAsByteArrayAsync(ct); + if (sequence >= 2) await ReadStarted.Task.WaitAsync(ct); + lock (_sync) + { + if (Uploaded.TryGetValue(sequence, out var existing)) + Assert.Equal(existing, bytes); + else + { + Assert.Equal(_segments.Count, sequence); + Uploaded.Add(sequence, bytes); + _segments.Add(new(sequence, duration, bytes.Length)); + Podcast = Podcast with { SegmentCount = _segments.Count, Bytes = Podcast.Bytes + bytes.Length }; + } + if (sequence == 0 && ++FirstSegmentAttempts == 1) + throw new HttpRequestException("Acknowledgement lost", null, HttpStatusCode.ServiceUnavailable); + if (Uploaded.Count >= 2) FirstTwo.TrySetResult(); + return (T)(object)Podcast; + } + } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + => CallAsync(method, path, body, ct); + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, contentFactory, ct); + public Task DownloadAsync(HttpMethod method, string path, CancellationToken ct = default) + { + var sequence = int.Parse(Path.GetFileNameWithoutExtension(new Uri(path).AbsolutePath), + System.Globalization.CultureInfo.InvariantCulture); + lock (_sync) + { + ReadStarted.TrySetResult(); + return Task.FromResult(Uploaded[sequence]); + } + } + public ValueTask DisposeAsync() { Http.Dispose(); return ValueTask.CompletedTask; } + } +} diff --git a/src/PostIt/PostIt.Tests/PodcastTests.cs b/src/PostIt/PostIt.Tests/PodcastTests.cs new file mode 100644 index 000000000..76a75a352 --- /dev/null +++ b/src/PostIt/PostIt.Tests/PodcastTests.cs @@ -0,0 +1,163 @@ +using System.Globalization; +using System.Net.Http; +using System.Runtime.InteropServices; +using LibVLCSharp.Shared; +using PostIt.Services; +using Yavsc.Abstract.Streaming; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +public sealed class PodcastTests +{ + private static readonly Guid Id = Guid.Parse("9e2f6d47-2b9a-47ae-a870-073980bf2628"); + + [Fact] + public void Parses_only_complete_ordered_capture_segments_with_invariant_durations() + { + var segments = HlsCapturePlaylist.Parse("#EXTM3U\n#EXTINF:2.5,\nsegment-00000000.ts\n#EXTINF:1.75,\nsegment-00000001.ts\n"); + Assert.Equal(2, segments.Count); + Assert.Equal(2.5, segments[0].DurationSeconds); + Assert.Equal(1, segments[1].Sequence); + Assert.Empty(HlsCapturePlaylist.Parse("#EXTM3U\n#EXTINF:2,\n")); + } + + [Theory] + [InlineData("#EXTINF:NaN,\nsegment-00000000.ts\n")] + [InlineData("#EXTINF:20,\nsegment-00000000.ts\n")] + [InlineData("#EXTINF:2,\n../escape.ts\n")] + [InlineData("#EXTINF:2,\nhttps://other.test/segment-00000000.ts\n")] + [InlineData("#EXTINF:2,\nsegment-00000000.ts\n#EXTINF:2,\nsegment-00000002.ts\n")] + public void Rejects_invalid_or_nonlocal_capture_playlist(string text) + => Assert.Throws(() => HlsCapturePlaylist.Parse(text)); + + [Fact] + public async Task Upload_uses_raw_transport_stream_and_replayable_content_factory() + { + var transport = new RecordingTransport(); + var client = new PodcastApiClient(transport, () => "https://api.example/"); + var previous = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR"); + await client.UploadAsync(Id, 0, 2.5, new byte[] { 0x47, 1 }, TestContext.Current.CancellationToken); + Assert.EndsWith($"/api/v1/podcasts/{Id}/segments/0?duration=2.5", transport.Path); + Assert.Equal(HttpMethod.Put, transport.Method); + Assert.NotNull(transport.ContentFactory); + using var first = transport.ContentFactory(); + using var retry = transport.ContentFactory(); + Assert.Equal("video/mp2t", first.Headers.ContentType?.MediaType); + Assert.Equal(new byte[] { 0x47, 1 }, await first.ReadAsByteArrayAsync(TestContext.Current.CancellationToken)); + Assert.Equal(await first.ReadAsByteArrayAsync(TestContext.Current.CancellationToken), + await retry.ReadAsByteArrayAsync(TestContext.Current.CancellationToken)); + } + finally { CultureInfo.CurrentCulture = previous; } + } + + [Theory] + [InlineData("https://api.example/")] + [InlineData("https://api.example/api/v1/")] + [InlineData("https://api.example/api/v1")] + public async Task Configured_api_url_never_duplicates_the_api_prefix(string baseUrl) + { + var transport = new RecordingTransport(); + var client = new PodcastApiClient(transport, () => baseUrl); + await client.GetAsync(Id, TestContext.Current.CancellationToken); + Assert.Equal($"https://api.example/api/v1/podcasts/{Id}", transport.Path); + } + [Fact] + public void Authenticated_playback_reads_segments_in_order_and_reports_end_of_replay() + { + var transport = new RecordingTransport(); + var client = new PodcastApiClient(transport, () => "https://api.example/"); + using var input = new PodcastMediaInput(client, Id, live: false); + var buffer = Marshal.AllocHGlobal(4); + try + { + Assert.True(input.Open(out var size)); + Assert.Equal(ulong.MaxValue, size); + Assert.Equal(2, input.Read(buffer, 4)); + Assert.Equal(0x47, Marshal.ReadByte(buffer)); + Assert.Equal(0, input.Read(buffer, 4)); + Assert.EndsWith($"/{Id}/segments/0.ts", transport.DownloadPath); + } + finally { Marshal.FreeHGlobal(buffer); } + } + + [Fact] + public void Playback_errors_are_reported_without_crossing_the_native_callback_boundary() + { + var transport = new RecordingTransport { FailDownload = true }; + var client = new PodcastApiClient(transport, () => "https://api.example/"); + using var input = new PodcastMediaInput(client, Id, live: false); + string? error = null; + input.Failed += message => error = message; + var buffer = Marshal.AllocHGlobal(4); + try + { + Assert.Equal(-1, input.Read(buffer, 4)); + Assert.Contains("401", error); + } + finally { Marshal.FreeHGlobal(buffer); } + } + + [Fact] + public void Live_playback_joins_three_segments_behind_the_edge_and_cancellation_is_quiet() + { + var transport = new RecordingTransport { SegmentCount = 8 }; + var client = new PodcastApiClient(transport, () => "https://api.example/"); + using var input = new PodcastMediaInput(client, Id, live: true); + string? error = null; + input.Failed += message => error = message; + var buffer = Marshal.AllocHGlobal(4); + try + { + Assert.Equal(2, input.Read(buffer, 4)); + Assert.EndsWith($"/{Id}/segments/5.ts", transport.DownloadPath); + input.Cancel(); + Assert.Equal(-1, input.Read(buffer, 4)); + Assert.Null(error); + } + finally { Marshal.FreeHGlobal(buffer); } + } + + private sealed class RecordingTransport : IYavscApiClient + { + public HttpClient Http { get; } = new(); + public HttpMethod? Method { get; private set; } + public string? Path { get; private set; } + public string? DownloadPath { get; private set; } + public Func? ContentFactory { get; private set; } + public bool FailDownload { get; init; } + public int SegmentCount { get; init; } = 1; + + private static PodcastResponse Podcast => new(Id, "Live", "alice", "Alice", + DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, PodcastState.Completed, 1, 2, 2); + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + Method = method; + Path = path; + object response = typeof(T) == typeof(PodcastManifestResponse) + ? new PodcastManifestResponse(Podcast with { SegmentCount = SegmentCount }, + Enumerable.Range(0, SegmentCount).Select(i => new PodcastSegmentResponse(i, 2, 2)).ToArray()) : Podcast; + return Task.FromResult((T)response); + } + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + { + ContentFactory = contentFactory; + return CallAsync(method, path, ct: ct); + } + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + => CallAsync(method, path, body, ct); + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, contentFactory, ct); + public Task DownloadAsync(HttpMethod method, string path, CancellationToken ct = default) + { + DownloadPath = path; + if (FailDownload) throw new HttpRequestException("401 Unauthorized"); + return Task.FromResult(new byte[] { 0x47, 1 }); + } + public ValueTask DisposeAsync() { Http.Dispose(); return ValueTask.CompletedTask; } + } +} diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index d6a0f6fed..d764507de 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -38,6 +38,7 @@ public static class ServiceCollectionHelpers var resourceUsageClient = new ResourceUsageApiClient(api, () => settings.ApiUrl); var administrationClient = new AdministrationApiClient(api, () => settings.ApiUrl); var notificationsClient = new NotificationsApiClient(api, () => settings.ApiUrl); + var podcastClient = new PodcastApiClient(api, () => settings.ApiUrl); var userDirectory = new UserDirectory(userSearchClient); var reverseGeocoding = new NominatimReverseGeocodingService(); @@ -46,6 +47,7 @@ public static class ServiceCollectionHelpers services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Models @@ -106,6 +108,8 @@ public static class ServiceCollectionHelpers services.AddSingleton(resourceUsageClient); services.AddSingleton(administrationClient); services.AddSingleton(notificationsClient); + services.AddSingleton(podcastClient); + services.AddTransient(); services.AddSingleton(userFilesClient); services.AddSingleton(reverseGeocoding); services.AddSingleton(userDirectory); diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 27bdf298c..e080b9a26 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -12,6 +12,7 @@ + diff --git a/src/PostIt/PostIt/Services/CameraStreamer.cs b/src/PostIt/PostIt/Services/CameraStreamer.cs index d78a84349..322331772 100644 --- a/src/PostIt/PostIt/Services/CameraStreamer.cs +++ b/src/PostIt/PostIt/Services/CameraStreamer.cs @@ -20,27 +20,14 @@ public class CameraStreamer "--aout=dummy", "--intf=dummy"); - public static LibVLC CreatePlaybackLibVlc() - { - if (!OperatingSystem.IsLinux()) - return new LibVLC(); - - var preferHardware = string.Equals(Environment.GetEnvironmentVariable("POSTIT_LIBVLC_ENABLE_HW"), "1", - StringComparison.OrdinalIgnoreCase); - - if (preferHardware && IsVaapiUsable()) - return new LibVLC(); - - // On Linux, a non-hardware display path is still valid for in-app playback as long as - // the platform is not explicitly forced into a broken VAAPI/VDPAU stack. - return new LibVLC("--avcodec-hw=none"); - } - public static LibVLC CreatePreferredLibVlc() { if (!OperatingSystem.IsLinux()) return new LibVLC(); + if (ShouldDisableHardwareAcceleration()) + return CreateHeadlessLibVlc(); + var preferHardware = string.Equals(Environment.GetEnvironmentVariable("POSTIT_LIBVLC_ENABLE_HW"), "1", StringComparison.OrdinalIgnoreCase); @@ -50,6 +37,10 @@ public class CameraStreamer return CreateHeadlessLibVlc(); } + private static bool ShouldDisableHardwareAcceleration() + => string.Equals(Environment.GetEnvironmentVariable("POSTIT_LIBVLC_DISABLE_HW"), "1", + StringComparison.OrdinalIgnoreCase); + private static bool IsVaapiUsable() { var vaapiDrivers = new[] diff --git a/src/PostIt/PostIt/Services/HlsCapturePlaylist.cs b/src/PostIt/PostIt/Services/HlsCapturePlaylist.cs new file mode 100644 index 000000000..5e4a02826 --- /dev/null +++ b/src/PostIt/PostIt/Services/HlsCapturePlaylist.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace PostIt.Services; + +public sealed record CaptureSegment(int Sequence, double DurationSeconds, string FileName); + +public static partial class HlsCapturePlaylist +{ + [GeneratedRegex(@"^segment-(\d{8})\.ts$", RegexOptions.CultureInvariant)] + private static partial Regex SegmentName(); + + public static IReadOnlyList Parse(string playlist) + { + var segments = new List(); + double? duration = null; + foreach (var line in playlist.Split('\n')) + { + var text = line.Trim(); + if (text.StartsWith("#EXTINF:", StringComparison.Ordinal)) + { + if (!double.TryParse(text[8..].Split(',')[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var value) + || !double.IsFinite(value) || value <= 0 || value > 15) + throw new FormatException("Duree du segment LibVLC invalide."); + duration = value; + } + else if (text.Length > 0 && !text.StartsWith('#')) + { + var match = SegmentName().Match(text); + if (!match.Success || duration is null) + throw new FormatException("Index HLS LibVLC invalide."); + var sequence = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + if (segments.Count > 0 && sequence != segments[^1].Sequence + 1) + throw new FormatException("Segments HLS non consecutifs."); + segments.Add(new(sequence, duration.Value, text)); + duration = null; + } + } + return segments; + } +} diff --git a/src/PostIt/PostIt/Services/PodcastBroadcaster.cs b/src/PostIt/PostIt/Services/PodcastBroadcaster.cs new file mode 100644 index 000000000..ac402f6e0 --- /dev/null +++ b/src/PostIt/PostIt/Services/PodcastBroadcaster.cs @@ -0,0 +1,231 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using LibVLCSharp.Shared; +using Yavsc.Abstract.Streaming; +using Yavsc.Api.Client; + +namespace PostIt.Services; + +public sealed class PodcastBroadcaster : IAsyncDisposable +{ + private readonly PodcastApiClient _api; + private readonly LibVLC _vlc; + private readonly MediaPlayer _player; + private readonly Media _media; + private readonly string _directory; + private readonly CancellationTokenSource _cancellation = new(); + private readonly SemaphoreSlim _stopGate = new(1); + private volatile bool _stopped; + private volatile bool _encoderFailed; + private bool _disposed; + + public PodcastResponse Podcast { get; private set; } + public Task Completion { get; private set; } = Task.CompletedTask; + public event Action? Progress; + + private PodcastBroadcaster(PodcastApiClient api, PodcastResponse podcast, + LibVLC vlc, MediaPlayer player, Media media, string directory) + { + _api = api; + Podcast = podcast; + _vlc = vlc; + _player = player; + _media = media; + _directory = directory; + _player.EncounteredError += OnEncoderError; + } + + public static void ConfigureOutput(Media media, string directory) + { + var path = directory.Replace('\\', '/').Replace("\"", "\\\""); + media.AddOption(":sout=#transcode{vcodec=h264,venc=x264{preset=ultrafast,tune=zerolatency,keyint=60},vb=1200,fps=30," + + "acodec=mp4a,ab=128,channels=2,samplerate=48000}:std{" + + $"access=livehttp{{seglen=2,delsegs=false,numsegs=0,initial-segment-number=0,index=\"{path}/index.m3u8\"," + + $"index-url=segment-########.ts}},mux=ts{{use-key-frames}},dst=\"{path}/segment-########.ts\"}}"); + } + + public static async Task StartAsync(PodcastApiClient api, string title, + string camera, string? microphone, CancellationToken ct) + => await StartAsync(api, title, vlc => new CameraStreamer(vlc).CreateCaptureMedia(camera, microphone), ct); + + internal static async Task StartAsync(PodcastApiClient api, string title, + Func createMedia, CancellationToken ct) + { + Core.Initialize(); + var vlc = CameraStreamer.CreatePreferredLibVlc(); + Media? media = null; + MediaPlayer? player = null; + PodcastBroadcaster? broadcaster = null; + var directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "PostIt", "podcast-capture", Guid.NewGuid().ToString("N")); + PodcastResponse? podcast = null; + try + { + Directory.CreateDirectory(directory); + media = createMedia(vlc); + ConfigureOutput(media, directory); + player = new MediaPlayer(vlc); + podcast = await api.CreateAsync(title, ct); + await File.WriteAllTextAsync(Path.Combine(directory, "podcast.json"), JsonSerializer.Serialize(podcast), ct); + ct.ThrowIfCancellationRequested(); + broadcaster = new PodcastBroadcaster(api, podcast, vlc, player, media, directory); + if (!player.Play(media)) + throw new InvalidOperationException("LibVLC ne peut pas demarrer la capture. Verifiez la camera et les encodeurs H.264/AAC."); + broadcaster.Completion = broadcaster.UploadAsync(broadcaster._cancellation.Token); + return broadcaster; + } + catch (Exception startupError) + { + if (broadcaster is not null) + { + broadcaster._player.EncounteredError -= broadcaster.OnEncoderError; + broadcaster._cancellation.Dispose(); + broadcaster._stopGate.Dispose(); + } + player?.Dispose(); + media?.Dispose(); + vlc.Dispose(); + if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true); + if (podcast is not null) + { + try + { + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15)); + await api.CompleteAsync(podcast.Id, timeout.Token); + } + catch (Exception finishError) + { + throw new AggregateException("Capture impossible ; le direct serveur expirera automatiquement.", startupError, finishError); + } + } + throw; + } + } + + public async Task StopAsync() + { + await StopEncoderAsync(); + await Completion; + } + + private async Task StopEncoderAsync() + { + await _stopGate.WaitAsync(); + try + { + if (!_stopped) + { + await Task.Run(_player.Stop); + _stopped = true; + } + } + finally { _stopGate.Release(); } + } + + private async Task UploadAsync(CancellationToken ct) + { + var next = 0; + var lastSegment = Stopwatch.StartNew(); + try + { + while (true) + { + ct.ThrowIfCancellationRequested(); + var finalPass = _stopped; + if (_encoderFailed) + throw new InvalidOperationException("La capture LibVLC a echoue. Verifiez camera, microphone et encodeurs H.264/AAC."); + var pendingBytes = Directory.EnumerateFiles(_directory, "*.ts").Sum(path => new FileInfo(path).Length); + if (pendingBytes > 64L * 1024 * 1024) + throw new IOException("Le reseau est trop lent : la capture depasse 64 Mio de segments en attente."); + var playlist = Path.Combine(_directory, "index.m3u8"); + if (File.Exists(playlist)) + { + using var reader = new StreamReader(new FileStream(playlist, FileMode.Open, + FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)); + var segments = HlsCapturePlaylist.Parse(await reader.ReadToEndAsync(ct)); + foreach (var segment in segments.Where(s => s.Sequence >= next)) + { + if (segment.Sequence != next) + throw new InvalidDataException("Un segment manque dans la capture."); + var path = Path.Combine(_directory, segment.FileName); + if (new FileInfo(path).Length > 8 * 1024 * 1024) + throw new IOException("Un segment de capture depasse 8 Mio."); + var bytes = await File.ReadAllBytesAsync(path, ct); + var response = await UploadWithRetryAsync(segment, bytes, ct); + Podcast = response; + File.Delete(path); + next++; + lastSegment.Restart(); + Progress?.Invoke(response); + } + } + if (finalPass) + { + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeout.CancelAfter(TimeSpan.FromSeconds(20)); + Podcast = await _api.CompleteAsync(Podcast.Id, timeout.Token); + return; + } + if (lastSegment.Elapsed > TimeSpan.FromSeconds(25)) + throw new IOException("Aucun segment produit depuis 25 secondes. Verifiez la capture et les encodeurs LibVLC."); + await Task.Delay(400, ct); + } + } + finally + { + // Stop the camera even when uploading or parsing fails. + await StopEncoderAsync(); + } + } + + private async Task UploadWithRetryAsync(CaptureSegment segment, byte[] bytes, CancellationToken ct) + { + for (var attempt = 0; ; attempt++) + { + try + { + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeout.CancelAfter(TimeSpan.FromSeconds(10)); + return await _api.UploadAsync(Podcast.Id, segment.Sequence, segment.DurationSeconds, bytes, timeout.Token); + } + catch (HttpRequestException ex) when (attempt < 2 && (ex.StatusCode is null + or HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests || (int)ex.StatusCode >= 500)) + { + await Task.Delay(TimeSpan.FromSeconds(attempt + 1), ct); + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested && attempt < 2) + { + await Task.Delay(TimeSpan.FromSeconds(attempt + 1), ct); + } + } + } + + private void OnEncoderError(object? sender, EventArgs e) => _encoderFailed = true; + + public async ValueTask DisposeAsync() + { + if (_disposed) return; + _disposed = true; + _cancellation.Cancel(); + try { await Completion; } + finally + { + _player.EncounteredError -= OnEncoderError; + _player.Dispose(); + _media.Dispose(); + _vlc.Dispose(); + _cancellation.Dispose(); + _stopGate.Dispose(); + // Keep failed, unacknowledged segments for manual recovery. + if (Completion.IsCompletedSuccessfully && Directory.Exists(_directory)) + Directory.Delete(_directory, recursive: true); + } + } +} diff --git a/src/PostIt/PostIt/Services/PodcastMediaInput.cs b/src/PostIt/PostIt/Services/PodcastMediaInput.cs new file mode 100644 index 000000000..1cd5657eb --- /dev/null +++ b/src/PostIt/PostIt/Services/PodcastMediaInput.cs @@ -0,0 +1,78 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using LibVLCSharp.Shared; +using Yavsc.Abstract.Streaming; +using Yavsc.Api.Client; + +namespace PostIt.Services; + +// LibVLC consumes a sequential transport stream; credentials stay in the existing HTTP transport. +public sealed class PodcastMediaInput(PodcastApiClient api, Guid id, bool live) : MediaInput +{ + private readonly CancellationTokenSource _cancellation = new(); + private byte[] _segment = Array.Empty(); + private int _offset; + private int _sequence = -1; + public event Action? Failed; + + public override bool Open(out ulong size) + { + size = ulong.MaxValue; + return true; + } + + public override int Read(IntPtr buffer, uint length) + { + if (length == 0) return 0; + try + { + var ct = _cancellation.Token; + ct.ThrowIfCancellationRequested(); + while (_offset == _segment.Length) + { + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeout.CancelAfter(TimeSpan.FromSeconds(20)); + var manifest = api.GetAsync(id, timeout.Token).GetAwaiter().GetResult(); + if (_sequence < 0) + _sequence = live ? Math.Max(0, manifest.Segments.Count - 3) : 0; + if (_sequence < manifest.Segments.Count) + { + _segment = api.DownloadSegmentAsync(id, _sequence, timeout.Token).GetAwaiter().GetResult(); + if (_segment.Length == 0 || _segment.Length != manifest.Segments[_sequence].Bytes) + throw new InvalidOperationException("Segment podcast incomplet."); + _sequence++; + _offset = 0; + break; + } + if (manifest.Podcast.State != PodcastState.Live) return 0; + if (ct.WaitHandle.WaitOne(700)) ct.ThrowIfCancellationRequested(); + } + var count = (int)Math.Min(length, (uint)(_segment.Length - _offset)); + Marshal.Copy(_segment, _offset, buffer, count); + _offset += count; + return count; + } + catch (OperationCanceledException) when (_cancellation.IsCancellationRequested) { return -1; } + // No managed exception may escape a native LibVLC callback. + catch (Exception ex) + { + Failed?.Invoke($"Lecture du podcast interrompue : {ex.Message}"); + return -1; + } + } + + public override bool Seek(ulong offset) => false; + public override void Close() { } + public void Cancel() => _cancellation.Cancel(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _cancellation.Cancel(); + _cancellation.Dispose(); + } + base.Dispose(disposing); + } +} diff --git a/src/PostIt/PostIt/Services/YavscApiClient.cs b/src/PostIt/PostIt/Services/YavscApiClient.cs index 296e8b3b1..c19b61f20 100644 --- a/src/PostIt/PostIt/Services/YavscApiClient.cs +++ b/src/PostIt/PostIt/Services/YavscApiClient.cs @@ -540,8 +540,11 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable response = await sendOnce().ConfigureAwait(false); } - await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false); - return await response.Content.ReadAsByteArrayAsync(ct).ConfigureAwait(false); + using (response) + { + await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false); + return await response.Content.ReadAsByteArrayAsync(ct).ConfigureAwait(false); + } } public async Task LogoutAsync() diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 5807df93a..eff0094a5 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -43,6 +43,7 @@ public class ViewLocator : IDataTemplate { BlogsViewModel => services.GetRequiredService(), VideoPageViewModel => services.GetRequiredService(), + PodcastsPageViewModel => services.GetRequiredService(), Settings => services.GetRequiredService(), AboutPageViewModel => services.GetRequiredService(), HomePageViewModel => services.GetRequiredService(), diff --git a/src/PostIt/PostIt/ViewModels/Blogs/VideoPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Blogs/VideoPageViewModel.cs index 9ce6e8f17..e8fc1e535 100644 --- a/src/PostIt/PostIt/ViewModels/Blogs/VideoPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Blogs/VideoPageViewModel.cs @@ -1,4 +1,6 @@ using System; +using Yavsc.Abstract.Streaming; +using Yavsc.Api.Client; namespace PostIt.ViewModels; @@ -6,7 +8,10 @@ public sealed class VideoPageViewModel(Uri source, string mimeType) : ViewModelB { public Uri Source { get; } = source; public string MimeType { get; } = mimeType; - public string Title => Uri.UnescapeDataString(System.IO.Path.GetFileName(Source.AbsolutePath)); + public PodcastApiClient? PodcastClient { get; init; } + public PodcastResponse? Podcast { get; init; } + public bool CanOpenExternal => Podcast is null; + public string Title => Podcast?.Title ?? Uri.UnescapeDataString(System.IO.Path.GetFileName(Source.AbsolutePath)); public override bool CanNavigateNext { get => false; protected set { } } public override bool CanNavigatePrevious { get => true; protected set { } } } diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs index 253ac17c0..d2e023fd4 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs @@ -74,10 +74,19 @@ public class HomePageViewModel : ViewModelBase OpenPerformerConfiguration = new AsyncRelayCommand(OpenPerformerConfigurationAsync); OpenAdministration = new AsyncRelayCommand(OpenAdministrationAsync, () => SessionStatus.IsAdministrator); OpenChat = new AsyncRelayCommand(OpenChatAsync); + OpenPodcasts = new AsyncRelayCommand(async () => + { + var app = Application.Current as App ?? throw new InvalidOperationException("Application indisponible."); + var vm = app.ServiceProvider?.GetRequiredService() + ?? throw new InvalidOperationException("Page podcasts indisponible."); + await app.PushPageAsync(vm); + }); } public IAsyncRelayCommand OpenChat { get; } + public IAsyncRelayCommand OpenPodcasts { get; } + public bool SupportsPodcasts => OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); public IAsyncRelayCommand OpenBlogs { get; } public IAsyncRelayCommand OpenProfile { get; } public IAsyncRelayCommand OpenPerformerConfiguration { get; } diff --git a/src/PostIt/PostIt/ViewModels/PodcastsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/PodcastsPageViewModel.cs new file mode 100644 index 000000000..e5375b2d0 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/PodcastsPageViewModel.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.ObjectModel; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LibVLCSharp.Shared; +using PostIt.Services; +using Yavsc.Abstract.Identity; +using Yavsc.Abstract.Streaming; +using Yavsc.Api.Client; + +namespace PostIt.ViewModels; + +public sealed record CaptureDevice(string Name, string Value); + +public partial class PodcastsPageViewModel : ViewModelBase +{ + private readonly PodcastApiClient _client; + private readonly YavscApiClient _api; + private readonly SessionStatusViewModel _session; + private CancellationTokenSource _pageCancellation = new(); + private PodcastBroadcaster? _broadcaster; + private string? _ownerId; + private string _title = ""; + private string _status = ""; + private bool _isBroadcasting; + private bool _busy; + private CaptureDevice? _camera; + private CaptureDevice? _microphone; + private PodcastResponse? _selected; + + public ObservableCollection Podcasts { get; } = new(); + public ObservableCollection Cameras { get; } = new(); + public ObservableCollection Microphones { get; } = new(); + public bool IsDesktop => OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); + + + public string Title { get => _title; set { if (SetProperty(ref _title, value)) UpdateCommands(); } } + + + public string Status { get => _status; private set => SetProperty(ref _status, value); } + public bool IsBroadcasting + { + get => _isBroadcasting; + private set { if (SetProperty(ref _isBroadcasting, value)) UpdateCommands(); } + } + + public bool IsBusy + { + get => _busy; + private set { if (SetProperty(ref _busy, value)) UpdateCommands(); } + } + + public CaptureDevice? SelectedCamera + { + get => _camera; + set { if (SetProperty(ref _camera, value)) UpdateCommands(); } + } + + public CaptureDevice? SelectedMicrophone + { + get => _microphone; + set { if (SetProperty(ref _microphone, value)) UpdateCommands(); } + } + + public PodcastResponse? SelectedPodcast + { + get => _selected; + set { if (SetProperty(ref _selected, value)) UpdateCommands(); } + } + + public IAsyncRelayCommand RefreshCommand { get; } + public IRelayCommand DetectDevicesCommand { get; } + public IAsyncRelayCommand StartCommand { get; } + public IAsyncRelayCommand StopCommand { get; } + public IAsyncRelayCommand PlayCommand { get; } + public IAsyncRelayCommand FinishCommand { get; } + public IAsyncRelayCommand DeleteCommand { get; } + public override bool CanNavigateNext { get => false; protected set { } } + public override bool CanNavigatePrevious { get => !IsBroadcasting; protected set { } } + + public PodcastsPageViewModel(PodcastApiClient client, YavscApiClient api, SessionStatusViewModel session) + { + _client = client; + _api = api; + _session = session; + RefreshCommand = new AsyncRelayCommand(RefreshAsync, () => !IsBusy); + DetectDevicesCommand = new RelayCommand(DetectDevices, () => IsDesktop && !IsBroadcasting && !IsBusy); + StartCommand = new AsyncRelayCommand(StartAsync, + () => IsDesktop && !IsBroadcasting && !IsBusy && _session.IsLoggedIn + && SelectedCamera is not null && !string.IsNullOrWhiteSpace(Title)); + StopCommand = new AsyncRelayCommand(StopAsync, () => IsBroadcasting && !IsBusy); + PlayCommand = new AsyncRelayCommand(PlayAsync, () => SelectedPodcast is { SegmentCount: > 0 } && !IsBroadcasting && !IsBusy); + FinishCommand = new AsyncRelayCommand(FinishAsync, + () => SelectedPodcast is { State: not PodcastState.Completed } p + && p.OwnerId == _ownerId && !IsBroadcasting && !IsBusy); + DeleteCommand = new AsyncRelayCommand(DeleteAsync, + () => SelectedPodcast is { State: not PodcastState.Live } p + && p.OwnerId == _ownerId && !IsBroadcasting && !IsBusy); + } + + private void UpdateCommands() + { + RefreshCommand.NotifyCanExecuteChanged(); + DetectDevicesCommand.NotifyCanExecuteChanged(); + StartCommand.NotifyCanExecuteChanged(); + StopCommand.NotifyCanExecuteChanged(); + PlayCommand.NotifyCanExecuteChanged(); + FinishCommand.NotifyCanExecuteChanged(); + DeleteCommand.NotifyCanExecuteChanged(); + OnPropertyChanged(nameof(CanNavigatePrevious)); + } + + public async Task RefreshAsync() + { + IsBusy = true; + try + { + var profileUrl = new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/"), "account/me"); + _ownerId = (await _api.CallAsync(HttpMethod.Get, profileUrl.AbsoluteUri)).UserId; + var podcasts = await _client.ListAsync(); + Podcasts.Clear(); + foreach (var podcast in podcasts) Podcasts.Add(podcast); + Status = "Directs et replays reserves aux utilisateurs connectes."; + } + catch (Exception ex) { Status = $"Chargement impossible : {ex.Message}"; } + finally { IsBusy = false; } + } + + public async Task ActivateAsync() + { + if (_pageCancellation.IsCancellationRequested) + { + _pageCancellation.Dispose(); + _pageCancellation = new(); + } + await RefreshAsync(); + } + + public async Task DeactivateAsync() + { + _pageCancellation.Cancel(); + if (_broadcaster is not null) await StopAsync(); + } + + private void DetectDevices() + { + try + { + Core.Initialize(); + using var vlc = CameraStreamer.CreatePreferredLibVlc(); + var capture = new CameraStreamer(vlc); + Cameras.Clear(); + foreach (var item in capture.GetCaptureDevices()) Cameras.Add(new(item.Key, item.Value)); + Microphones.Clear(); + Microphones.Add(new("Sans microphone", "")); + foreach (var item in capture.GetAudioCaptureDevices()) Microphones.Add(new(item.Key, item.Value)); + SelectedCamera = Cameras.Count == 0 ? null : Cameras[0]; + SelectedMicrophone = Microphones.Count > 1 ? Microphones[1] : Microphones[0]; + Status = Cameras.Count == 0 ? "Aucune camera detectee." : "Choisissez camera et microphone avant de demarrer."; + } + catch (Exception ex) { Status = $"Detection impossible : {ex.Message}. Verifiez l'installation de LibVLC."; } + } + + private async Task StartAsync() + { + IsBusy = true; + try + { + if (SelectedCamera is null) throw new InvalidOperationException("Choisissez une camera."); + _broadcaster = await PodcastBroadcaster.StartAsync(_client, Title, SelectedCamera.Value, + string.IsNullOrWhiteSpace(SelectedMicrophone?.Value) ? null : SelectedMicrophone.Value, _pageCancellation.Token); + _broadcaster.Progress += OnProgress; + _session.LogoutCompleted += OnLogout; + IsBroadcasting = true; + Status = "Capture en cours ; attente du premier segment HLS."; + _ = ObserveAsync(_broadcaster); + } + catch (Exception ex) { Status = $"Demarrage impossible : {ex.Message}"; } + finally { IsBusy = false; } + } + + private void OnProgress(PodcastResponse podcast) + => Dispatcher.UIThread.Post(() => + { + if (_broadcaster?.Podcast.Id == podcast.Id) + Status = $"En direct : {podcast.SegmentCount} segments, {podcast.DurationSeconds:0} s envoyees."; + }); + + private async Task ObserveAsync(PodcastBroadcaster broadcaster) + { + try + { + await broadcaster.Completion; + Status = broadcaster.Podcast.SegmentCount > 0 + ? "Direct termine. Le replay est disponible." + : "Direct termine sans segment recu : aucun replay a lire."; + } + catch (Exception ex) + { + Status = $"Diffusion interrompue : {ex.Message}. Les segments recus restent disponibles ; les segments non envoyes sont conserves localement."; + } + finally + { + broadcaster.Progress -= OnProgress; + _session.LogoutCompleted -= OnLogout; + try { await broadcaster.DisposeAsync(); } + catch (Exception ex) { Status += $" Nettoyage : {ex.Message}"; } + _broadcaster = null; + IsBroadcasting = false; + } + } + + public async Task StopAsync() + { + IsBusy = true; + try + { + if (_broadcaster is not null) await _broadcaster.StopAsync(); + } + catch (Exception ex) { Status = $"Arret du direct : {ex.Message}"; } + finally { IsBusy = false; } + } + + private async void OnLogout() + { + // Releasing capture is required even after the HTTP credentials have been cleared. + await StopAsync(); + } + + private async Task PlayAsync() + { + try + { + var podcast = SelectedPodcast ?? throw new InvalidOperationException("Selectionnez un podcast."); + var app = Application.Current as App ?? throw new InvalidOperationException("Application indisponible."); + var manifestUrl = new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/"), $"podcasts/{podcast.Id}/index.m3u8"); + await app.PushPageAsync(new VideoPageViewModel(manifestUrl, "application/vnd.apple.mpegurl") + { + PodcastClient = _client, Podcast = podcast + }); + } + catch (Exception ex) { Status = $"Lecture impossible : {ex.Message}"; } + } + + private async Task FinishAsync() + { + IsBusy = true; + try + { + var podcast = SelectedPodcast ?? throw new InvalidOperationException("Selectionnez un podcast."); + await _client.CompleteAsync(podcast.Id); + await RefreshAsync(); + } + catch (Exception ex) { Status = $"Finalisation impossible : {ex.Message}"; } + finally { IsBusy = false; } + } + + private async Task DeleteAsync() + { + IsBusy = true; + try + { + var podcast = SelectedPodcast ?? throw new InvalidOperationException("Selectionnez un podcast."); + await _client.DeleteAsync(podcast.Id); + SelectedPodcast = null; + await RefreshAsync(); + } + catch (Exception ex) { Status = $"Suppression impossible : {ex.Message}"; } + finally { IsBusy = false; } + } +} diff --git a/src/PostIt/PostIt/Views/Blogs/VideoPage.axaml b/src/PostIt/PostIt/Views/Blogs/VideoPage.axaml index c00cdf644..4f40934d8 100644 --- a/src/PostIt/PostIt/Views/Blogs/VideoPage.axaml +++ b/src/PostIt/PostIt/Views/Blogs/VideoPage.axaml @@ -7,12 +7,12 @@ x:DataType="vm:VideoPageViewModel"> - +