From ad92744a94c829947994ffef5e8a6598f62868f8 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 19 Oct 2026 22:18:22 +0100 Subject: [PATCH] feat(android): share podcast playback and foreground broadcasting Add native Camera2 capture, MPEG-TS segmentation and foreground service with notification stop. Reuse shared authenticated uploads and replay finalization, add capture regression tests and document platform limitations. Update Avalonia to 12.1.4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 21 +- INSTALL.md | 81 +++++ src/PostIt/Directory.Packages.props | 6 +- src/PostIt/PostIt.Android/MainActivity.cs | 32 +- .../PostIt.Android/PlatformBootstrap.cs | 7 + .../PostIt.Android/PostIt.Android.csproj | 5 + .../Properties/AndroidManifest.xml | 9 + .../Services/AndroidPodcastCapture.cs | 309 ++++++++++++++++++ .../Services/AndroidVideoSurface.cs | 37 +++ .../Services/PodcastCaptureService.cs | 109 ++++++ .../Services/PodcastPermissions.cs | 47 +++ .../AndroidPodcastCaptureTestActivity.cs | 100 ++++++ src/PostIt/PostIt.Tests/BlogReaderTests.cs | 24 +- .../PostIt.Tests/PodcastBroadcasterTests.cs | 175 ++++++++++ .../TransportStreamCaptureTests.cs | 146 +++++++++ src/PostIt/PostIt/Services/IPodcastCapture.cs | 16 + .../PostIt/Services/LibVlcPodcastCapture.cs | 62 ++++ src/PostIt/PostIt/Services/Platform.cs | 21 +- .../PostIt/Services/PodcastBroadcaster.cs | 53 ++- .../PostIt/Services/TransportStreamCapture.cs | 222 +++++++++++++ .../ViewModels/PodcastsPageViewModel.cs | 40 ++- src/PostIt/PostIt/Views/Blogs/VideoPage.axaml | 3 +- .../PostIt/Views/Blogs/VideoPage.axaml.cs | 33 +- src/PostIt/PostIt/Views/PodcastsPage.axaml | 7 +- 24 files changed, 1495 insertions(+), 70 deletions(-) create mode 100644 src/PostIt/PostIt.Android/Services/AndroidPodcastCapture.cs create mode 100644 src/PostIt/PostIt.Android/Services/AndroidVideoSurface.cs create mode 100644 src/PostIt/PostIt.Android/Services/PodcastCaptureService.cs create mode 100644 src/PostIt/PostIt.Android/Services/PodcastPermissions.cs create mode 100644 src/PostIt/PostIt.Tests/AndroidPodcastCaptureTestActivity.cs create mode 100644 src/PostIt/PostIt.Tests/PodcastBroadcasterTests.cs create mode 100644 src/PostIt/PostIt.Tests/TransportStreamCaptureTests.cs create mode 100644 src/PostIt/PostIt/Services/IPodcastCapture.cs create mode 100644 src/PostIt/PostIt/Services/LibVlcPodcastCapture.cs create mode 100644 src/PostIt/PostIt/Services/TransportStreamCapture.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f47984c..c431a1099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,26 @@ # Changelog -## [1.0.8-rc28] - unstable +## [1.0.8-rc29] - unstable +### Added + +* [PostIt.Android] Lecture des podcasts directs et replays dans la page + video partagee avec le desktop : surface LibVLC native Android et moteur + embarque, transport authentifie commun et controles uniques. Liberation + du lecteur en arriere-plan ; reouverture du flux au retour dans l'application. + +* [PostIt.Android] Emission depuis la camera et le microphone Android + (Android 8 minimum), encodage H.264/AAC natif et segmentation MPEG-TS + sur images cles. Interface, envoi authentifie, retries et finalisation + du replay partages avec le desktop. Service de premier plan avec + notification d'arret et maintien de la capture en arriere-plan. + +### Changed + +* [PostIt] Mise a jour d'Avalonia vers 12.1.4. + + +## [1.0.8-rc28] - unstable ### Added diff --git a/INSTALL.md b/INSTALL.md index 62b7c15b6..99c4f04b5 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -140,6 +140,87 @@ 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. +### Android podcasts + +The Android APK bundles LibVLC and uses a native Android video surface inside +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. + +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. + +Broadcasting requires **Android 8 (API 26) or newer**. In the shared podcast +page, detect devices, select front/rear camera and optionally the microphone, +then start the live broadcast while PostIt is visible. Camera permission, +microphone permission when selected, and notification permission on Android 13+ +must be granted. A permission denial is reported in the page without starting +capture or creating a server broadcast. Permissions are requested before the +server session is created so a long permission dialog cannot expire it. +Watching does not request these permissions. + +Android uses Camera2 and its native MediaRecorder H.264/AAC encoder, up to 480p at 30 fps, +not the desktop LibVLC camera inputs or software encoders. MPEG-TS is segmented +at IDR frames, with timestamps used for segment durations. Closed segments +enter the same ordered upload/retry/quota/replay pipeline as desktop. +Devices without a supported camera/encoder report a startup error. + +A camera/microphone foreground service and a partial wake lock maintain the +broadcast when PostIt is backgrounded or the screen locks. Its ongoing +notification returns to the app and offers **Arreter et publier**. Stopping +flushes the final segment before completing the replay. Leaving the podcast +page or logging out still stops capture. Android's explicit service stop, +process termination or application restart does not automatically restart a +camera; segments already received by the server remain available under the +interrupted-replay rules above. Device power policies can impose additional +restrictions and must be tested on the intended physical devices. + +**Native library limitation:** bundled `VideoLAN.LibVLC.Android` 3.6.5 emits +`XA0141`: its native libraries are not aligned for 16 KiB memory pages. +Playback on 16 KiB-page devices and store distribution requiring this alignment +are not validated/supported by this package choice. A compatible native LibVLC +release is required before claiming that support; a successful APK build or +a test on a 4 KiB-page emulator does not establish it. + +Build with the Android workload and SDK configured: + +```bash +dotnet build src/PostIt/PostIt.Android/PostIt.Android.csproj +``` + +On an Android device, verify replay video/audio, live playback, pause/resume, +rotation, Back navigation and returning after backgrounding the app. Also +verify an expired session or interrupted network reports a playback error. +For broadcasting, test front/rear cameras, microphone enabled/disabled, +permission denial, screen lock, background capture, notification stop and the +final replay. No automatic retry of a denied permission or a killed camera +service is performed. + +The Debug APK includes a local native-capture test activity (excluded from +Release); it does not create a server podcast. On a dedicated emulator, grant +camera, audio and notification permissions, then run: + +```bash +adb shell am start -W -f 0x18000000 -n fr.pschneider.postit/fr.pschneider.postit.PodcastCaptureTestActivity +# Wait about 20 seconds for foreground/background recording and final flush. +adb shell run-as fr.pschneider.postit cat files/podcast-capture-test-result.txt +# Repeat with the second camera, no audio, and the notification stop action: +adb shell am start -W -f 0x18000000 -n fr.pschneider.postit/fr.pschneider.postit.PodcastCaptureTestActivity --es microphone false --es camera front --es notificationStop true +``` + +The result reports the segment count, the background progress and the private +recording directory under `files/podcast-capture-tests`. Recordings remain +there for local decoder inspection and can be removed after the test. + +The implementation was checked on an Android 16 x64 emulator with 4 KiB +pages: camera/microphone capture produced five independently decodable +H.264/AAC segments across foreground/background operation; the second camera +without audio produced six decodable H.264 segments, including screen-off +operation and stop through the notification action. The foreground service +was gone after both tests. These local tests do not replace an authenticated +Android-to-server broadcast/replay test or validation on physical devices. + ## Resources System resources needed to install. diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 3b23fc8fb..a6a656368 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -4,7 +4,7 @@ true - 12.1.3 + 12.1.4 @@ -13,11 +13,13 @@ - + + + diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index f8fd5a0fd..4cf0947eb 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -25,6 +25,7 @@ public class MainActivity : AvaloniaMainActivity /// The current MainActivity instance. /// public static MainActivity? Current { get; private set; } + internal static bool IsForeground { get; private set; } protected override void OnCreate(global::Android.OS.Bundle? savedInstanceState) { @@ -36,8 +37,8 @@ public class MainActivity : AvaloniaMainActivity EmojiCompat.Config config = new FontRequestEmojiCompatConfig(this, fontRequest); EmojiCompat.Init(config); PlatformBootstrap.InitPlatform(); - base.OnCreate(savedInstanceState); Current = this; + base.OnCreate(savedInstanceState); } /// /// Receives the deep-link Intent fired by the system browser after the @@ -60,9 +61,34 @@ public class MainActivity : AvaloniaMainActivity } - public override void OnRequestPermissionsResult(int requestCode, string[]? permissions, Permission[]? grantResults) + protected override void OnStop() { - if (PostIt.Android.Services.AndroidCurrentLocationProvider + PostIt.Services.Platform.SuspendPlayback(); + base.OnStop(); + } + + protected override void OnResume() + { + base.OnResume(); + IsForeground = true; + } + + protected override void OnPause() + { + IsForeground = false; + base.OnPause(); + } + + protected override void OnRestart() + { + base.OnRestart(); + PostIt.Services.Platform.ResumePlayback(); + } + + public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + { + if (PostIt.Android.Services.PodcastPermissions.HandleResult(requestCode, permissions, grantResults) + || PostIt.Android.Services.AndroidCurrentLocationProvider .HandlePermissionResult(requestCode, grantResults)) { return; diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs index 5b90267f0..2648b0f94 100644 --- a/src/PostIt/PostIt.Android/PlatformBootstrap.cs +++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs @@ -21,5 +21,12 @@ internal static class PlatformBootstrap }; Platform.TryGetCurrentLocationAsync = AndroidCurrentLocationProvider.TryGetCurrentLocationAsync; + Platform.CreateVideoSurface = player => new AndroidVideoSurface(player); + if (System.OperatingSystem.IsAndroidVersionAtLeast(26)) + { + Platform.GetPodcastCaptureDevices = AndroidPodcastCapture.GetDevices; + Platform.RequestPodcastPermissionsAsync = AndroidPodcastCapture.RequestPermissionsAsync; + Platform.CreatePodcastCaptureAsync = AndroidPodcastCapture.StartAsync; + } } } diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 18bfec459..6b60f83b6 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -21,12 +21,17 @@ + + + + +