Files
Titano/Titano/Diagnostics/Mp4Playback.cs
T
Alby96andClaude Opus 5 e476ebb648 Correzione dei difetti emersi sui DNG GoPro reali
Provato il programma sulle quattro sequenze in K:\2024 (oltre 3000 scatti DNG
convertiti da GPR con le applicazioni Adobe). Ne sono usciti quattro difetti,
tutti latenti perché la verifica sintetica usa JPEG piccoli.

1. Risoluzione letta dalla miniatura. Nei DNG la IFD0 descrive un'anteprima
   256x192 marcata NewSubfileType=1 e l'immagine vera vive in una SubIFD:
   leggendo ImageWidth dalla prima directory il programma credeva di lavorare
   su 256x192 e avrebbe esportato un video di quelle dimensioni. Ora si sceglie
   la directory dell'immagine principale, preferendo quelle non marcate come
   versione ridotta.

2. Lettura dei metadati lentissima. Si leggevano 4 MiB per file a prescindere:
   179 s per una cartella da 1005 scatti. La finestra sul file ora si estende
   solo quando un offset punta oltre quanto gia' caricato, e di un DNG da 15 MiB
   ne bastano 128 KiB. Stessa cartella: 7,4 s.

3. Decodifica corrotta quando interveniva il ridimensionamento. Il convertitore
   a 48bppRGB precedeva lo scaler; con i codec RAW quell'ordine restituisce
   righe disallineate, immagini a strisce e luminanza sbagliata di due stop,
   senza segnalare alcun errore. Lo scaler ora precede il convertitore, come
   indica Microsoft. I 16 bit per canale restano.

4. Multiplexer che corrompeva il primo fotogramma chiave. Il buffer di
   conversione Annex-B veniva riallocato senza copiare il contenuto: le NAL gia'
   scritte per quel campione diventavano zeri e il campione usciva con un
   prefisso di lunghezza nullo in testa. Il file restava formalmente valido ma
   il lettore di sistema si fermava dopo sedici fotogrammi su quaranta. Con i
   JPEG del test il campione non superava mai la capacita' iniziale, per questo
   non era mai emerso; ora il buffer parte piccolo, cosi' il percorso di crescita
   viene esercitato da qualunque sequenza.

Aggiunto inoltre il vincolo di conformita' dei codec. Una sorgente 4:3 da
4000x3000 supera il Livello 5.2 di H.264: l'encoder hardware la accetta e
dichiara il Livello 6.0, ma i decodificatori comuni non aprono il file. La
risoluzione viene ora ricondotta al massimo riproducibile conservando le
proporzioni, e la riduzione e' dichiarata nella barra di stato invece di
avvenire in silenzio. Le sorgenti 16:9 fino al 4K UHD non sono toccate.

Il campo di movimento non viene piu' calcolato quando non serve: con pose da
30 s su intervalli da 34 s lo shutter angle e' gia' 317 gradi e non c'e'
sfocatura da sintetizzare. Il render della sequenza aurora passa da 1,3 a
8,5 fotogrammi al secondo.

La verifica del motore sale a 24 controlli: si aggiungono l'invarianza della
luminanza alla scala di decodifica e l'allineamento delle NAL dentro ogni
campione, i due invarianti che avrebbero intercettato i difetti 3 e 4. Nuovo
comando --diagnose per esaminare una cartella reale.

Verificato sui file dell'utente: 40 fotogrammi a risoluzione nativa, H.264 e
HEVC, entrambi riletti per intero dal lettore di sistema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:01:35 +02:00

189 lines
7.4 KiB
C#

