1
1
mirror of https://github.com/wader/fq.git synced 2024-11-25 05:55:43 +03:00
fq/format/riff/avi.go
2024-04-09 20:09:12 +02:00

658 lines
17 KiB
Go

package riff
// TODO:
// mp3 mappig, samples can span over sample ranges?
// hevc mapping?
// DV handler https://learn.microsoft.com/en-us/windows/win32/directshow/dv-data-in-the-avi-file-format
// palette change
// rec groups
// nested indexes
// unknown fields for unreachable chunk header for > 1gb samples
// 2fields, field index?
// https://learn.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
// http://www.jmcgowan.com/odmlff2.pdf
// https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/avidec.c
// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/um/aviriff.h
import (
"embed"
"strconv"
"github.com/wader/fq/format"
"github.com/wader/fq/pkg/decode"
"github.com/wader/fq/pkg/interp"
"github.com/wader/fq/pkg/ranges"
"github.com/wader/fq/pkg/scalar"
)
//go:embed avi.md
var aviFS embed.FS
var aviMp3FrameGroup decode.Group
var aviMpegAVCAUGroup decode.Group
var aviMpegHEVCAUGroup decode.Group
var aviFLACFrameGroup decode.Group
func init() {
interp.RegisterFormat(
format.AVI,
&decode.Format{
Description: "Audio Video Interleaved",
DecodeFn: aviDecode,
DefaultInArg: format.AVI_In{
DecodeSamples: true,
DecodeExtendedChunks: true,
},
Dependencies: []decode.Dependency{
{Groups: []*decode.Group{format.AVC_AU}, Out: &aviMpegAVCAUGroup},
{Groups: []*decode.Group{format.HEVC_AU}, Out: &aviMpegHEVCAUGroup},
{Groups: []*decode.Group{format.MP3_Frame}, Out: &aviMp3FrameGroup},
{Groups: []*decode.Group{format.FLAC_Frame}, Out: &aviFLACFrameGroup},
},
Groups: []*decode.Group{format.Probe},
})
interp.RegisterFS(aviFS)
}
var aviListTypeDescriptions = scalar.StrMapDescription{
"hdrl": "AVI main list",
"strl": "Stream list",
"movi": "Stream Data",
"rec": "Chunk group",
}
const (
aviStrhTypeAudio = "auds"
aviStrhTypeMidi = "mids"
aviStrhTypeVideo = "vids"
aviStrhTypeText = "txts"
)
var aviStrhTypeDescriptions = scalar.StrMapDescription{
aviStrhTypeAudio: "Audio stream",
aviStrhTypeMidi: "MIDI stream",
aviStrhTypeText: "Text stream",
aviStrhTypeVideo: "Video stream",
}
const (
aviIndexTypeIndexes = 0
aviIndexTypeChunks = 1
)
var aviIndexTypeNames = scalar.UintMapSymStr{
aviIndexTypeIndexes: "indexes",
aviIndexTypeChunks: "chunks",
}
const (
aviIndexSubType2Fields = 1
)
var aviIndexSubTypeNames = scalar.UintMapSymStr{
aviIndexSubType2Fields: "2fields",
}
const (
aviStreamChunkTypeUncompressedVideo = "db"
aviStreamChunkTypeCompressedVideo = "dc"
aviStreamChunkTypePaletteChange = "pc"
aviStreamChunkTypeAudio = "wb"
aviStreamChunkTypeIndex = "ix"
)
var aviStreamChunkTypeDescriptions = scalar.StrMapDescription{
aviStreamChunkTypeUncompressedVideo: "Uncompressed video frame",
aviStreamChunkTypeCompressedVideo: "Compressed video frame",
aviStreamChunkTypePaletteChange: "Palette change",
aviStreamChunkTypeAudio: "Audio data",
aviStreamChunkTypeIndex: "Index",
}
type idx1Sample struct {
offset int64
size int64
streamNr int
streamType string
}
type aviStream struct {
typ string
handler string
formatTag uint64
compression string
hasFormat bool
format *decode.Group
formatInArg any
sampleSize uint64
indexes []ranges.Range
ixSamples []ranges.Range
}
func aviParseChunkID(id string) (string, int, bool) {
if len(id) != 4 {
return "", 0, false
}
isDigits := func(s string) bool {
for _, c := range s {
if !(c >= '0' && c <= '9') {
return false
}
}
return true
}
var typ string
var indexStr string
switch {
case isDigits(id[0:2]):
// ##dc media etc
indexStr, typ = id[0:2], id[2:4]
case isDigits(id[2:4]):
// ix## index etc
typ, indexStr = id[0:2], id[2:4]
default:
return "", 0, false
}
index, err := strconv.Atoi(indexStr)
if err != nil {
panic("unreachable")
}
return typ, index, true
}
func aviIsStreamType(typ string) bool {
switch typ {
case aviStreamChunkTypeUncompressedVideo,
aviStreamChunkTypeCompressedVideo,
aviStreamChunkTypeAudio:
return true
default:
return false
}
}
func aviDecorateStreamID(d *decode.D, id string) (string, int) {
typ, index, ok := aviParseChunkID(id)
if ok && aviIsStreamType(typ) {
d.FieldValueStr("stream_type", typ, aviStreamChunkTypeDescriptions)
d.FieldValueUint("stream_nr", uint64(index))
return typ, index
}
return "", 0
}
// ix frame index and indx frame index
func aviDecodeChunkIndex(d *decode.D) []ranges.Range {
var rs []ranges.Range
d.FieldU16("longs_per_entry") // TODO: use?
d.FieldU8("index_subtype", aviIndexSubTypeNames)
d.FieldU8("index_type", aviIndexTypeNames)
nEntriesInUse := d.FieldU32("entries_in_use")
chunkID := d.FieldUTF8("chunk_id", 4)
aviDecorateStreamID(d, chunkID)
baseOffset := int64(d.FieldU64("base_offset"))
d.FieldU32("unused")
d.FieldArray("index", func(d *decode.D) {
for i := 0; i < int(nEntriesInUse); i++ {
d.FieldStruct("index", func(d *decode.D) {
offset := int64(d.FieldU32("offset"))
sizeKeyFrame := d.FieldU32("size_keyframe")
size := sizeKeyFrame & 0x7f_ff_ff_ff
d.FieldValueUint("size", size)
d.FieldValueBool("key_frame", sizeKeyFrame&0x80_00_00_00 == 0)
rs = append(rs, ranges.Range{
Start: baseOffset*8 + offset*8,
Len: int64(size) * 8,
})
})
}
})
return rs
}
func aviDecodeEx(d *decode.D, ai format.AVI_In, extendedChunk bool) {
var streams []*aviStream
var idx1Samples []idx1Sample
var moviListPos int64 // point to first bit after type
requiredRiffType := "AVI "
if extendedChunk {
requiredRiffType = "AVIX"
}
var foundRiffType string
riffDecode(
d,
nil,
func(d *decode.D, path path) (string, int64) {
id := d.FieldUTF8("id", 4, scalar.ActualTrimSpace, chunkIDDescriptions)
aviDecorateStreamID(d, id)
size := d.FieldU32("size")
return id, int64(size)
},
func(d *decode.D, id string, path path) (bool, any) {
switch id {
case "RIFF":
foundRiffType = d.FieldUTF8("type", 4, d.StrAssert(requiredRiffType))
return true, nil
case "LIST":
typ := d.FieldUTF8("type", 4, scalar.ActualTrimSpace, aviListTypeDescriptions)
switch typ {
case "strl":
return true, &aviStream{}
case "movi":
moviListPos = d.Pos()
}
return true, nil
case "idx1":
d.FieldArray("indexes", func(d *decode.D) {
// TODO: seems there are files with weird tailing extra index entries
// TODO: limit using total_frame somehow instead?
for d.BitsLeft() >= 4*32 {
d.FieldStruct("index", func(d *decode.D) {
id := d.FieldUTF8("id", 4)
typ, index := aviDecorateStreamID(d, id)
d.FieldStruct("flags", func(d *decode.D) {
d.FieldRawLen("unused0", 3)
d.FieldBool("key_frame")
d.FieldRawLen("unused1", 3)
d.FieldBool("list")
d.FieldRawLen("unused2", 24)
})
offset := int64(d.FieldU32("offset"))
length := int64(d.FieldU32("length"))
idx1Samples = append(idx1Samples, idx1Sample{
offset: offset * 8,
size: length * 8,
streamNr: index,
streamType: typ,
})
})
}
})
return false, nil
case "avih":
d.FieldU32("micro_sec_per_frame")
d.FieldU32("max_bytes_per_sec")
d.FieldU32("padding_granularity")
d.FieldStruct("flags", func(d *decode.D) {
d.FieldRawLen("unused0", 2)
d.FieldBool("must_use_index")
d.FieldBool("has_index") // Index at end of file?
d.FieldRawLen("unused1", 8)
d.FieldBool("trust_ck_type") // Use CKType to find key frames
d.FieldRawLen("unused2", 2)
d.FieldBool("is_interleaved")
d.FieldRawLen("unused3", 6)
d.FieldBool("copyrighted")
d.FieldBool("was_capture_file")
d.FieldRawLen("unused4", 8)
})
d.FieldU32("total_frames")
d.FieldU32("initial_frames")
d.FieldU32("streams")
d.FieldU32("suggested_buffer_size")
d.FieldU32("width")
d.FieldU32("height")
d.FieldRawLen("reserved", 32*4)
return false, nil
case "dmlh":
d.FieldU32("total_frames")
d.FieldRawLen("future", 32*61)
return false, nil
case "strh":
typ := d.FieldUTF8("type", 4, aviStrhTypeDescriptions)
handler := d.FieldUTF8("handler", 4)
d.FieldStruct("flags", func(d *decode.D) {
d.FieldRawLen("unused0", 7)
d.FieldBool("disabled")
d.FieldRawLen("unused1", 15)
d.FieldBool("pal_changes")
d.FieldRawLen("unused2", 8)
})
d.FieldU16("priority")
d.FieldU16("language")
d.FieldU32("initial_frames")
d.FieldU32("scale")
d.FieldU32("rate")
d.FieldU32("start")
d.FieldU32("length")
d.FieldU32("suggested_buffer_size")
d.FieldU32("quality")
sampleSize := d.FieldU32("sample_size")
d.FieldStruct("frame", func(d *decode.D) {
d.FieldU16("left")
d.FieldU16("top")
d.FieldU16("right")
d.FieldU16("bottom")
})
if stream, ok := path.topData().(*aviStream); ok {
stream.typ = typ
stream.handler = handler
stream.sampleSize = sampleSize
}
return false, nil
case "strf":
stream, streamOk := path.topData().(*aviStream)
if !streamOk {
stream = &aviStream{}
}
typ := stream.typ
switch typ {
case aviStrhTypeVideo:
// BITMAPINFOHEADER
d.BitsLeft()
d.FieldU32("bi_size")
d.FieldU32("width")
d.FieldU32("height")
d.FieldU16("planes")
d.FieldU16("bit_count")
compression := d.FieldUTF8("compression", 4)
d.FieldU32("size_image")
d.FieldU32("x_pels_per_meter")
d.FieldU32("y_pels_per_meter")
d.FieldU32("clr_used")
d.FieldU32("clr_important")
if d.BitsLeft() > 0 {
d.FieldRawLen("extra", d.BitsLeft())
}
stream.compression = compression
// TODO: if dvsd handler and extraSize >= 32 then DVINFO?
switch compression {
case format.BMPTagH264,
format.BMPTagH264_h264,
format.BMPTagH264_X264,
format.BMPTagH264_x264,
format.BMPTagH264_avc1,
format.BMPTagH264_DAVC,
format.BMPTagH264_SMV2,
format.BMPTagH264_VSSH,
format.BMPTagH264_Q264,
format.BMPTagH264_V264,
format.BMPTagH264_GAVC,
format.BMPTagH264_UMSV,
format.BMPTagH264_tshd,
format.BMPTagH264_INMC:
stream.format = &aviMpegAVCAUGroup
stream.hasFormat = true
case format.BMPTagHEVC,
format.BMPTagHEVC_H265:
stream.format = &aviMpegHEVCAUGroup
stream.hasFormat = true
}
case aviStrhTypeAudio:
// WAVEFORMATEX
formatTag := d.FieldU16("format_tag", format.WAVTagNames)
d.FieldU16("channels")
d.FieldU32("samples_per_sec")
d.FieldU32("avg_bytes_per_sec")
d.FieldU16("block_align")
d.FieldU16("bits_per_sample")
// TODO: seems to be optional
if d.BitsLeft() >= 16 {
cbSize := d.FieldU16("cb_size")
d.FieldRawLen("extra", int64(cbSize)*8)
}
stream.formatTag = formatTag
switch formatTag {
case format.WAVTagMP3:
stream.format = &aviMp3FrameGroup
stream.hasFormat = true
case format.WAVTagFLAC:
// TODO: can flac in avi have streaminfo somehow?
stream.format = &aviFLACFrameGroup
stream.hasFormat = true
}
case "iavs":
// DVINFO
d.FieldU32("dva_aux_src")
d.FieldU32("dva_aux_ctl")
d.FieldU32("dva_aux_src1")
d.FieldU32("dva_aux_ctl1")
d.FieldU32("dvv_aux_src")
d.FieldU32("dvv_aux_ctl")
d.FieldRawLen("dvv_reserved", 32*2)
}
streams = append(streams, stream)
return false, nil
case "indx":
stream, _ := path.topData().(*aviStream)
d.FieldU16("longs_per_entry") // TODO: use?
d.FieldU8("index_subtype")
d.FieldU8("index_type")
nEntriesInUse := d.FieldU32("entries_in_use")
chunkID := d.FieldUTF8("chunk_id", 4)
aviDecorateStreamID(d, chunkID)
d.FieldU64("base")
d.FieldU32("unused0")
d.FieldArray("index", func(d *decode.D) {
for i := 0; i < int(nEntriesInUse); i++ {
d.FieldStruct("index", func(d *decode.D) {
offset := int64(d.FieldU64("offset"))
size := int64(d.FieldU32("size"))
d.FieldU32("duration")
if stream != nil {
stream.indexes = append(stream.indexes, ranges.Range{
Start: offset * 8,
Len: size * 8,
})
}
})
}
})
if d.BitsLeft() > 0 {
d.FieldRawLen("unused1", d.BitsLeft())
}
return false, nil
case "vprp":
d.FieldU32("video_format_token")
d.FieldU32("video_standard")
d.FieldU32("vertical_refresh_rate")
d.FieldU32("h_total_in_t")
d.FieldU32("v_total_in_lines")
d.FieldStruct("frame_aspect_ratio", func(d *decode.D) {
d.FieldU16("x")
d.FieldU16("y")
})
d.FieldU32("frame_width_in_pixels")
d.FieldU32("frame_height_in_lines")
nbFieldPerFrame := d.FieldU32("nb_field_per_frame")
d.FieldArray("field_info", func(d *decode.D) {
for i := 0; i < int(nbFieldPerFrame); i++ {
d.FieldStruct("field_info", func(d *decode.D) {
d.FieldU32("compressed_bm_height")
d.FieldU32("compressed_bm_width")
d.FieldU32("valid_bm_height")
d.FieldU32("valid_bm_width")
d.FieldU32("valid_bmx_offset")
d.FieldU32("valid_bmy_offset")
d.FieldU32("video_x_offset_in_t")
d.FieldU32("video_y_valid_start_line")
})
}
})
return false, nil
default:
if riffIsStringChunkID(id) {
d.FieldUTF8NullFixedLen("value", int(d.BitsLeft())/8)
return false, nil
}
typ, index, _ := aviParseChunkID(id)
switch {
case typ == "ix":
sampleRanges := aviDecodeChunkIndex(d)
if index < len(streams) {
s := streams[index]
s.ixSamples = append(s.ixSamples, sampleRanges...)
}
case d.BitsLeft() > 0 &&
ai.DecodeSamples &&
aviIsStreamType(typ) &&
index < len(streams) &&
streams[index].hasFormat:
s := streams[index]
d.FieldFormatLen("data", d.BitsLeft(), s.format, s.formatInArg)
default:
d.FieldRawLen("data", d.BitsLeft())
}
return false, nil
}
},
)
if foundRiffType != requiredRiffType {
d.Errorf("wrong or no AVI riff type found (%s)", requiredRiffType)
}
if !extendedChunk {
d.FieldArray("streams", func(d *decode.D) {
for streamIndex, stream := range streams {
d.FieldStruct("stream", func(d *decode.D) {
d.FieldValueStr("type", stream.typ)
d.FieldValueStr("handler", stream.handler)
switch stream.typ {
case aviStrhTypeAudio:
d.FieldValueUint("format_tag", stream.formatTag, format.WAVTagNames)
case aviStrhTypeVideo:
d.FieldValueStr("compression", stream.compression)
}
var streamIndexSampleRanges []ranges.Range
if len(stream.indexes) > 0 {
d.FieldArray("indexes", func(d *decode.D) {
for _, i := range stream.indexes {
d.FieldStruct("index", func(d *decode.D) {
d.RangeFn(i.Start, i.Len, func(d *decode.D) {
d.FieldUTF8("type", 4)
d.FieldU32("cb")
sampleRanges := aviDecodeChunkIndex(d)
streamIndexSampleRanges = append(streamIndexSampleRanges, sampleRanges...)
})
})
}
})
}
// TODO: palette change
decodeSample := func(d *decode.D, sr ranges.Range) {
d.RangeFn(sr.Start, sr.Len, func(d *decode.D) {
if sr.Len == 0 {
d.FieldRawLen("sample", d.BitsLeft())
return
}
subSampleSize := int64(stream.sampleSize) * 8
// TODO: <= no format and <= 8*8 heuristics to not create separate pcm samples
if subSampleSize == 0 || (!stream.hasFormat && subSampleSize <= 8*8) {
subSampleSize = sr.Len
}
for d.BitsLeft() > 0 {
d.FramedFn(subSampleSize, func(d *decode.D) {
if ai.DecodeSamples && stream.hasFormat {
d.FieldFormat("sample", stream.format, stream.formatInArg)
} else {
d.FieldRawLen("sample", d.BitsLeft())
}
})
}
})
}
// try only add indexed samples once with priority:
// stream index
// ix chunks (might be same as stream index)
// idx1 chunks
if len(streamIndexSampleRanges) > 0 {
d.FieldArray("samples", func(d *decode.D) {
for _, sr := range streamIndexSampleRanges {
decodeSample(d, sr)
}
})
} else if len(stream.ixSamples) > 0 {
d.FieldArray("samples", func(d *decode.D) {
for _, sr := range stream.ixSamples {
decodeSample(d, sr)
}
})
} else if len(idx1Samples) > 0 {
d.FieldArray("samples", func(d *decode.D) {
for _, is := range idx1Samples {
if is.streamNr != streamIndex {
continue
}
decodeSample(d, ranges.Range{
Start: moviListPos + is.offset + 32, // +32 skip size field
Len: is.size,
})
}
})
}
})
}
})
}
}
func aviDecode(d *decode.D) any {
var ai format.AVI_In
d.ArgAs(&ai)
d.Endian = decode.LittleEndian
aviDecodeEx(d, ai, false)
if ai.DecodeExtendedChunks {
d.FieldArray("extended_chunks", func(d *decode.D) {
for {
// TODO: other way? spec says check hdrx chunk but there seems to be none?
riff, _ := d.TryPeekBytes(4)
if string(riff) != "RIFF" {
break
}
d.FieldStruct("chunk", func(d *decode.D) {
aviDecodeEx(d, ai, true)
})
}
})
}
return nil
}