Adaptive video streaming in Go with FFmpeg

Adaptive streaming is essential for delivering high-quality video content efficiently across various network conditions. In this DevTip, we'll explore how to implement adaptive video streaming in Go using FFmpeg, specifically focusing on HTTP Live Streaming (HLS) and MPEG-DASH formats. Before we dive in, ensure you have a Go development environment set up and FFmpeg installed on your system.
Introduction to adaptive streaming: HLS vs MPEG-DASH
Adaptive bitrate streaming dynamically adjusts video quality based on the viewer's network
conditions. Two popular standards are HLS (developed by Apple) and MPEG-DASH (an open standard). HLS
uses .m3u8
manifest files, while MPEG-DASH utilizes .mpd
files. Both formats segment videos into
smaller chunks, allowing seamless quality adjustments.
Setting up FFmpeg with Go: choosing the right wrapper
Several Go wrappers for FFmpeg exist, but goffmpeg
(specifically v2
) stands out due to its
simplicity and active maintenance. First, initialize a Go module in your project directory if you
haven't already:
go mod init your_module_name
Then, install goffmpeg/v2
using:
go get github.com/xfrr/goffmpeg/v2
After updating your code and go.mod
file, it's good practice to run go mod tidy
to clean up
dependencies.
Converting videos to HLS format with goffmpeg
Here's how you can convert a video to HLS format using goffmpeg/v2
:
package main
import (
"log"
"time"
"github.com/xfrr/goffmpeg/v2/transcoder"
)
func main() {
trans := transcoder.New() // Use the New() constructor
inputPath := "input.mp4"
outputPath := "output.m3u8"
// Initialize the transcoder with input, output, and HLS options using the builder pattern
err := trans.
Input(inputPath).
Output(outputPath).
OutputOptions(
transcoder.WithOutputFormat("hls"), // Specify HLS format
transcoder.HLSSegmentDuration(10*time.Second), // Set HLS segment duration
transcoder.HLSPlaylistType("vod"), // Set HLS playlist type
).
Initialize()
if err != nil {
log.Fatalf("Failed to initialize transcoder: %v", err)
}
// Run the transcoding process
done := trans.Run(true) // Pass true to show progress, false to hide
// Handle progress output (Progress is a struct in v2)
progress := trans.Output()
for p := range progress {
log.Printf("Progress: %+v\n", p) // Example: log the Progress struct
}
// Wait for the transcoding to complete
err = <-done
if err != nil {
log.Fatalf("HLS conversion failed: %v", err)
} else {
log.Println("HLS conversion completed successfully.")
}
}
Implementing MPEG-DASH conversion in Go
To convert videos to MPEG-DASH, you can use FFmpeg directly via Go's os/exec
package. This
approach gives you direct access to FFmpeg's command-line interface.
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("ffmpeg", "-i", "input.mp4", "-f", "dash", "output.mpd")
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
Creating adaptive bitrate variants
Adaptive streaming requires multiple bitrate variants to cater to different network speeds. Here's how to generate them for MPEG-DASH using FFmpeg. For production scenarios, you might also consider separate bitrate variants for audio streams.
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("ffmpeg", "-i", "input.mp4",
"-map", "0:v", "-map", "0:a", // Map video and audio streams
"-b:v:0", "800k", "-b:v:1", "1200k", "-b:v:2", "2000k", // Define video bitrates for different variants
"-f", "dash", "output.mpd")
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
Building a streaming server with segment handling
Use Go's built-in HTTP server to serve video segments. Ensure the directory specified (e.g.,
./videos
) exists and contains your manifest files (.mpd
or .m3u8
) and the corresponding video
segment files.
package main
import (
"log"
"net/http"
)
func main() {
// Serve files from the "./videos" directory under the "/videos/" path
http.Handle("/videos/", http.StripPrefix("/videos/", http.FileServer(http.Dir("./videos"))))
log.Println("Starting server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Optimizing performance with concurrent processing
Leverage Go's concurrency features (goroutines and wait groups) to process multiple videos simultaneously, significantly speeding up batch operations.
package main
import (
"log"
"sync"
"os/exec"
)
func processVideo(input, output string, wg *sync.WaitGroup) {
defer wg.Done() // Decrement the counter when the goroutine completes
log.Printf("Processing %s to %s\n", input, output)
cmd := exec.Command("ffmpeg", "-i", input, "-f", "dash", output)
if err := cmd.Run(); err != nil {
log.Printf("Error processing %s: %v\n", input, err)
} else {
log.Printf("Finished processing %s\n", input)
}
}
func main() {
var wg sync.WaitGroup
videos := []struct{ input, output string }{
{"input1.mp4", "output1.mpd"},
{"input2.mp4", "output2.mpd"},
// Add more videos here
}
for _, v := range videos {
wg.Add(1) // Increment the counter for each goroutine
go processVideo(v.input, v.output, &wg)
}
wg.Wait() // Wait for all goroutines to complete
log.Println("All videos processed.")
}
Testing and debugging your streaming implementation
Use tools like VLC media player or browser-based HLS/DASH players (e.g., Shaka Player, Video.js) to
test your streams. Verify that your manifest files (.m3u8
or .mpd
) correctly reference the video
segments and that playback adapts to simulated network condition changes if your player supports it.
Check FFmpeg logs for any errors during transcoding.
Conclusion
Implementing adaptive streaming in Go with FFmpeg is straightforward and powerful. By following these steps, you can efficiently deliver high-quality video content tailored to your users' network conditions.
If you're looking for a managed solution, Transloadit's 🤖 /video/adaptive Robot simplifies adaptive streaming by handling HLS and MPEG-DASH conversions seamlessly. Check out our go-sdk for easy integration.