using System.Runtime.InteropServices;
using Titano.Video;
namespace Titano.Diagnostics;
[ComImport, Guid("70ae66f2-c809-4e4f-8915-bdcb406b7993"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IMFSourceReader
{
[PreserveSig] int GetStreamSelection(uint streamIndex, [MarshalAs(UnmanagedType.Bool)] out bool selected);
[PreserveSig] int SetStreamSelection(uint streamIndex, [MarshalAs(UnmanagedType.Bool)] bool selected);
[PreserveSig] int GetNativeMediaType(uint streamIndex, uint typeIndex, out IMFMediaType type);
[PreserveSig] int GetCurrentMediaType(uint streamIndex, out IMFMediaType type);
[PreserveSig] int SetCurrentMediaType(uint streamIndex, IntPtr reserved, IMFMediaType type);
[PreserveSig] int SetCurrentPosition(ref Guid timeFormat, ref PropVariant position);
[PreserveSig] int ReadSample(uint streamIndex, uint controlFlags, out uint actualStreamIndex,
out uint streamFlags, out long timestamp, out IMFSample? sample);
[PreserveSig] int Flush(uint streamIndex);
[PreserveSig] int GetServiceForStream(uint streamIndex, ref Guid service, ref Guid riid, out IntPtr instance);
[PreserveSig] int GetPresentationAttribute(uint streamIndex, ref Guid attribute, IntPtr value);
}
/// <summary>
/// Rilegge un MP4 prodotto dal multiplexer usando il lettore di sistema: è la prova che il
/// contenitore non è soltanto ben formato sulla carta, ma effettivamente riproducibile.
/// </summary>
internal static class Mp4Playback
{
private const uint FirstVideoStream = 0xFFFFFFFC;
private const uint AllStreams = 0xFFFFFFFE;
private const uint EndOfStream = 0x00000002;
private static Guid VideoFormatRgb32 = new("00000016-0000-0010-8000-00aa00389b71");
private static Guid EnableVideoProcessing = new("fb394f3d-ccf1-42ee-bbb3-f9b845d5681d");
[DllImport("mfreadwrite.dll", ExactSpelling = true)]
private static extern int MFCreateSourceReaderFromURL([MarshalAs(UnmanagedType.LPWStr)] string url,
IMFAttributes? attributes, out IMFSourceReader reader);
[DllImport("mfplat.dll", ExactSpelling = true)]
private static extern int MFCreateAttributes(out IMFAttributes attributes, uint initialSize);
public sealed record Playback(int FrameCount, int Width, int Height, List<double> MeanLuma, string? Error);
public static Playback Read(string path, int maxFrames = 4096)
{
MediaFoundationRuntime.Startup();
IMFAttributes? attributes = null;
IMFSourceReader? reader = null;
try
{
if (MFCreateAttributes(out attributes, 1) >= 0)
{
var key = EnableVideoProcessing;
attributes.SetUINT32(ref key, 1);
}
int hr = MFCreateSourceReaderFromURL(path, attributes, out reader);
if (hr < 0) return new Playback(0, 0, 0, [], $"apertura non riuscita (HRESULT 0x{hr:X8})");
reader.SetStreamSelection(AllStreams, false);
reader.SetStreamSelection(FirstVideoStream, true);
int width = 0, height = 0;
if (reader.GetNativeMediaType(FirstVideoStream, 0, out IMFMediaType native) >= 0)
{
try
{
var sizeKey = MediaFoundation.MtFrameSize;
if (native.GetUINT64(ref sizeKey, out ulong packed) >= 0)
{
width = (int)(packed >> 32);
height = (int)(packed & 0xFFFFFFFF);
}
}
finally
{
Marshal.ReleaseComObject(native);
}
}
// Si richiede RGB a 8 bit: il lettore inserisce da sé il convertitore di formato.
if (MediaFoundation.MFCreateMediaType(out IMFMediaType target) >= 0)
{
try
{
var majorKey = MediaFoundation.MtMajorType;
var majorValue = MediaFoundation.MajorTypeVideo;
target.SetGUID(ref majorKey, ref majorValue);
var subKey = MediaFoundation.MtSubtype;
target.SetGUID(ref subKey, ref VideoFormatRgb32);
reader.SetCurrentMediaType(FirstVideoStream, IntPtr.Zero, target);
}
finally
{
Marshal.ReleaseComObject(target);
}
}
var luma = new List<double>();
int frames = 0;
int emptyReads = 0;
uint flags = 0;
while (frames < maxFrames)
{
hr = reader.ReadSample(FirstVideoStream, 0, out _, out flags, out _, out IMFSample? sample);
if (hr < 0) return new Playback(frames, width, height, luma, $"lettura interrotta (HRESULT 0x{hr:X8})");
if (sample is not null)
{
emptyReads = 0;
try
{
luma.Add(MeasureMeanLuma(sample, width, height));
frames++;
}
finally
{
Marshal.ReleaseComObject(sample);
}
}
else if (++emptyReads > 64)
{
// Il lettore restituisce campioni nulli senza mai segnalare la fine:
// meglio fermarsi che girare a vuoto.
return new Playback(frames, width, height, luma,
$"{frames} fotogrammi, poi solo campioni nulli (flag 0x{flags:X})");
}
if ((flags & EndOfStream) != 0) break;
}
return new Playback(frames, width, height, luma,
frames == 0 ? $"nessun fotogramma decodificato (flag 0x{flags:X})" : null);
}
catch (Exception ex)
{
return new Playback(0, 0, 0, [], ex.Message);
}
finally
{
if (reader is not null) Marshal.ReleaseComObject(reader);
if (attributes is not null) Marshal.ReleaseComObject(attributes);
}
}
private static unsafe double MeasureMeanLuma(IMFSample sample, int width, int height)
{
sample.ConvertToContiguousBuffer(out IMFMediaBuffer buffer);
try
{
buffer.Lock(out IntPtr pointer, out _, out uint length);
try
{
if (length == 0 || width <= 0 || height <= 0) return 0;
int stride = (int)(length / Math.Max(1, height));
if (stride < width * 4) return 0;
byte* data = (byte*)pointer;
double sum = 0;
int count = 0;
for (int y = 0; y < height; y += 4)
{
byte* row = data + (long)y * stride;
for (int x = 0; x < width; x += 4)
{
byte* pixel = row + x * 4; // BGRX
sum += 0.2126 * pixel[2] + 0.7152 * pixel[1] + 0.0722 * pixel[0];
count++;
}
}
return count == 0 ? 0 : sum / count / 255.0;
}
finally
{
buffer.Unlock();
}
}
finally
{
Marshal.ReleaseComObject(buffer);
}
}
}