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>
192 lines
7.3 KiB
C#
192 lines
7.3 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
|
|
namespace Titano.Diagnostics;
|
|
|
|
/// <summary>
|
|
/// Verificatore della struttura ISO-BMFF prodotta dal multiplexer: percorre l'albero dei box
|
|
/// e ne estrae i valori che devono risultare coerenti (numero di campioni, dimensioni,
|
|
/// presenza della configurazione del codec).
|
|
/// </summary>
|
|
internal static class Mp4Inspector
|
|
{
|
|
public sealed record Report(
|
|
bool Valid,
|
|
string Summary,
|
|
int SampleCount,
|
|
int Width,
|
|
int Height,
|
|
uint Timescale,
|
|
long MediaDuration,
|
|
int CodecConfigBytes,
|
|
List<string> TopLevelBoxes,
|
|
string? SampleProblem);
|
|
|
|
private static readonly string[] Containers =
|
|
["moov", "trak", "mdia", "minf", "stbl", "edts", "dinf", "avc1", "hvc1"];
|
|
|
|
public static Report Inspect(string path)
|
|
{
|
|
var boxes = new List<string>();
|
|
var sampleSizes = new List<uint>();
|
|
long firstChunkOffset = -1;
|
|
int sampleCount = 0, width = 0, height = 0, configBytes = 0;
|
|
uint timescale = 0;
|
|
long mediaDuration = 0;
|
|
var problems = new List<string>();
|
|
|
|
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
|
long length = stream.Length;
|
|
|
|
Walk(stream, 0, length, 0);
|
|
|
|
bool hasFtyp = boxes.Contains("ftyp");
|
|
bool hasMdat = boxes.Contains("mdat");
|
|
bool hasMoov = boxes.Contains("moov");
|
|
|
|
if (!hasFtyp) problems.Add("box ftyp assente");
|
|
if (!hasMdat) problems.Add("box mdat assente");
|
|
if (!hasMoov) problems.Add("box moov assente");
|
|
if (sampleCount == 0) problems.Add("tabella stsz vuota");
|
|
if (configBytes == 0) problems.Add("configurazione del codec assente");
|
|
|
|
string? sampleProblem = VerifySampleData(stream, sampleSizes, firstChunkOffset);
|
|
if (sampleProblem is not null) problems.Add(sampleProblem);
|
|
|
|
string summary = problems.Count == 0
|
|
? "struttura conforme"
|
|
: string.Join(", ", problems);
|
|
|
|
return new Report(problems.Count == 0, summary, sampleCount, width, height,
|
|
timescale, mediaDuration, configBytes, boxes, sampleProblem);
|
|
|
|
void Walk(FileStream file, long start, long end, int depth)
|
|
{
|
|
Span<byte> header = stackalloc byte[8];
|
|
long position = start;
|
|
|
|
while (position + 8 <= end && depth < 8)
|
|
{
|
|
file.Position = position;
|
|
if (file.Read(header) != 8) return;
|
|
|
|
long size = BinaryPrimitives.ReadUInt32BigEndian(header);
|
|
string type = Encoding.ASCII.GetString(header[4..]);
|
|
long payload = position + 8;
|
|
|
|
if (size == 1)
|
|
{
|
|
if (file.Read(header) != 8) return;
|
|
size = BinaryPrimitives.ReadInt64BigEndian(header);
|
|
payload += 8;
|
|
}
|
|
else if (size == 0)
|
|
{
|
|
size = end - position;
|
|
}
|
|
|
|
if (size < 8 || position + size > end) return;
|
|
if (depth == 0) boxes.Add(type);
|
|
|
|
switch (type)
|
|
{
|
|
case "stsz":
|
|
{
|
|
file.Position = payload + 8; // versione/flag + sample_size
|
|
sampleCount = (int)ReadUInt32(file);
|
|
for (int s = 0; s < sampleCount && s < 100_000; s++) sampleSizes.Add(ReadUInt32(file));
|
|
break;
|
|
}
|
|
case "co64":
|
|
file.Position = payload + 8; // versione/flag + entry_count
|
|
firstChunkOffset = (long)ReadUInt64(file);
|
|
break;
|
|
case "stco":
|
|
file.Position = payload + 8;
|
|
firstChunkOffset = ReadUInt32(file);
|
|
break;
|
|
case "mdhd":
|
|
file.Position = payload + 12; // versione/flag + due timestamp
|
|
timescale = ReadUInt32(file);
|
|
mediaDuration = ReadUInt32(file);
|
|
break;
|
|
case "avcC" or "hvcC":
|
|
configBytes = (int)(size - (payload - position));
|
|
break;
|
|
case "avc1" or "hvc1":
|
|
file.Position = payload + 24;
|
|
width = ReadUInt16(file);
|
|
height = ReadUInt16(file);
|
|
Walk(file, payload + 78, position + size, depth + 1);
|
|
break;
|
|
}
|
|
|
|
if (Containers.Contains(type) && type is not ("avc1" or "hvc1"))
|
|
{
|
|
long childStart = type == "stsd" ? payload + 8 : payload;
|
|
Walk(file, childStart, position + size, depth + 1);
|
|
}
|
|
else if (type == "stsd")
|
|
{
|
|
Walk(file, payload + 8, position + size, depth + 1);
|
|
}
|
|
|
|
position += size;
|
|
}
|
|
}
|
|
|
|
static ulong ReadUInt64(FileStream file)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[8];
|
|
return file.Read(buffer) == 8 ? BinaryPrimitives.ReadUInt64BigEndian(buffer) : 0;
|
|
}
|
|
|
|
static uint ReadUInt32(FileStream file)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[4];
|
|
return file.Read(buffer) == 4 ? BinaryPrimitives.ReadUInt32BigEndian(buffer) : 0;
|
|
}
|
|
|
|
static ushort ReadUInt16(FileStream file)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[2];
|
|
return file.Read(buffer) == 2 ? BinaryPrimitives.ReadUInt16BigEndian(buffer) : (ushort)0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica che ogni campione sia una successione esatta di NAL con lunghezza prefissata.
|
|
/// Un prefisso nullo o che sborda dal campione significa dati corrotti dal multiplexer:
|
|
/// il contenitore resterebbe formalmente valido ma il flusso sarebbe indecodificabile.
|
|
/// </summary>
|
|
private static string? VerifySampleData(FileStream stream, List<uint> sizes, long firstChunkOffset)
|
|
{
|
|
if (sizes.Count == 0 || firstChunkOffset < 0) return null;
|
|
|
|
long position = firstChunkOffset;
|
|
Span<byte> prefix = stackalloc byte[4];
|
|
|
|
for (int index = 0; index < sizes.Count; index++)
|
|
{
|
|
long end = position + sizes[index];
|
|
if (end > stream.Length) return $"campione {index + 1} oltre la fine del file";
|
|
|
|
long cursor = position;
|
|
while (cursor + 4 <= end)
|
|
{
|
|
stream.Position = cursor;
|
|
if (stream.Read(prefix) != 4) return $"campione {index + 1} troncato";
|
|
|
|
uint length = BinaryPrimitives.ReadUInt32BigEndian(prefix);
|
|
if (length == 0) return $"campione {index + 1}: NAL di lunghezza nulla";
|
|
if (cursor + 4 + length > end) return $"campione {index + 1}: NAL che sborda dal campione";
|
|
cursor += 4 + length;
|
|
}
|
|
|
|
if (cursor != end) return $"campione {index + 1}: {end - cursor} byte residui non allineati";
|
|
position = end;
|
|
}
|
|
return null;
|
|
}
|
|
}
|