fix(android): correct camera frame rotation and recorder timestamps
All checks were successful
Dotnet build and test / build (push) Successful in 9m59s
Forgejo Release / release (push) Successful in 9m41s

Rotate camera frames before encoding while accounting for Camera2 texture transforms. Align camera timestamps with MediaRecorder and cover capture duration, tracks and rotation with regression tests. Include LibVLC ProGuard rules and update documentation and changelog.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-24 23:09:37 +01:00
commit ed75f06fb2
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
14 changed files with 792 additions and 8 deletions

View file

@ -1,11 +1,13 @@
#if ANDROID
using System;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Media;
using Android.OS;
using Avalonia.Threading;
using PostIt.Android;
@ -34,7 +36,7 @@ public sealed class AndroidPodcastCaptureTestActivity : Activity
try
{
await RunAsync(result);
File.WriteAllText(resultPath, $"PASS\nsegments={result.GetInt("segments")}\ndirectory={result.GetString("directory")}\nbackground={result.GetString("background")}");
File.WriteAllText(resultPath, $"PASS\nsegments={result.GetInt("segments")}\ndurationSeconds={result.GetDouble("durationSeconds")}\naudio={result.GetBoolean("audio")}\ndirectory={result.GetString("directory")}\nbackground={result.GetString("background")}\norientation={result.GetString("orientation")}\ntextureTransform={result.GetString("textureTransform")}");
}
catch (Exception ex)
{
@ -68,6 +70,11 @@ public sealed class AndroidPodcastCaptureTestActivity : Activity
await Task.Delay(9000);
var after = SegmentCount(directory);
if (capture.Completion.IsFaulted) await capture.Completion;
if (capture is AndroidPodcastCapture androidCapture)
{
result.PutString("orientation", androidCapture.OrientationDiagnostics);
result.PutString("textureTransform", androidCapture.TextureTransform);
}
if (after <= before) throw new InvalidOperationException($"Capture did not continue in background: {before} -> {after} segments.");
if (_notificationStop)
{
@ -81,7 +88,25 @@ public sealed class AndroidPodcastCaptureTestActivity : Activity
await capture.Completion.WaitAsync(TimeSpan.FromSeconds(15));
var segments = HlsCapturePlaylist.Parse(await File.ReadAllTextAsync(Path.Combine(directory, "index.m3u8")));
if (segments.Count < 3) throw new InvalidOperationException("Insufficient capture segments.");
var duration = segments.Sum(segment => segment.DurationSeconds);
if (duration < 8 || duration > 16)
throw new InvalidOperationException($"Expected 8-16 seconds of capture, got {duration}.");
using var extractor = new MediaExtractor();
extractor.SetDataSource(Path.Combine(directory, segments[0].FileName));
var video = false;
var audio = false;
for (var track = 0; track < extractor.TrackCount; track++)
{
using var format = extractor.GetTrackFormat(track);
var mime = format.GetString(MediaFormat.KeyMime);
video |= mime == "video/avc";
audio |= mime == "audio/mp4a-latm";
}
if (!video || (_microphone && !audio))
throw new InvalidOperationException($"Missing capture tracks: H.264={video}, AAC={audio}.");
result.PutInt("segments", segments.Count);
result.PutDouble("durationSeconds", duration);
result.PutBoolean("audio", audio);
result.PutString("directory", directory);
result.PutString("background", $"segments {before} -> {after}");
}