A spec-compliant AV1 encoder written from scratch in safe Rust with zero runtime dependencies.
wav1c can be used as:
- A Rust library (
wav1ccrate) - A CLI encoder (
wav1c-cli) - A C ABI library (
wav1c-ffi) - A WebAssembly module (
wav1c-wasm) - An FFmpeg external encoder (
libwav1c)
- AV1 4:2:0 encoding for SDR and HDR workflows
- Bit depths: 8-bit and 10-bit
- HDR signaling:
- Sequence-header color signaling (range + color description)
- HDR metadata OBUs:
- CLL (
max_cll,max_fall) - MDCV (mastering display metadata)
- CLL (
- AVIF compatibility signaling:
- AVIF item properties:
clli(+ optionalmdcv) - Single-frame AVIF sequence headers set
still_picture=1
- AVIF item properties:
- Y4M parsing:
C420*8-bit andC420p10XCOLORRANGE=FULL|LIMITEDin stream andFRAMEheaders- Typed parse errors for malformed/truncated input
- Intra + inter coding pipeline with RD decisions, transforms, and entropy coding
- B-frame pipeline support
- Large-dimension support in core encoder via AV1 multi-tile payload assembly (memory permitting)
Current scope limits:
- Chroma format: 4:2:0 only
- Bit depth: 8/10-bit only (no 12-bit)
- Container limits in CLI:
- IVF/MP4:
width <= 65535,height <= 65535 - AVIF: large dimensions supported
- IVF/MP4:
wav1c/ Core library (Rust API, bitstream generation)
wav1c-cli/ Command-line encoder
wav1c-ffi/ C FFI shared/static library
wav1c-wasm/ WebAssembly bindings (wasm-bindgen)
docs/ Project documentation (including HDR + HEIC guide)
cargo build --workspace --releaseAll examples below use the workspace binary:
cargo run -q -p wav1c-cli -- <args...>Basic SDR encode from Y4M:
cargo run -q -p wav1c-cli -- input.y4m -o output.ivfSolid-color frame encode (5 positional args: W H Y U V):
cargo run -q -p wav1c-cli -- 320 240 128 128 128 -o gray.ivf10-bit HDR10 encode from Y4M:
cargo run -q -p wav1c-cli -- input_10bit.y4m -o output_hdr.ivf \
--bit-depth 10 \
--hdr10 \
--color-range full \
--max-cll 203 \
--max-fall 64Custom color description + MDCV:
cargo run -q -p wav1c-cli -- input_10bit.y4m -o output_hdr.ivf \
--bit-depth 10 \
--color-range limited \
--color-primaries 9 \
--transfer 16 \
--matrix 9 \
--mdcv 34000,16000,13250,34500,7500,3000,15635,16450,10000000,1CLI HDR flags:
--bit-depth <8|10>--hdr10--color-range <limited|full>--color-primaries <u8>--transfer <u8>--matrix <u8>--max-cll <u16>and--max-fall <u16>(must be provided together)--mdcv <rx,ry,gx,gy,bx,by,wx,wy,max_lum,min_lum>
Notes:
- When input is Y4M and
--bit-depthor--color-rangeare omitted, values are inferred from Y4M headers. --hdr10applies default color description (primaries=9,transfer=16,matrix=9).
use wav1c::y4m::FramePixels;
use wav1c::{encode_packets, EncodeConfig};
let frame = FramePixels::solid(320, 240, 128, 128, 128);
let packets = encode_packets(&[frame], &EncodeConfig::default());
assert!(!packets.is_empty());
assert!(!packets[0].data.is_empty());use std::path::Path;
use wav1c::y4m::FramePixels;
use wav1c::{encode_packets, EncodeConfig, Fps};
let frames = FramePixels::all_from_y4m_file(Path::new("input.y4m"))?;
let config = EncodeConfig {
base_q_idx: 110,
keyint: 60,
fps: Fps::from_int(30).unwrap(),
b_frames: true,
gop_size: 4,
..EncodeConfig::default()
};
let packets = encode_packets(&frames, &config);
assert!(!packets.is_empty());
# Ok::<(), Box<dyn std::error::Error>>(())use wav1c::y4m::FramePixels;
use wav1c::{
encode_packets, BitDepth, ColorRange, ContentLightLevel, EncodeConfig, Fps,
MasteringDisplayMetadata, VideoSignal,
};
let frame = FramePixels::solid_with_bit_depth(
1920,
1080,
512,
512,
512,
BitDepth::Ten,
ColorRange::Full,
);
let config = EncodeConfig {
keyint: 60,
fps: Fps::from_int(30).unwrap(),
b_frames: true,
gop_size: 4,
video_signal: VideoSignal::hdr10(ColorRange::Full),
content_light: Some(ContentLightLevel {
max_content_light_level: 203,
max_frame_average_light_level: 64,
}),
mastering_display: Some(MasteringDisplayMetadata {
primaries: [[34000, 16000], [13250, 34500], [7500, 3000]],
white_point: [15635, 16450],
max_luminance: 10_000_000,
min_luminance: 1,
}),
..EncodeConfig::default()
};
let packets = encode_packets(&[frame], &config);
assert!(!packets.is_empty());Exported from the crate root:
BitDepthColorRangeColorDescriptionFpsVideoSignalContentLightLevelMasteringDisplayMetadata
Header: wav1c-ffi/include/wav1c.h
Canonical API:
wav1c_default_config()wav1c_encoder_new(...)wav1c_encoder_send_frame(...)(8-bit planes)wav1c_encoder_send_frame_u16(...)(10-bit planes)wav1c_encoder_rate_control_stats(...)wav1c_last_error_message()
Wav1cConfig fields:
fps_num,fps_den: frame rate rational (num/den)bit_depth:8or10color_range:0limited,1fullcolor_primaries,transfer_characteristics,matrix_coefficients: set to-1to omit color descriptionhas_cll,max_cll,max_fallhas_mdcv, primaries/white-point/luminance fields
Simple 8-bit SDR usage:
#include <stdio.h>
#include "wav1c.h"
Wav1cConfig cfg = wav1c_default_config();
cfg.base_q_idx = 128;
cfg.keyint = 60;
cfg.fps_num = 30;
cfg.fps_den = 1;
cfg.b_frames = 1;
cfg.gop_size = 4;
Wav1cEncoder *enc = wav1c_encoder_new(320, 240, &cfg);
if (!enc) {
fprintf(stderr, "encoder_new failed: %s\n", wav1c_last_error_message());
return -1;
}
// y/u/v are 4:2:0 8-bit planes. Lengths are sample counts, not byte counts.
if (wav1c_encoder_send_frame(enc, y, y_len, u, u_len, v, v_len, 0, 0) != WAV1C_STATUS_OK) {
fprintf(stderr, "send_frame failed: %s\n", wav1c_last_error_message());
wav1c_encoder_free(enc);
return -1;
}
wav1c_encoder_flush(enc);
for (Wav1cPacket *pkt = NULL; (pkt = wav1c_encoder_receive_packet(enc)) != NULL; ) {
// pkt->data / pkt->size contains AV1 OBU payload
wav1c_packet_free(pkt);
}
wav1c_encoder_free(enc);Simple rate-control stats query:
#include <stdio.h>
#include "wav1c.h"
Wav1cConfig cfg = wav1c_default_config();
cfg.target_bitrate = 2 * 1000 * 1000; // 2 Mbps
Wav1cEncoder *enc = wav1c_encoder_new(1280, 720, &cfg);
if (!enc) return -1;
Wav1cRateControlStats stats;
if (wav1c_encoder_rate_control_stats(enc, &stats) == WAV1C_STATUS_OK) {
printf("target=%llu, avg_qp=%u, buffer=%u%%\n",
(unsigned long long)stats.target_bitrate,
stats.avg_qp,
stats.buffer_fullness_pct);
}
wav1c_encoder_free(enc);10-bit HDR usage:
#include <stdio.h>
#include "wav1c.h"
Wav1cConfig cfg = wav1c_default_config();
cfg.keyint = 60;
cfg.fps_num = 30;
cfg.fps_den = 1;
cfg.b_frames = 1;
cfg.gop_size = 4;
cfg.bit_depth = 10;
cfg.color_range = 1; // full
cfg.color_primaries = 9;
cfg.transfer_characteristics = 16;
cfg.matrix_coefficients = 9;
cfg.has_cll = 1;
cfg.max_cll = 203;
cfg.max_fall = 64;
Wav1cEncoder *enc = wav1c_encoder_new(1920, 1080, &cfg);
if (!enc) {
fprintf(stderr, "encoder_new failed: %s\n", wav1c_last_error_message());
return -1;
}
// y_len/u_len/v_len are sample counts, not byte counts.
if (wav1c_encoder_send_frame_u16(
enc, y, y_len, u, u_len, v, v_len, y_stride, uv_stride) != WAV1C_STATUS_OK) {
fprintf(stderr, "send_frame_u16 failed: %s\n", wav1c_last_error_message());
wav1c_encoder_free(enc);
return -1;
}
wav1c_encoder_flush(enc);
Wav1cPacket *pkt = wav1c_encoder_receive_packet(enc);
if (pkt) wav1c_packet_free(pkt);
wav1c_encoder_free(enc);Build artifacts:
libwav1c_ffi.dylib/libwav1c_ffi.solibwav1c_ffi.a
Main entry point: WasmEncoder
Constructor:
new(width, height, base_q_idx, keyint, b_frames, gop_size, fps_num, fps_den, target_bitrate, bit_depth, color_range, cp, tc, mc, has_cll, max_cll, max_fall)
10-bit/HDR methods:
encode_frame_10bit(y, u, v)set_hdr10(color_range)set_video_signal(bit_depth, color_range, cp, tc, mc)set_content_light_level(max_cll, max_fall)set_mastering_display_metadata(...)rate_control_stats()
Important: signal and metadata mutators must be called before the first submitted frame.
This repository contains ffmpeg-libwav1c.patch, and we also maintain direct FFmpeg integration updates in ../FFmpeg during active development.
Build wav1c-ffi first:
cargo build -p wav1c-ffi --releaseConfigure FFmpeg with libwav1c:
cd ../FFmpeg
./configure --enable-libwav1c \
--extra-cflags="-I/absolute/path/to/wav1c/wav1c-ffi/include" \
--extra-ldflags="-L/absolute/path/to/wav1c/target/release"
make -j$(sysctl -n hw.ncpu 2>/dev/null || nproc)Run with dynamic library path (macOS example):
DYLD_LIBRARY_PATH=/absolute/path/to/wav1c/target/release ./ffmpeg \
-i input.y4m \
-pix_fmt yuv420p10le \
-c:v libwav1c \
-wav1c-hdr10 1 \
-wav1c-max-cll 203 \
-wav1c-max-fall 64 \
-f ivf output.ivfSupported FFmpeg libwav1c pixel formats:
yuv420pyuv420p10le
Supported FFmpeg private options:
-wav1c-q-wav1c-b-frames-wav1c-gop-size-wav1c-hdr10-wav1c-max-cll-wav1c-max-fall-wav1c-mdcv
For a full HEIC -> HDR AV1 workflow (CLI and FFmpeg), including verification with ffprobe and dav1d, see:
docs/HDR_HEIC_MANUAL.md
Practical note for HEIC inputs:
- Some HEIC files decode as tile grids.
- Do not force-map an individual dependent tile if you want the full composed image.
- For very large outputs, prefer AVIF when IVF/MP4 16-bit container dimensions would be exceeded.
cargo test --workspaceIntegration tests decode encoded output with dav1d. If dav1d is unavailable, tests that require it are skipped.
This project is licensed under the Mozilla Public License 2.0. See LICENSE.