diff --git a/CHANGELOG.md b/CHANGELOG.md index 79684a2c7..f0cd45fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added +* [PostIt.Android] Keep the screen awake during foreground video playback and release it on pause, stop, completion, error or video teardown. Add a persisted, default-off podcast option to keep the screen awake during camera preview/live capture; release on capture end and allow normal sleep in the background without changing the broadcast service. Include native LibVLC libraries when building with a single runtime identifier (including physical ARM64 phones). +* [PostIt/Podcasts] Preview the selected camera before starting a live broadcast, without creating an emission, encoding files or opening the microphone. Release preview on stop, navigation, logout and Android backgrounding, and before starting the actual broadcast. Correct Android portrait orientation for rear and front cameras: subtract the sensor rotation already carried by Camera2's SurfaceTexture matrix, reversing the remaining rotation when that matrix mirrors the front camera (it was turned 90° or upside down); track display changes for both preview and encoded frames, preserving aspect ratio and fixed live stream dimensions. * [PostIt] Schedule silent OIDC renewal independently of API traffic and SignalR reconnects, while the app is active or background chat is enabled. Derive the wait from the access-token expiration minus the existing 60-second safety margin instead of polling; recalculate after token rotation, login/logout, activity and preference changes. Persist rotated tokens and serialize with request-driven renewal/logout. Run the same maintenance in headless Android chat; background execution and absolute refresh-token lifetime remain subject to Android/identity-provider limits. * [PostIt.Android] Request Android's battery optimization exemption when explicitly saving settings with background chat enabled, only if not already exempt. Keep automatic settings saves and reconnects free of prompts; refusal does not disable SignalR. * [PostIt.Android] Add an opt-in ordinary background service for the existing SignalR ChatHub connection, without a permanent notification; show native alerts only for received messages/events and attempt restart after phone startup where Android permits it. Reuse one process-wide connection between service, pages and calls, restore locally remembered rooms per account/host and clean up on opt-out/logout. No Firebase, Google push configuration or server push migration; explain background-data/battery settings and Android execution limits. diff --git a/INSTALL.md b/INSTALL.md index 09dba0d5e..24b9567a1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -180,6 +180,16 @@ the same Avalonia video page as desktop. Catalogue, playback controls and authenticated segment downloads are shared; no external browser, public media URL or camera/microphone permissions are needed to watch a podcast. +Video playback keeps the screen awake while PostIt is visible. Pausing, +stopping, reaching the end, a playback error or leaving the video releases +this request; backgrounding the app always allows the screen to sleep. + +The podcast page offers **Garder l'ecran allume pendant l'apercu camera et le +direct** on Android. This preference is saved and disabled by default. It +only keeps the screen awake during an active camera preview or broadcast +while PostIt is visible, and can be changed during capture. Turning it off +does not stop capture or alter the existing background broadcast service. + Leaving the app stops playback and cancels downloads. Returning recreates the player: a replay starts from the beginning, and a live stream rejoins near the live edge. Seeking and background audio are not supported. diff --git a/doc/architecture/audio-video-calls.md b/doc/architecture/audio-video-calls.md index 0a15880f1..1fb9fc7ef 100644 --- a/doc/architecture/audio-video-calls.md +++ b/doc/architecture/audio-video-calls.md @@ -123,6 +123,10 @@ de fichier média temporaire ou de moteur WebRTC Android ajouté. segments ni de lecture de capture par callbacks managés. - La prévisualisation réutilise la surface EGL existante. Le retour distant utilise la surface Android LibVLC déjà employée pour les Podcasts. + La rotation capteur/écran tient compte de celle déjà portée par la matrice + `SurfaceTexture` (miroir de la caméra avant compris) et suit les changements + portrait/paysage. Les dimensions + encodées restent fixes pendant l'appel, avec bandes noires si nécessaire. - Le service de capture partagé affiche une notification d'appel avec l'action « Raccrocher » ; cette action ne publie aucun Podcast. Un Podcast et un appel ne peuvent pas posséder la capture simultanément. diff --git a/doc/architecture/postit.md b/doc/architecture/postit.md index 625cc1875..7286985b8 100644 --- a/doc/architecture/postit.md +++ b/doc/architecture/postit.md @@ -53,6 +53,40 @@ Le rafraîchissement silencieux de session reste unique par processus. Cette restauration couvre la recréation d'activité dans le même processus, pas la destruction du processus par Android. +## Prévisualisation et orientation des Podcasts + +« Prévisualiser la caméra » ouvre un aperçu local avant « Démarrer le +direct », sans exiger de titre. Cette phase ne crée aucune émission côté +serveur, n'enregistre aucun segment et n'ouvre pas le microphone. Le choix +de caméra est verrouillé pendant l'aperçu ; l'arrêter pour changer de caméra. +Le microphone sélectionné n'est ouvert qu'au démarrage du direct. + +« Arrêter la prévisualisation », la sortie de page et la déconnexion libèrent +la caméra. Le passage au direct ferme d'abord l'aperçu, puis ouvre la capture +avec les périphériques sélectionnés : les images d'essai ne sont pas publiées. +Sous Android, un aperçu seul s'arrête aussi au passage en arrière-plan, +sans lancer de service de capture ni de notification de direct. +Les Podcasts en direct conservent leur service caméra/microphone existant. + +Le chemin Android partage le rendu Camera2/EGL entre aperçu seul et encodage. +La rotation physique utilise l'orientation du capteur, la caméra avant/arrière +et la rotation de l'écran. Camera2 peut déjà inclure la rotation du capteur +dans la matrice `SurfaceTexture`, ainsi qu'un miroir pour la caméra avant : +seule la rotation restante est appliquée, dans le sens inverse si la matrice +contient un miroir, afin de ne pas tourner l'image deux fois. +Un changement de rotation d'écran recalcule ce rendu pendant la capture. +L'aperçu conserve les proportions ; les dimensions encodées restent celles +du démarrage du direct, avec des bandes noires si la nouvelle orientation +l'exige, pour ne pas modifier le format HLS/SRTP en cours d'émission. + +Les tests partagés couvrent l'aperçu sans appel API, le passage au direct, +les arrêts et une sortie de page pendant l'ouverture de caméra. L'activité +Debug `CameraFrameRotationTestActivity` vérifie les quatre rotations avec +encodage, puis l'aperçu sans encodeur et ses changements de rotation. +Une validation physique caméra avant/arrière en portrait et paysage reste +nécessaire : les tests synthétiques ne vérifient pas le comportement du HAL +de chaque téléphone. + ## Administration et profil personnel L'accueil présente un seul accès **Administration**, visible et exécutable diff --git a/doc/testing.md b/doc/testing.md index 5b41ddba3..141162822 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -156,6 +156,13 @@ adb shell am start -n fr.pschneider.postit/fr.pschneider.postit.CameraFrameRotat adb shell run-as fr.pschneider.postit cat files/camera-rotation-test-result.txt ``` +Pour un telephone ARM64, ajouter `-r android-arm64` au build et installer +l'APK de `bin/Debug/net10.0-android/android-arm64/`. Le projet transmet ce RID +aux targets natives LibVLC : l'APK doit contenir `libvlc.so` et +`libc++_shared.so`. Utiliser `adb -s ` si plusieurs appareils sont +connectes. Laisser l'appareil deverrouille et les tests au premier plan ; +ne lancer le test suivant qu'apres `PASS` ou `FAIL`. + Attendre la fin du test : `RUNNING` doit devenir `PASS`. Il encode quatre quadrants colores en MPEG-TS pour 0, 90, 180 et 270 degres, decode une image et verifie les dimensions (640x480 ou 480x640) et les couleurs aux quatre positions. @@ -184,13 +191,36 @@ Le resultat inclut les angles capteur/ecran et la premiere matrice SurfaceTexture recue. Les tests unitaires couvrent notamment la matrice reelle `[0,-1,0,0,-1,0,0,0,0,0,1,0,1,1,0,1]` : pour un capteur a 90 degres et un ecran a 0, elle porte deja la rotation et ne doit pas recevoir 90 degres -supplementaires. Le test synthetique seul ne detecte pas cette double rotation. +supplementaires. La camera avant mesuree +`[0,-1,0,0,1,0,0,0,0,0,1,0,0,1,0,1]` (capteur a 270 degres) contient en plus +un miroir : la rotation restante y est appliquee dans le sens inverse. +Le test synthetique seul ne detecte pas cette double rotation. Verifier aussi manuellement les cameras avant/arriere en portrait et paysage, le son, le direct et le replay, et la poursuite de l'emission en arriere-plan. L'orientation est fixee au demarrage d'une emission ; tourner le telephone pendant l'emission ne reconfigure pas le format encode. +### Anti-veille Android + +`ScreenAwakeRequestsTests`, `PodcastPreferencesTests` et les regressions +d'apercu de `PodcastBroadcasterTests` couvrent les demandes independantes, +leur liberation, le changement d'option pendant la capture et sa persistance. +Le test natif Debug utilise une video MP4 H.264 locale synthetique, sans API, +camera ni microphone : + +```bash +adb push /chemin/video-test.mp4 /data/local/tmp/postit-screen-awake-fixture.mp4 +adb shell run-as fr.pschneider.postit cp /data/local/tmp/postit-screen-awake-fixture.mp4 files/screen-awake-fixture.mp4 +adb shell am start -n fr.pschneider.postit/fr.pschneider.postit.ScreenAwakeTestActivity +adb shell run-as fr.pschneider.postit cat files/screen-awake-test-result.txt +``` + +Attendre `PASS` : lecture, pause/reprise, arret, fin, destruction de la surface, +demandes simultanees, arriere-plan et retour au premier plan sont verifies +sur le drapeau Android `KeepScreenOn`. Les changements de page et le retour +temporaire a l'accueil pendant ce test sont automatiques. + ## 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/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 687b8bf51..0b56b3776 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -39,6 +39,18 @@ public class MainActivity : AvaloniaMainActivity PlatformBootstrap.InitPlatform(); Current = this; base.OnCreate(savedInstanceState); + PostIt.Services.Platform.ScreenAwake.Changed += OnScreenAwakeChanged; + } + + private void OnScreenAwakeChanged() => RunOnUiThread(UpdateScreenAwake); + + private void UpdateScreenAwake() + { + if (Window is not { } window) return; + if (ReferenceEquals(Current, this) && IsForeground && PostIt.Services.Platform.ScreenAwake.IsRequested) + window.AddFlags(WindowManagerFlags.KeepScreenOn); + else + window.ClearFlags(WindowManagerFlags.KeepScreenOn); } /// /// Receives the deep-link Intent fired by the system browser after the @@ -72,17 +84,21 @@ public class MainActivity : AvaloniaMainActivity base.OnResume(); IsForeground = true; PostIt.Services.Platform.IsApplicationActive = true; + UpdateScreenAwake(); } protected override void OnPause() { IsForeground = false; PostIt.Services.Platform.IsApplicationActive = false; + UpdateScreenAwake(); base.OnPause(); } protected override void OnDestroy() { + PostIt.Services.Platform.ScreenAwake.Changed -= OnScreenAwakeChanged; + Window?.ClearFlags(WindowManagerFlags.KeepScreenOn); if (ReferenceEquals(Current, this)) { Current = null; diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs index ca07591a7..9117aea69 100644 --- a/src/PostIt/PostIt.Android/PlatformBootstrap.cs +++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs @@ -34,6 +34,7 @@ internal static class PlatformBootstrap Platform.GetPodcastCaptureDevices = AndroidPodcastCapture.GetDevices; Platform.RequestPodcastPermissionsAsync = AndroidPodcastCapture.RequestPermissionsAsync; Platform.CreatePodcastCaptureAsync = AndroidPodcastCapture.StartAsync; + Platform.CreatePodcastPreviewAsync = AndroidPodcastCapture.StartPreviewAsync; } } } diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 8742f0b38..a2e97677b 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -9,6 +9,8 @@ 1.0 apk false + + $(RuntimeIdentifier) 1.1.0.0 1.1.0.0 1.1.0-feat-calls.1+472.Branch.feat-calls.Sha.d8200cb98ddef152313ef1f102b866c50f107cbd @@ -34,6 +36,7 @@ +