Build an FFmpeg Wrapper in Go for Live Streaming
This article provides a step-by-step guide on how to build a custom FFmpeg wrapper in Go (Golang) to facilitate live video streaming. You will learn how to execute FFmpeg as a subprocess, handle input and output pipes, manage the process lifecycle, and stream live video to a destination like an RTMP server.
Why Build a Custom Wrapper?
While there are existing Go bindings for FFmpeg, many rely on CGO,
which complicates cross-compilation and deployment. Building a wrapper
around the FFmpeg command-line interface (CLI) using Go’s
os/exec package avoids CGO, allows for easy scaling, and
gives you direct control over the streaming process.
Step 1: Define the Wrapper Structure
First, create a struct in Go to manage the FFmpeg process, its inputs, and its error logs.
package main
import (
"io"
"os/exec"
)
// FFmpegStreamer manages the FFmpeg subprocess for live streaming
type FFmpegStreamer struct {
cmd *exec.Cmd
stdin io.WriteCloser
stderr io.ReadCloser
}Step 2: Initialize the FFmpeg Command
To stream live video, you need to configure FFmpeg to accept input
via a pipe (stdin) and output the stream to a destination
(such as an RTMP or SRT URL).
The following function initializes the command with optimized arguments for low-latency live streaming.
// NewFFmpegStreamer configures the FFmpeg command
func NewFFmpegStreamer(rtmpURL string, width, height int, fps int) (*FFmpegStreamer, error) {
// Arguments to read raw RGBA video from stdin and stream to an RTMP server
args := []string{
"-f", "rawvideo",
"-vcodec", "rawvideo",
"-pix_fmt", "rgba",
"-s", string(width) + "x" + string(height),
"-r", string(fps),
"-i", "-", // Read input from stdin pipe
"-c:v", "libx264",
"-preset", "veryfast",
"-tune", "zerolatency",
"-f", "flv", // FLV container format for RTMP
rtmpURL,
}
cmd := exec.Command("ffmpeg", args...)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
return &FFmpegStreamer{
cmd: cmd,
stdin: stdin,
stderr: stderr,
}, nil
}Step 3: Start and Monitor the Process
You need to start the process and monitor its stderr
output. FFmpeg writes its logs and status updates to
stderr, which is essential for debugging and tracking
stream health.
import (
"bufio"
"log"
)
// Start runs the FFmpeg process and monitors errors in a goroutine
func (s *FFmpegStreamer) Start() error {
if err := s.cmd.Start(); err != nil {
return err
}
// Read stderr in a separate goroutine to prevent blocking
go func() {
scanner := bufio.NewScanner(s.stderr)
for scanner.Scan() {
log.Printf("[FFmpeg] %s\n", scanner.Text())
}
}()
return nil
}Step 4: Write Video Frames to the Stream
With the process running and the input pipe open, you can write raw video frame data directly into the FFmpeg wrapper.
// WriteFrame sends raw byte data (e.g., RGBA pixels) to FFmpeg
func (s *FFmpegStreamer) WriteFrame(frame []byte) error {
_, err := s.stdin.Write(frame)
return err
}Step 5: Stop the Stream Safely
To stop streaming, close the stdin pipe to tell FFmpeg
that the input stream has ended. This allows FFmpeg to finish writing
remaining packets and shut down gracefully.
// Close stops the FFmpeg process safely
func (s *FFmpegStreamer) Close() error {
err := s.stdin.Close()
if err != nil {
return err
}
return s.cmd.Wait()
}How to Use the Wrapper
Below is a complete implementation showing how to initialize the wrapper and stream generated dummy frames (e.g., solid color buffers) to an RTMP server.
package main
import (
"log"
"time"
)
func main() {
rtmpDestination := "rtmp://localhost/live/stream_key"
width, height, fps := 640, 480, 30
frameSize := width * height * 4 // 4 bytes per pixel for RGBA
streamer, err := NewFFmpegStreamer(rtmpDestination, width, height, fps)
if err != nil {
log.Fatalf("Failed to initialize streamer: %v", err)
}
if err := streamer.Start(); err != nil {
log.Fatalf("Failed to start streamer: %v", err)
}
log.Println("Streaming started...")
// Create a dummy frame (red color)
dummyFrame := make([]byte, frameSize)
for i := 0; i < frameSize; i += 4 {
dummyFrame[i] = 255 // R
dummyFrame[i+1] = 0 // G
dummyFrame[i+2] = 0 // B
dummyFrame[i+3] = 255 // A
}
// Stream for 10 seconds
ticker := time.NewTicker(time.Second / time.Duration(fps))
defer ticker.Stop()
stopChan := time.After(10 * time.Second)
for {
select {
case <-ticker.C:
if err := streamer.WriteFrame(dummyFrame); err != nil {
log.Printf("Error writing frame: %v", err)
return
}
case <-stopChan:
log.Println("Stopping stream...")
if err := streamer.Close(); err != nil {
log.Printf("Error closing streamer: %v", err)
}
log.Println("Stream stopped.")
return
}
}
}