Basic moov and stbl box parsing (for upcoming image sequence support)
diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b7560..5765e7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ - fuzz inputs made with colorist - `.clang-format` file - `avifArray*()` functions for basic dynamic arrays when parsing +- `moov` box parsing ### Changed - Reorganized internal struct avifCodec to accomodate multiple codecs simultaneously (compile time; not exposed to API)
diff --git a/src/read.c b/src/read.c index 2e6e921..022eeb5 100644 --- a/src/read.c +++ b/src/read.c
@@ -61,7 +61,7 @@ // one "item" worth (all iref, iloc, iprp, etc refer to one of these) typedef struct avifItem { - int id; + uint32_t id; uint8_t type[4]; uint32_t offset; uint32_t size; @@ -86,11 +86,85 @@ } avifProperty; AVIF_ARRAY_DECLARE(avifPropertyArray, avifProperty, prop); +// --------------------------------------------------------------------------- +// avifTrack + +typedef struct avifSampleTableChunk +{ + uint64_t offset; + uint64_t size; +} avifSampleTableChunk; +AVIF_ARRAY_DECLARE(avifSampleTableChunkArray, avifSampleTableChunk, chunk); + +typedef struct avifSampleTableSampleToChunk +{ + uint32_t firstChunk; + uint32_t samplesPerChunk; + uint32_t sampleDescriptionIndex; +} avifSampleTableSampleToChunk; +AVIF_ARRAY_DECLARE(avifSampleTableSampleToChunkArray, avifSampleTableSampleToChunk, sampleToChunk); + +typedef struct avifSampleTableSampleSize +{ + uint32_t size; +} avifSampleTableSampleSize; +AVIF_ARRAY_DECLARE(avifSampleTableSampleSizeArray, avifSampleTableSampleSize, sampleSize); + +typedef struct avifSampleTableTimeToSample +{ + uint32_t sampleCount; + uint32_t sampleDelta; +} avifSampleTableTimeToSample; +AVIF_ARRAY_DECLARE(avifSampleTableTimeToSampleArray, avifSampleTableTimeToSample, timeToSample); + +typedef struct avifSampleTable +{ + avifSampleTableChunkArray chunks; + avifSampleTableSampleToChunkArray sampleToChunks; + avifSampleTableSampleSizeArray sampleSizes; + avifSampleTableTimeToSampleArray timeToSamples; +} avifSampleTable; + +avifSampleTable * avifSampleTableCreate() +{ + avifSampleTable * sampleTable = (avifSampleTable *)avifAlloc(sizeof(avifSampleTable)); + memset(sampleTable, 0, sizeof(avifSampleTable)); + avifArrayCreate(&sampleTable->chunks, sizeof(avifSampleTableChunk), 16); + avifArrayCreate(&sampleTable->sampleToChunks, sizeof(avifSampleTableSampleToChunk), 16); + avifArrayCreate(&sampleTable->sampleSizes, sizeof(avifSampleTableSampleSize), 16); + avifArrayCreate(&sampleTable->timeToSamples, sizeof(avifSampleTableTimeToSample), 16); + return sampleTable; +} + +void avifSampleTableDestroy(avifSampleTable * sampleTable) +{ + avifArrayDestroy(&sampleTable->chunks); + avifArrayDestroy(&sampleTable->sampleToChunks); + avifArrayDestroy(&sampleTable->sampleSizes); + avifArrayDestroy(&sampleTable->timeToSamples); + avifFree(sampleTable); +} + +// one video track ("trak" contents) +typedef struct avifTrack +{ + uint32_t id; + uint32_t auxForID; // if non-zero, this item is an auxC plane for Track #{auxForID} + uint32_t mediaTimescale; + uint64_t mediaDuration; + avifSampleTable * sampleTable; +} avifTrack; +AVIF_ARRAY_DECLARE(avifTrackArray, avifTrack, track); + +// --------------------------------------------------------------------------- +// avifData + typedef struct avifData { avifFileType ftyp; avifItemArray items; avifPropertyArray properties; + avifTrackArray tracks; int propertyCount; } avifData; @@ -100,6 +174,7 @@ memset(data, 0, sizeof(avifData)); avifArrayCreate(&data->items, sizeof(avifItem), 8); avifArrayCreate(&data->properties, sizeof(avifProperty), 16); + avifArrayCreate(&data->tracks, sizeof(avifTrack), 2); return data; } @@ -107,10 +182,16 @@ { avifArrayDestroy(&data->items); avifArrayDestroy(&data->properties); + for (uint32_t i = 0; i < data->tracks.count; ++i) { + if (data->tracks.track[i].sampleTable) { + avifSampleTableDestroy(data->tracks.track[i].sampleTable); + } + } + avifArrayDestroy(&data->tracks); avifFree(data); } -avifItem * avifDataFindItem(avifData * data, int itemID) +avifItem * avifDataFindItem(avifData * data, uint32_t itemID) { if (itemID == 0) { return NULL; @@ -494,6 +575,284 @@ return AVIF_TRUE; } +static avifBool avifParseTrackHeaderBox(avifData * data, avifTrack * track, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + uint8_t version; + uint8_t flags[3]; + CHECK(avifStreamReadVersionAndFlags(&s, &version, flags)); + + uint32_t ignored32, trackID; + uint64_t ignored64; + if (version == 1) { + CHECK(avifStreamReadU64(&s, &ignored64)); // unsigned int(64) creation_time; + CHECK(avifStreamReadU64(&s, &ignored64)); // unsigned int(64) modification_time; + CHECK(avifStreamReadU32(&s, &trackID)); // unsigned int(32) track_ID; + } else if (version == 0) { + CHECK(avifStreamReadU32(&s, &ignored32)); // unsigned int(32) creation_time; + CHECK(avifStreamReadU32(&s, &ignored32)); // unsigned int(32) modification_time; + CHECK(avifStreamReadU32(&s, &trackID)); // unsigned int(32) track_ID; + } else { + // Unsupported version + return AVIF_FALSE; + } + + // TODO: support scaling based on width/height track header info? + + track->id = trackID; + return AVIF_TRUE; +} + +static avifBool avifParseMediaHeaderBox(avifData * data, avifTrack * track, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + uint8_t version; + uint8_t flags[3]; + CHECK(avifStreamReadVersionAndFlags(&s, &version, flags)); + + uint32_t ignored32, mediaTimescale, mediaDuration32; + uint64_t ignored64, mediaDuration64; + if (version == 1) { + CHECK(avifStreamReadU64(&s, &ignored64)); // unsigned int(64) creation_time; + CHECK(avifStreamReadU64(&s, &ignored64)); // unsigned int(64) modification_time; + CHECK(avifStreamReadU32(&s, &mediaTimescale)); // unsigned int(32) timescale; + CHECK(avifStreamReadU64(&s, &mediaDuration64)); // unsigned int(64) duration; + track->mediaDuration = mediaDuration64; + } else if (version == 0) { + CHECK(avifStreamReadU32(&s, &ignored32)); // unsigned int(32) creation_time; + CHECK(avifStreamReadU32(&s, &ignored32)); // unsigned int(32) modification_time; + CHECK(avifStreamReadU32(&s, &mediaTimescale)); // unsigned int(32) timescale; + CHECK(avifStreamReadU32(&s, &mediaDuration32)); // unsigned int(32) duration; + track->mediaDuration = (uint64_t)mediaDuration32; + } else { + // Unsupported version + return AVIF_FALSE; + } + + track->mediaTimescale = mediaTimescale; + return AVIF_TRUE; +} + +static avifBool avifParseChunkOffsetBox(avifData * data, avifSampleTable * sampleTable, avifBool largeOffsets, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + CHECK(avifStreamReadAndEnforceVersion(&s, 0)); + + uint32_t entryCount; + CHECK(avifStreamReadU32(&s, &entryCount)); // unsigned int(32) entry_count; + for (uint32_t i = 0; i < entryCount; ++i) { + uint64_t offset; + if (largeOffsets) { + CHECK(avifStreamReadU64(&s, &offset)); // unsigned int(32) chunk_offset; + } else { + uint32_t offset32; + CHECK(avifStreamReadU32(&s, &offset32)); // unsigned int(32) chunk_offset; + offset = (uint64_t)offset32; + } + + avifSampleTableChunk * chunk = (avifSampleTableChunk *)avifArrayPushPtr(&sampleTable->chunks); + chunk->offset = offset; + } + return AVIF_TRUE; +} + +static avifBool avifParseSampleToChunkBox(avifData * data, avifSampleTable * sampleTable, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + CHECK(avifStreamReadAndEnforceVersion(&s, 0)); + + uint32_t entryCount; + CHECK(avifStreamReadU32(&s, &entryCount)); // unsigned int(32) entry_count; + for (uint32_t i = 0; i < entryCount; ++i) { + avifSampleTableSampleToChunk * sampleToChunk = (avifSampleTableSampleToChunk *)avifArrayPushPtr(&sampleTable->sampleToChunks); + CHECK(avifStreamReadU32(&s, &sampleToChunk->firstChunk)); // unsigned int(32) first_chunk; + CHECK(avifStreamReadU32(&s, &sampleToChunk->samplesPerChunk)); // unsigned int(32) samples_per_chunk; + CHECK(avifStreamReadU32(&s, &sampleToChunk->sampleDescriptionIndex)); // unsigned int(32) sample_description_index; + } + return AVIF_TRUE; +} + +static avifBool avifParseSampleSizeBox(avifData * data, avifSampleTable * sampleTable, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + CHECK(avifStreamReadAndEnforceVersion(&s, 0)); + + uint32_t allSamplesSize, entryCount; + CHECK(avifStreamReadU32(&s, &allSamplesSize)); // unsigned int(32) sample_size; + CHECK(avifStreamReadU32(&s, &entryCount)); // unsigned int(32) entry_count; + + for (uint32_t i = 0; i < entryCount; ++i) { + avifSampleTableSampleSize * sampleSize = (avifSampleTableSampleSize *)avifArrayPushPtr(&sampleTable->sampleSizes); + if (allSamplesSize == 0) { + CHECK(avifStreamReadU32(&s, &sampleSize->size)); // unsigned int(32) entry_size; + } else { + // This could be done more efficiently, memory-wise. + sampleSize->size = allSamplesSize; + } + } + return AVIF_TRUE; +} + +static avifBool avifParseTimeToSampleBox(avifData * data, avifSampleTable * sampleTable, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + CHECK(avifStreamReadAndEnforceVersion(&s, 0)); + + uint32_t entryCount; + CHECK(avifStreamReadU32(&s, &entryCount)); // unsigned int(32) entry_count; + + for (uint32_t i = 0; i < entryCount; ++i) { + avifSampleTableTimeToSample * timeToSample = (avifSampleTableTimeToSample *)avifArrayPushPtr(&sampleTable->timeToSamples); + CHECK(avifStreamReadU32(&s, &timeToSample->sampleCount)); // unsigned int(32) sample_count; + CHECK(avifStreamReadU32(&s, &timeToSample->sampleDelta)); // unsigned int(32) sample_delta; + } + return AVIF_TRUE; +} + +static avifBool avifParseSampleTableBox(avifData * data, avifTrack * track, uint8_t * raw, size_t rawLen) +{ + if (track->sampleTable) { + // A TrackBox may only have one SampleTable + return AVIF_FALSE; + } + track->sampleTable = avifSampleTableCreate(); + + BEGIN_STREAM(s, raw, rawLen); + + while (avifStreamHasBytesLeft(&s, 1)) { + avifBoxHeader header; + CHECK(avifStreamReadBoxHeader(&s, &header)); + + if (!memcmp(header.type, "stco", 4)) { + CHECK(avifParseChunkOffsetBox(data, track->sampleTable, AVIF_FALSE, avifStreamCurrent(&s), header.size)); + } else if (!memcmp(header.type, "co64", 4)) { + CHECK(avifParseChunkOffsetBox(data, track->sampleTable, AVIF_TRUE, avifStreamCurrent(&s), header.size)); + } else if (!memcmp(header.type, "stsc", 4)) { + CHECK(avifParseSampleToChunkBox(data, track->sampleTable, avifStreamCurrent(&s), header.size)); + } else if (!memcmp(header.type, "stsz", 4)) { + CHECK(avifParseSampleSizeBox(data, track->sampleTable, avifStreamCurrent(&s), header.size)); + } else if (!memcmp(header.type, "stts", 4)) { + CHECK(avifParseTimeToSampleBox(data, track->sampleTable, avifStreamCurrent(&s), header.size)); + } + + CHECK(avifStreamSkip(&s, header.size)); + } + + // Now calculate chunk sizes from the read-in sample table + uint32_t sampleSizeIndex = 0; + for (uint32_t chunkIndex = 0; chunkIndex < track->sampleTable->chunks.count; ++chunkIndex) { + avifSampleTableChunk * chunk = &track->sampleTable->chunks.chunk[chunkIndex]; + + // First, figure out how many samples are in this chunk + uint32_t sampleCount = 0; + for (int sampleToChunkIndex = track->sampleTable->sampleToChunks.count - 1; sampleToChunkIndex >= 0; --sampleToChunkIndex) { + avifSampleTableSampleToChunk * sampleToChunk = &track->sampleTable->sampleToChunks.sampleToChunk[sampleToChunkIndex]; + if (sampleToChunk->firstChunk <= (chunkIndex + 1)) { + sampleCount = sampleToChunk->samplesPerChunk; + break; + } + } + if (sampleCount == 0) { + // chunks with 0 samples are invalid + return AVIF_FALSE; + } + + // Then sum up the next sampleCount samples into this chunk + for (uint32_t sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) { + if (sampleSizeIndex >= track->sampleTable->sampleSizes.count) { + // We've run out of samples to sum + return AVIF_FALSE; + } + + avifSampleTableSampleSize * sampleSize = &track->sampleTable->sampleSizes.sampleSize[sampleSizeIndex]; + chunk->size += sampleSize->size; + ++sampleSizeIndex; + } + } + + return AVIF_TRUE; +} + +static avifBool avifParseMediaInformationBox(avifData * data, avifTrack * track, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + while (avifStreamHasBytesLeft(&s, 1)) { + avifBoxHeader header; + CHECK(avifStreamReadBoxHeader(&s, &header)); + + if (!memcmp(header.type, "stbl", 4)) { + CHECK(avifParseSampleTableBox(data, track, avifStreamCurrent(&s), header.size)); + } + + CHECK(avifStreamSkip(&s, header.size)); + } + return AVIF_TRUE; +} + +static avifBool avifParseMediaBox(avifData * data, avifTrack * track, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + while (avifStreamHasBytesLeft(&s, 1)) { + avifBoxHeader header; + CHECK(avifStreamReadBoxHeader(&s, &header)); + + if (!memcmp(header.type, "mdhd", 4)) { + CHECK(avifParseMediaHeaderBox(data, track, avifStreamCurrent(&s), header.size)); + } else if (!memcmp(header.type, "minf", 4)) { + CHECK(avifParseMediaInformationBox(data, track, avifStreamCurrent(&s), header.size)); + } + + CHECK(avifStreamSkip(&s, header.size)); + } + return AVIF_TRUE; +} + +static avifBool avifParseTrackBox(avifData * data, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + avifTrack * track = (avifTrack *)avifArrayPushPtr(&data->tracks); + + while (avifStreamHasBytesLeft(&s, 1)) { + avifBoxHeader header; + CHECK(avifStreamReadBoxHeader(&s, &header)); + + if (!memcmp(header.type, "tkhd", 4)) { + CHECK(avifParseTrackHeaderBox(data, track, avifStreamCurrent(&s), header.size)); + } else if (!memcmp(header.type, "mdia", 4)) { + CHECK(avifParseMediaBox(data, track, avifStreamCurrent(&s), header.size)); + } + + CHECK(avifStreamSkip(&s, header.size)); + } + return AVIF_TRUE; +} + +static avifBool avifParseMoovBox(avifData * data, uint8_t * raw, size_t rawLen) +{ + BEGIN_STREAM(s, raw, rawLen); + + while (avifStreamHasBytesLeft(&s, 1)) { + avifBoxHeader header; + CHECK(avifStreamReadBoxHeader(&s, &header)); + + if (!memcmp(header.type, "trak", 4)) { + CHECK(avifParseTrackBox(data, avifStreamCurrent(&s), header.size)); + } + + CHECK(avifStreamSkip(&s, header.size)); + } + return AVIF_TRUE; +} + static avifBool avifParseFileTypeBox(avifData * data, uint8_t * raw, size_t rawLen) { BEGIN_STREAM(s, raw, rawLen); @@ -527,6 +886,8 @@ CHECK(avifParseFileTypeBox(data, avifStreamCurrent(&s), header.size)); } else if (!memcmp(header.type, "meta", 4)) { CHECK(avifParseMetaBox(data, avifStreamCurrent(&s), header.size)); + } else if (!memcmp(header.type, "moov", 4)) { + CHECK(avifParseMoovBox(data, avifStreamCurrent(&s), header.size)); } CHECK(avifStreamSkip(&s, header.size));