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.