FFmpeg Source Code: Architecture Overview and How to Navigate 2 Million Lines of C
Prerequisites
- ›Basic C programming (structs, pointers, function pointers)
- ›Familiarity with Makefiles and build systems
- ›General understanding of what FFmpeg does (video/audio processing)
FFmpeg Source Code: Architecture Overview and How to Navigate 2 Million Lines of C
FFmpeg is one of the most consequential open-source projects ever written. Nearly every piece of software that touches multimedia—VLC, Chrome, OBS, Handbrake, YouTube's backend, your smart TV—depends on FFmpeg either directly or through its libraries. The project contains roughly 2.4 million lines of C code, supports over 400 codecs, nearly 400 container formats, and dozens of I/O protocols. Despite its ubiquity, surprisingly few developers have read beyond the public API headers.
This article is the first in a six-part series that will take you deep inside FFmpeg's source code. We'll start here with the big picture: what the project contains, how its pieces fit together, and the design patterns you'll encounter everywhere once you start reading the code.
What FFmpeg Is: 7 Libraries and 3 Tools
FFmpeg is not a single program—it's a platform. The repository ships seven shared libraries and three command-line tools, each with a distinct responsibility:
| Library | Purpose |
|---|---|
| libavutil | Common utilities: math, logging, pixel formats, AVBuffer, AVFrame, AVClass |
| libswscale | Image scaling and pixel format conversion |
| libswresample | Audio resampling and sample format conversion |
| libavcodec | Encoding and decoding (400+ codecs) |
| libavformat | Container muxing/demuxing and I/O protocols |
| libavfilter | Audio and video filter graph engine |
| libavdevice | Platform-specific capture and playback devices |
| Tool | Purpose |
|---|---|
| ffmpeg | The transcoding workhorse—reads, decodes, filters, encodes, and writes media |
| ffplay | SDL2-based media player for previewing |
| ffprobe | Stream/container analysis and metadata extraction |
The libraries are designed to be used independently. You can link just libavcodec and libavutil if all you need is decoding—you don't have to drag in the entire project.
Directory Structure Map
Navigating 2.4 million lines requires knowing where things live. Here's the top-level directory map:
| Directory | Contents |
|---|---|
libavutil/ |
~300 files. Shared primitives: buffers, frames, pixel/sample formats, math, logging |
libavcodec/ |
~2,700 files. Every codec implementation plus the codec framework |
libavformat/ |
~800 files. Container format demuxers/muxers, I/O protocols |
libavfilter/ |
~600 files. Audio/video filter implementations and the graph engine |
libswscale/ |
~100 files. Pixel format conversion and scaling |
libswresample/ |
~50 files. Audio sample rate/format conversion |
libavdevice/ |
~50 files. OS-specific device I/O (ALSA, PulseAudio, V4L2, etc.) |
fftools/ |
~20 files. The three command-line tools and the scheduler/pipeline engine |
ffbuild/ |
Build tool support files (common.mak, library.mak, version.sh) |
doc/ |
Documentation and 24 standalone example programs in doc/examples/ |
tests/ |
FATE test framework and test data |
compat/ |
Compatibility shims for non-POSIX platforms |
Tip: The
libavcodec/directory alone accounts for more than half the codebase. Each codec typically lives in one or two.cfiles named after the codec (e.g.,libavcodec/h264dec.c,libavcodec/libx264.c).
Library Dependency Hierarchy
The seven libraries form a strict dependency hierarchy. This isn't a suggestion—it's enforced at link time by the Makefile. The linking order is defined at Makefile#L33-L41:
# $(FFLIBS-yes) needs to be in linking order
FFLIBS-$(CONFIG_AVDEVICE) += avdevice
FFLIBS-$(CONFIG_AVFILTER) += avfilter
FFLIBS-$(CONFIG_AVFORMAT) += avformat
FFLIBS-$(CONFIG_AVCODEC) += avcodec
FFLIBS-$(CONFIG_SWRESAMPLE) += swresample
FFLIBS-$(CONFIG_SWSCALE) += swscale
FFLIBS := avutil
The dependency flows downward—each library may only use symbols from libraries listed below it:
flowchart TD
avdevice["libavdevice"] --> avfilter["libavfilter"]
avdevice --> avformat["libavformat"]
avfilter --> avformat
avfilter --> avcodec["libavcodec"]
avformat --> avcodec
avcodec --> swresample["libswresample"]
avcodec --> swscale["libswscale"]
swresample --> avutil["libavutil"]
swscale --> avutil
avcodec --> avutil
This layering is a deliberate architectural decision. It means libavutil has zero dependencies on any other FFmpeg library, making it a safe foundation. It also means libavcodec knows nothing about container formats—the separation between codec and container is physical, not just conceptual.
The Build Pipeline: configure → config.h → make
FFmpeg uses a custom configure script—not autotools, not CMake—written as a single POSIX shell script. The build pipeline has three stages:
flowchart LR
A["./configure"] --> B["config.h\nconfig_components.h\nffbuild/config.mak"]
B --> C["make"]
C --> D["7 libraries + 3 tools"]
The configure script starts with shell compatibility detection at configure#L1-L55—it literally tries alternative shells if the current one is broken. This pragmatism is characteristic of the entire project.
The most interesting part of configure is find_things_extern, the function that discovers available components at configure#L4507-L4513:
find_things_extern(){
thing=$1
pattern=$2
file=$source_path/$3
out=${4:-$thing}
sed -n "s/^[^#]*extern.*$pattern *ff_\([^ ]*\)_$thing;/\1_$out/p" "$file"
}
This function uses sed to scan source files for extern declarations matching a pattern. For example, it scans libavcodec/allcodecs.c for lines like extern const FFCodec ff_h264_decoder; and extracts h264_decoder. The result is a list of every available component, which configure then uses to generate config_components.h with #define CONFIG_H264_DECODER 1 entries.
On the Make side, the DOSUBDIR macro at Makefile#L123-L132 is the engine that builds all seven libraries from a single template:
define DOSUBDIR
$(foreach V,$(SUBDIR_VARS),$(eval $(call RESET,$(V))))
SUBDIR := $(1)/
include $(SRC_PATH)/$(1)/Makefile
-include $(SRC_PATH)/$(1)/$(ARCH)/Makefile
-include $(SRC_PATH)/$(1)/$(INTRINSICS)/Makefile
include $(SRC_PATH)/ffbuild/library.mak
endef
$(foreach D,$(FFLIBS),$(eval $(call DOSUBDIR,lib$(D))))
For each library, DOSUBDIR resets all variables, includes the library's own Makefile (which sets OBJS, HEADERS, etc.), optionally includes arch-specific Makefiles for SIMD code, and then includes library.mak which contains the actual build rules. It's a clean example of Make metaprogramming.
Component Registration Pattern
FFmpeg achieves compile-time component selection through a registration pattern that connects configure-time discovery with runtime iteration. The pattern appears identically in three files:
libavcodec/allcodecs.c#L39-L80— codecslibavformat/allformats.c#L33-L80— container formatslibavfilter/allfilters.c#L25-L60— filters
Each file declares extern symbols for every possible component:
extern const FFCodec ff_a64multi_encoder;
extern const FFCodec ff_aasc_decoder;
extern const FFCodec ff_h264_decoder;
// ... hundreds more
flowchart TD
subgraph Build Time
A["allcodecs.c declares\nextern FFCodec ff_h264_decoder"] --> B["configure scans with\nfind_things_extern"]
B --> C["Generates config_components.h\n#define CONFIG_H264_DECODER 1"]
end
subgraph Link Time
C --> D["Linker includes h264dec.o\nonly if CONFIG_H264_DECODER=1"]
end
subgraph Runtime
D --> E["av_codec_iterate() walks\nthe codec_list[] array"]
end
The configure script uses find_things_extern to discover which components exist by scanning these extern declarations. Components that are enabled get #define CONFIG_XXX 1 in config_components.h. The library-specific Makefile conditionally compiles the implementation file only when the config flag is set. The linker resolves the extern symbol only for enabled components; disabled ones simply aren't compiled.
At runtime, functions like av_codec_iterate() walk a static array of pointers to the enabled component structs, which is also generated based on the config flags. This design means adding a new codec requires exactly three changes: writing the implementation, adding an extern line to allcodecs.c, and adding a build rule to the Makefile.
Naming Conventions and How to Find Things
FFmpeg's codebase is vast, but its naming conventions are remarkably consistent. Once you learn the patterns, you can predict file and symbol names:
Symbol prefixes:
av_— Public API (safe for external use, ABI-stable)ff_— Internal to FFmpeg (shared between files within a library, but not public)- No prefix — Static to a single file
Codec naming:
- Decoder:
ff_{name}_decoder(e.g.,ff_h264_decoder) - Encoder:
ff_{name}_encoder(e.g.,ff_libx264_encoder) - File: usually
{name}dec.cor{name}enc.corlib{name}.cfor external wrappers
Format naming:
- Demuxer:
ff_{name}_demuxer(e.g.,ff_mov_demuxer) - Muxer:
ff_{name}_muxer(e.g.,ff_mp4_muxer)
Filter naming:
- Video filter:
ff_vf_{name}(e.g.,ff_vf_scale) - Audio filter:
ff_af_{name}(e.g.,ff_af_aresample) - Video source:
ff_vsrc_{name}, Audio source:ff_asrc_{name} - Video sink:
ff_vsink_{name}, Audio sink:ff_asink_{name}
Tip: When looking for a specific codec implementation, grep
allcodecs.cfor the codec name. The extern declaration tells you the symbol name, and the codec name maps predictably to the source file. For instance,ff_h264_decoderlives inlibavcodec/h264dec.c.
The Public/Private Struct Split Pattern
This is the single most important design pattern in FFmpeg. If you understand it, you can navigate every major subsystem. If you don't, the code will seem bewildering.
Every major type in FFmpeg has two versions: a public struct that's part of the ABI, and a private struct that embeds the public one as its first member. The private struct is only used within the library; external consumers only see the public struct.
Here's the pattern as seen in libavcodec/codec_internal.h#L127-L131:
typedef struct FFCodec {
/**
* The public AVCodec. See codec.h for it.
*/
AVCodec p;
// ... private fields follow
} FFCodec;
The C language guarantees that a pointer to a struct can be cast to a pointer to its first member (and vice versa). This means an FFCodec* can be safely cast to an AVCodec* and passed to external code. Internally, FFmpeg casts back with a simple inline function:
static av_always_inline const FFCodec *ffcodec(const AVCodec *codec)
{
return (const FFCodec*)codec;
}
classDiagram
class AVCodec {
+name: const char*
+long_name: const char*
+type: AVMediaType
+id: AVCodecID
+capabilities: int
}
class FFCodec {
+p: AVCodec
+caps_internal: unsigned
+cb_type: unsigned
+cb: union
+init(): int
+close(): int
}
class AVInputFormat {
+name: const char*
+long_name: const char*
+flags: int
}
class FFInputFormat {
+p: AVInputFormat
+read_probe(): int
+read_header(): int
+read_packet(): int
}
class AVFilter {
+name: const char*
+description: const char*
+inputs: AVFilterPad*
+outputs: AVFilterPad*
}
class FFFilter {
+p: AVFilter
+preinit(): int
+init(): int
+activate(): int
+uninit(): void
}
FFCodec *-- AVCodec : embeds as first member
FFInputFormat *-- AVInputFormat : embeds as first member
FFFilter *-- AVFilter : embeds as first member
This pattern appears across every subsystem:
| Public (ABI-stable) | Private (internal) | Cast helper |
|---|---|---|
AVCodec |
FFCodec |
ffcodec() |
AVInputFormat |
FFInputFormat |
ffinputformat() |
AVOutputFormat |
FFOutputFormat |
ffoutputformat() |
AVFilter |
FFFilter |
fffilter() |
AVFilterContext |
FFFilterContext |
fffilterctx() |
The benefit is ABI stability: the public struct's layout never changes (fields are only appended), while the private struct can be freely rearranged or extended. This is why FFmpeg can maintain binary compatibility across versions despite rapid internal development.
What's Next
With this mental model—seven layered libraries, a configure-based build system, extern-based component registration, and the ubiquitous public/private struct split—you're equipped to explore any part of FFmpeg's source code.
In Part 2, we'll dive deep into the data structures that serve as the "currency" of the entire ecosystem: AVBuffer for reference counting, AVPacket for encoded data, AVFrame for decoded data, and the AVClass/AVOption reflection system that enables structured logging and runtime configuration across every subsystem.