oxideav-avi
Pure-Rust AVI (RIFF) container — demuxer + muxer for the legacy Microsoft AVI 1.0 format with a wide FourCC / WAVEFORMATEX mapping table into stable oxideav codec ids. Zero C dependencies.
Part of the oxideav framework but usable standalone.
Installation
[]
= "0.1"
= "0.1"
= "0.0"
Feature matrix
| Feature | Demux | Mux |
|---|---|---|
| RIFF/AVI 1.0 (hdrl/strl/movi/idx1) | yes | yes |
| Multi-stream (video + audio + ...) | yes | yes |
LIST INFO metadata (title/artist/album/...) |
yes (known FourCCs + avi:info.<fourcc> for unknowns; nested-in-hdrl + sibling-of-hdrl placements; info_for / info_all_for byte-keyed + all_info_for string-keyed accessors) |
yes (AviMuxOptions::with_info + with_top_level_info for sibling layout) |
Duration from avih (microseconds-per-frame) |
yes | n/a |
avih.dwFlags (AVIF_* bits) |
yes (typed avih_flags() decode + raw avi:flags metadata) |
yes (AviMuxOptions::with_avih_flags / with_avih_flag_bit) |
idx1 legacy index — parse for keyframe seek |
yes | yes |
idx1 offsets — file-absolute + movi-relative |
yes | yes |
idx1 entry dwFlags (AVIIF_* bits) |
yes (typed idx1_typed_flags_for_packet decode + raw idx1_flags_for_packet + public AVIIF_LIST / AVIIF_KEYFRAME / AVIIF_FIRSTPART / AVIIF_LASTPART / AVIIF_NO_TIME / AVIIF_COMPRESSOR constants) |
yes (AVIIF_KEYFRAME on every keyframe; `AVIIF_FIRSTPART |
idx1 ↔ ix## cross-validator |
yes (lenient: multi-segment OpenDML files surface avi:idx1.<n>.divergent_offsets; strict: open_avi_strict returns Error::InvalidData) |
n/a |
LIST rec packet grouping inside movi |
yes | yes (packet-cap or byte-budget) |
OpenDML 2.0 multi-RIFF AVIX continuation |
yes | yes |
OpenDML 2.0 indx super-index in strl |
yes (parse) | yes (emit) |
OpenDML 2.0 ix## per-segment std-index in movi |
yes (parse) | yes (segment-tail emit + opt-in mid-movi periodic flush via AviMuxOptions::with_mid_movi_index) |
idx1-from-ix## synthesis (AVI 1.0 reader compat) |
n/a | yes (AviMuxOptions::synthesise_idx1_from_ix(true) rebuilds primary segment's idx1 from per-packet ix## records) |
OpenDML 2.0 LIST odml dmlh extended header |
yes (parse) | yes (emit) |
OpenDML 2.0 vprp Video Properties Header |
yes (parse + avi:vprp.<n>.* metadata + typed vprp_field_descs per-field rects + typed vprp_frame_aspect_ratio returning the §5.0 dwFrameAspectRatio unpacked as a numeric (x, y) pair) |
yes (NTSC/PAL/SECAM presets + custom aspect) |
OpenDML 2.0 AVI_INDEX_2FIELD interlaced std-index |
yes (parse + metadata surface + per-packet field2_offset_for_packet accessor) |
yes (open_avi + set_field2_offset) |
| OpenDML 2.0 super-index overflow signalling | yes (avi:indx.<n>.overflow_entries) |
yes (AviMuxer::truncated_super_index_segments()) |
VBR audio framing via Packet.duration |
n/a | yes (drives strh.dwLength for non-PCM) |
OpenDML-driven seeking (ix## std-index) |
yes (no-idx1 fallback + explicit seek_to_keyframe_strict_via_std_index returning KeyframeSeekResult) |
n/a |
Idx1Flags-aware non-AVIIF_NO_TIME keyframe seek |
yes (seek_to_first_video_keyframe_after skips palette/text/data side-band entries that carry AVIIF_NO_TIME) |
n/a |
Per-stream dwMaxBytesPerSec cap |
n/a | yes (AviMuxOptions::with_per_stream_max_bytes_per_sec + AviMuxer::over_budget_streams + with_strict_per_stream_budget for hard write_trailer error) |
Uncompressed db video chunks |
yes | yes |
| Variable stream interleave | yes | yes |
Palette-change (xxpc) chunks |
skip + per-stream palette_change_count(stream) + palette_change_data(stream) body accessors + avi:palette_change.<n> metadata |
yes (AviMuxer::write_palette_change) |
Text/subtitle (xxtx) chunks |
skip + per-stream text_chunk_count(stream) + text_chunk_data(stream) body accessors + avi:text_chunk.<n> metadata |
yes (AviMuxer::write_text_chunk) |
| Truncated-head tolerance (capture-card crash dumps) | yes | n/a |
Top-down DIB orientation (negative biHeight) |
yes (stream_top_down accessor + avi:vids.<n>.top_down metadata; preserved across parse) |
yes (AviMuxOptions::with_top_down_video; honoured only for BI_RGB per VfW §"biHeight sign rules") |
BI_BITFIELDS color masks (16/32-bpp RGB) |
yes (stream_bitfields_masks accessor + avi:vids.<n>.bitfields = "r=...,g=...,b=..." metadata) |
n/a |
WAVE_FORMAT_EXTENSIBLE (0xFFFE) — 22-byte cbSize extension |
yes (stream_audio_strf / stream_channel_mask / stream_valid_bits_per_sample / stream_subformat accessors + avi:auds.<n>.{channel_mask,valid_bits_per_sample,subformat,subformat_wformat_tag} metadata; depth-aware codec-id resolution for the 7 documented KSDATAFORMAT_SUBTYPE_* GUIDs, incl. pcm_s24le for 24-in-32 container PCM) |
yes (AviMuxOptions::with_extensible_audio(stream, channel_mask, valid_bps, subformat_guid)) |
Per-stream strn name chunk (AVI 1.0 §"AVI Stream Headers") |
yes (stream_name(stream_index) accessor + avi:strn.<n> metadata; UTF-8-lossy decode; multi-trailing-NUL tolerated; empty-payload strn parses as None so absence stays distinguishable) |
yes (AviMuxOptions::with_stream_name(stream_index, name) — last builder call per index wins; NUL terminator added) |
IDIT digitization-date chunk (RIFF Hdrl Tags DateTimeOriginal) |
yes (digitization_date() accessor + avi:idit metadata; direct child of LIST hdrl; trailing NUL/whitespace stripped, UTF-8-lossy; format-agnostic — asctime and ISO-8601 both round-trip verbatim; empty/all-whitespace body parses as None so absence stays observable) |
yes (AviMuxOptions::with_digitization_date(date) — last builder call wins; NUL terminator added) |
ISMP SMPTE-timecode chunk (RIFF Hdrl Tags TimeCode) |
yes (smpte_timecode() accessor + avi:ismp metadata; direct child of LIST hdrl, sibling of IDIT; trailing NUL/whitespace stripped, UTF-8-lossy; format-agnostic — non-drop-frame "HH:MM:SS:FF", drop-frame "HH:MM:SS;FF" and fractional forms all round-trip verbatim; empty/all-whitespace body parses as None so absence stays observable) |
yes (AviMuxOptions::with_smpte_timecode(tc) — last builder call wins; NUL terminator added) |
Per-stream strd codec-driver data chunk (AVI 1.0 §"AVI Stream Headers") |
yes (stream_header_data(stream_index) accessor returning raw bytes verbatim + avi:strd.<n>.len metadata; empty-payload strd (cb=0) parses as Some(&[]) so "no chunk" stays distinguishable from "empty driver blob") |
yes (AviMuxOptions::with_stream_header_data(stream_index, bytes) — last builder call per index wins; RIFF word-pad applied to odd lengths) |
Per-stream strh.rcFrame destination rectangle (AVI 1.0 §"AVISTREAMHEADER") |
yes (stream_frame_rect(stream_index) -> Option<(i16,i16,i16,i16)> accessor returning (left, top, right, bottom) from byte offset 48 of the 56-byte AVISTREAMHEADER + avi:strh.<n>.frame_rect = "l,t,r,b" metadata; the all-zero "whole movie rectangle" default and the short 48-byte strh both parse as None so a default/unspecified rect stays observable) |
yes (AviMuxOptions::with_stream_frame_rect(stream_index, l, t, r, b) — last builder call per index wins; overrides the default 0,0,width,height for video / all-zero for non-video, for any stream type) |
avih.dwPaddingGranularity + JUNK-aligned packet emission (AVI 1.0 §"AVIMAINHEADER" / §"Other Data Chunks") |
yes (padding_granularity() accessor + avi:padding_granularity metadata key — both omit the legacy 0 sentinel so absence is observable) |
yes (AviMuxOptions::with_padding_granularity(n); powers of two in [2, 65536]; inserts one JUNK chunk per misaligned packet so every packet header lands at a file-absolute offset divisible by n) |
CBR-audio ix## block-alignment validator (OpenDML 2.0 §3.0 "AVI Standard Index Chunk") |
yes (cbr_audio_block_alignment_violations() returns one BlockAlignViolation { stream_index, entry_index, dw_size, block_align } per std-index entry whose dwSize is not a multiple of the stream's WAVEFORMATEX.nBlockAlign; scoped to PCM / A-law / µ-law / IMA-ADPCM streams with nBlockAlign > 1; informational, never fails open()) |
n/a |
OpenDML 2.0 super-index dwDuration round-trip + cross-check (OpenDML 2.0 §"AVI Super Index Chunk" / §5.0 "Extended AVI Header") |
yes (super_index_segment_durations(stream) lists the per-segment _avisuperindex_entry.dwDuration values; super_index_duration_violations() returns one SuperIndexDurationViolation { stream_index, super_index_duration_total, dmlh_total_frames } per video stream whose duration sum disagrees with dmlh.dwTotalFrames; informational, never fails open(); parse_indx keeps the legitimate primary-segment qwOffset == 0 entry) |
yes (dwDuration carries the indexed stream's per-segment frame count, not the all-stream packet total) |
OpenDML 2.0 muxing is opt-in via muxer::open_with_kind with an
AviKind::OpenDml(RiffSegmentLimit::OneGiB) (or Bytes(n) for
testing). The muxer rolls a new RIFF AVIX segment whenever the
running segment would exceed the byte ceiling; the primary segment
carries an indx super-index in the first stream's strl with one
entry per segment back-patched in write_trailer, and each segment
emits a per-stream ix## AVISTDINDEX chunk at the tail of its
movi LIST. Demuxer-side, seek_to falls back to the ix##
standard indexes when no AVI 1.0 idx1 is present (typical for
OpenDML-only files written by recent ffmpeg / VirtualDub2 with
--max_riff_size set). The legacy muxer::open (which Avi10
defaults to) refuses to grow past ~2 GiB and returns
Error::Unsupported.
Codec mapping
AVI identifies video streams by a 4-byte biCompression FourCC in the
strf/BITMAPINFOHEADER, and audio streams by a 16-bit wFormatTag in
the strf/WAVEFORMATEX. oxideav-avi translates these both ways into
the stable codec ids used everywhere else in oxideav.
FourCC matching is case-insensitive. Unknown tags round-trip as
avi:<fourcc> (video) or avi:tag_xxxx (audio) rather than failing —
so a caller can still walk an unrecognised stream, it just won't find a
decoder.
Video FourCC to codec id
| Codec id | FourCCs accepted (case-insensitive) |
|---|---|
mjpeg |
MJPG, AVRN, LJPG, JPGL |
ffv1 |
FFV1 |
h263 |
H263, U263, M263, ILVR, VX1K, VIV1, X263, T263, S263, L263 |
h264 |
H264, AVC1, X264, VSSH, DAVC, PAVC, AVC2, AVC3, AVC-Intra AI13/AI15/... |
h265 |
HEVC, H265, HVC1, HEV1, X265, DXHE |
mpeg1video |
MPG1, MPEG |
mpeg2video |
MPG2, MP2V, EM2V, HDV flavours HDV1..HDV9 |
mpeg4video |
XVID, DIVX, DX50, DX40, MP4V, FMP4, DIV3..DIV6, 3IV2, M4S2, MP4S, BLZ0, DXGM, RMP4, SMP4, UMP4, XVIX, WV1F, DIVF, MP43 |
vp6 |
VP60, VP61, VP62, VP6F, VP6A |
vp8 / vp9 |
VP80/VP8 , VP90/VP9 |
av1 |
AV01 |
theora |
THEO |
huffyuv / ffvhuff |
HFYU, FFVH |
utvideo |
UTVS, ULRG, ULRA, ULY0/ULY2/ULY4, ULH0/ULH2/ULH4 |
magicyuv |
8-bit M8RG/M8RA/M8Y4/M8Y2/M8Y0/M8YA/M8G0, 10-bit M0RG/M0RA/M0Y4/M0Y2/M0Y0/M0G0, 12-bit M2RG/M2RA, 14-bit M4RG/M4RA |
prores |
APCH, APCN, APCS, APCO, AP4H, AP4X |
dv |
DVSD, DV25, DV50, DVC , DVCP, DVHD, DVH1 |
wmv1/wmv2/wmv3 |
WMV1, WMV2, WMV3 |
vc1 |
WVC1, WMVA |
cinepak |
CVID |
indeo3/indeo4/indeo5 |
IV31/IV32, IV41/IV42, IV50 |
svq1 / svq3 |
SVQ1, SVQ3 |
flv1 |
FLV1 |
rgb24 |
BI_RGB (all-zero FourCC), DIB , RGB , RAW |
yuyv422 / uyvy422 |
YUY2/YUYV/V422/YUNV, UYVY/Y422/UYNV |
nv12 / nv21 |
NV12, NV21 |
yuv420p |
I420, IYUV, YV12 |
yuv411p |
Y41P |
yuv444p10le / v210 / r210 |
V410, V210, R210/R10K |
Audio wFormatTag to codec id
| Codec id | WAVEFORMATEX tag(s) |
|---|---|
pcm_u8 |
0x0001 with wBitsPerSample = 8 |
pcm_s16le |
0x0001 with wBitsPerSample = 16 |
pcm_s24le |
0x0001 with wBitsPerSample = 24 |
pcm_s32le |
0x0001 with wBitsPerSample = 32 |
pcm_f32le / pcm_f64le |
0x0003 with 32 / 64 bits per sample |
pcm_alaw |
0x0006 |
pcm_mulaw |
0x0007 |
adpcm_ms |
0x0002 |
adpcm_ima_wav |
0x0011 |
adpcm_yamaha |
0x0020 |
adpcm_ima_dk4/adpcm_ima_dk3 |
0x0061, 0x0062 |
g722 |
0x0028 |
g723_1 |
0x0014 |
gsm_ms |
0x0031, 0x0032 |
mp2 |
0x0050 |
mp3 |
0x0055 |
aac |
0x00FF, 0x706D, 0x4143, 0xA106 |
wmav1/wmav2/wmapro/wmalossless |
0x0160, 0x0161, 0x0162, 0x0163 |
ac3 |
0x2000, 0x6AC3 |
eac3 |
0x2001, 0xEAC3 |
dts |
0x2002, 0x0008 |
opus |
0x4F70, 0x704F, 0x7075 |
vorbis |
0x674F/0x6750/0x6751/0x676F/0x6770/0x6771 |
flac |
0xF1AC |
Muxer-side packaging (strf + strh) is implemented for a subset of
the above: mjpeg, ffv1, mpeg1video, mpeg2video, mpeg4video,
h263, h264, h265, vp8, vp9, av1, magicyuv, rgb24, all
six raw PCM variants (pcm_u8/s16le/s24le/s32le/f32le/f64le),
pcm_alaw, pcm_mulaw, mp2, mp3, aac, ac3, eac3, flac.
Other codec ids return Error::Unsupported at open().
For codec ids that share several FourCCs (e.g. mpeg4video →
XVID/DIVX/...; magicyuv → 17 native FourCCs), the muxer picks a
default FourCC unless the caller hints otherwise. For magicyuv, the
hint is the leading 4 bytes of CodecParameters::extradata if they
spell one of the 17 native FourCCs; otherwise M8RG is used.
Quick use
Demux
use ;
let mut containers = new;
register;
let input: =
Boxnew;
let mut dmx = containers.open_demuxer?;
for s in dmx.streams
loop
# Ok::
seek_to(stream, pts) lands on the nearest keyframe at or before the
target, using the idx1 index. Files without idx1 (streamed AVI,
half-written recordings) return Error::Unsupported for seek; linear
decode still works.
Mux
use ;
use ;
let mut containers = new;
register;
// One video + one audio stream.
let mut v = video;
v.width = Some;
v.height = Some;
v.frame_rate = Some;
let mut a = audio;
a.channels = Some;
a.sample_rate = Some;
a.sample_format = Some;
let streams = ;
let out: =
Boxnew;
let mut mux = containers.open_muxer?;
mux.write_header?;
// mux.write_packet(&pkt)? for each encoded packet, interleaved
mux.write_trailer?;
# Ok::
The muxer always emits a legacy idx1 index at the end of the file so
the resulting AVI is seekable. The 2 GiB ceiling is enforced strictly —
if write_packet would push the file past ~2 GiB, it returns
Error::Unsupported so the caller can roll a new segment.
Container id
- Container:
"avi", matches.aviby extension and theRIFF....AVImagic. - Registered via
oxideav_avi::register(&mut ContainerRegistry).
License
MIT — see LICENSE.