Most Go media libraries eventually hit the same wall.
They don't have their own decoder.
Instead, they rely on a system-installed FFmpeg package, shared libraries, platform-specific DLLs, or external binaries that must already exist on the target machine.
Everything works fine during development. Then deployment starts.
And suddenly you're debugging missing DLLs on Windows, incompatible FFmpeg versions on Linux, Homebrew version mismatches on macOS, container images missing required libs, or a routine package update silently breaking media processing in production.
The Goal
I needed a decoder that:
- Has pure Go API
- Compiles into a single binary
- Requires no FFmpeg or other runtime dependencies
- Supports modern codecs — AV1, H.264, H.265, VP8, VP9, Opus, AAC, MP3, and more
- Can read from files, memory buffers, HTTP streams, pipes, SSH streams, and any
io.Reader
The result is gopeg.
Instead of depending on whatever media stack happens to be installed on the machine, the required FFmpeg components and the dav1d decoder are statically linked into the application at build time via cgo.
By the time the binary is ready, everything needed to decode video is already inside it.
Why This Matters
An enormous amount of time gets spent on deployment problems rather than on actual application logic. A typical media processing pipeline often looks like this:
- Install FFmpeg
- Install compatible shared libraries
- Package them correctly
- Make sure production machines have compatible versions
- Hope that future OS updates don't break anything
The application becomes dependent on external state. With static linking, the deployment story collapses to a single step: go build, and you're done.
No external codec packages. No DLLs. No runtime library search paths or hacky cmd.Exec wrappers. No "works on my machine."
io.Reader Support
Flexibility was another core design goal. The decoder doesn't need direct file access — it should work with arbitrary streams:
resp, _ := http.Get(url)
dec, err := gopeg.NewDecoder(resp.Body)
Or:
dec, err := gopeg.NewDecoder(os.Stdin)
Or any data arriving over an SSH connection or a custom transport. When the source implements Seek(), the library can extract metadata as well — though note that for true streams, accurate duration cannot be determined.
Simplicity Over Abstraction
One of my main goals was to avoid building a heavyweight framework. The API is intentionally as minimal as possible.
Open a source:
dec, err := gopeg.NewDecoder(file)
Read metadata:
meta := dec.Meta()
Decode frames:
for {
frame, err := dec.DecodeFrame()
if err != nil {
panic(err)
}
if frame == nil {
break
}
// process frame
}
No background workers. No hidden processes. No external dependencies. Just a clean Go interface with idiomatic types.
Wrapping Up
FFmpeg is an incredible piece of software, but depending on a system-installed FFmpeg introduces operational complexity that many applications simply don't need. For my use cases, a statically linked approach turned out to be the more sensible choice — and as a bonus, the final binary size came out smaller than expected.
The result is a media decoder that's easy to embed, easy to deploy, and behaves predictably in any environment.
One self-contained binary. Everything needed to decode video, already inside.
That's why gopeg is the first complete Go solution that gives access to all modern decoders without requiring platform DLLs or external packages — making deployment dramatically simpler.
Link: gopeg repository on github.
Top comments (0)