Optimize QuickTime streaming with Go and qt-faststart
To prepare an MP4 or MOV for progressive HTTP playback, use Go’s os/exec package to run FFmpeg with
-c copy -movflags +faststart. The program below creates a non-fragmented MP4 with its playback index
ahead of the media data, keeps the first video and all audio streams, and refuses an existing output.
It uses the qtfaststart approach without a third-party Go package or a separate qt-faststart
executable.
QuickTime streaming challenges
MP4 and QuickTime containers store information in boxes, also called atoms. The moov box contains
track and timing information; mdat holds media data. When moov follows mdat, a player may need
an extra HTTP range request to fetch the index. Without range support, it may need to download the
file before it can read that index.
Moving moov before mdat makes the index available near the start of a sequential download.
Playback still depends on buffering, codecs, the player, and the HTTP server. This changes the file
layout; it does not establish a startup-time or seeking benchmark.
Use FFmpeg’s fast-start operation
FFmpeg offers both the qt-faststart utility and the -movflags +faststart muxer option. The latter
runs a second pass to move the index to the front of the file. It does not apply to fragmented output,
as the MOV/MP4 muxer documentation
explains. This walkthrough uses the FFmpeg command-line program for the remux and lets Go handle
temporary storage and publication.
Setting up the Go environment
Use a Linux filesystem that supports hard links, a writable output directory, and a valid local
MP4 or MOV containing video. Its selected video and audio codecs must be supported by the MP4 muxer
and your intended player. H.264 video with AAC audio is the tested combination; a .mov extension
alone does not establish codec compatibility. The shell commands use Bash and grep.
Install Go and a package containing both FFmpeg and FFprobe. FFprobe is a separate executable used for inspection. These examples were tested with Go 1.26.8 and FFmpeg/FFprobe 9.0.1 on Linux. Those are tested versions, not intrinsic minimums; use maintained patch releases in those release lines. Go’s release archive includes the tested Go version. No module initialization or third-party Go dependencies are needed.
Before saving the program, check that all three commands are on your PATH:
go version && ffmpeg -version && ffprobe -version
Optimizing QuickTime files with Go
Save this program as faststart.go in your working directory. It remuxes into a private temporary
directory inside the destination directory, then creates a hard link at the requested output path.
Go’s os.Link provides that publication step. The input is read without
replacement, and an existing output, including a symbolic link, is refused.
package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
)
func optimize(inputPath, outputPath string) error {
input, err := filepath.EvalSymlinks(inputPath)
if err != nil {
return err
}
input, err = filepath.Abs(input)
if err != nil {
return err
}
info, err := os.Stat(input)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("input must be a regular local file")
}
parent, name := filepath.Split(outputPath)
if name == "" {
return fmt.Errorf("output must name a new file")
}
if parent == "" {
parent = "."
}
parent, err = filepath.EvalSymlinks(parent)
if err != nil {
return err
}
parent, err = filepath.Abs(parent)
if err != nil {
return err
}
output := filepath.Join(parent, name)
if _, err := os.Lstat(output); !os.IsNotExist(err) {
return fmt.Errorf("output must not exist")
}
temporary, err := os.MkdirTemp(parent, ".faststart-")
if err != nil {
return err
}
defer os.RemoveAll(temporary)
candidate := filepath.Join(temporary, "result.mp4")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, "ffmpeg", "-nostdin", "-n", "-i", input,
"-map", "0:v:0", "-map", "0:a?", "-c", "copy", "-movflags", "+faststart", candidate)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("fast-start conversion failed: %w", err)
}
if err := os.Link(candidate, output); err != nil {
return fmt.Errorf("cannot publish result without overwriting: %w", err)
}
return nil
}
func main() {
if len(os.Args) != 3 {
fmt.Fprintln(os.Stderr, "Usage: go run faststart.go <input.mp4> <new-output.mp4>")
os.Exit(1)
}
if err := optimize(os.Args[1], os.Args[2]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("Fast-start conversion complete")
}
Replace input.mp4 with your local MP4 or MOV path. Choose an output filename that does not exist;
the output is MP4 regardless of the input’s container or the destination’s suffix:
go run faststart.go input.mp4 output_optimized.mp4
On success, the program prints Fast-start conversion complete. FFmpeg diagnostics go to the terminal. A failed remux or publication returns a nonzero status; on that return path, Go removes its temporary directory. Fix the reported problem and retry with a new output name. To remux the same input again after success, choose another output name too.
The stream selection is intentional:
-map 0:v:0selects the first video stream, even if another video stream is marked as default.-map 0:a?keeps every audio stream and allows video-only input.-c copyuses stream copying without re-encoding.
Subtitles, data tracks, and additional video streams are excluded. Remuxing can change container metadata and packet representation, so the resulting file is not a byte-for-byte copy of the input.
Check the result
Inspect the top-level boxes in the new file:
(
set -o pipefail
ffprobe -v trace output_optimized.mp4 2>&1 |
grep -E "type:'(moov|mdat|moof)' parent:'root'"
)
Read the lines in order: moov must precede mdat, and there should be no moof box for this
non-fragmented output. Trace formatting is diagnostic output, not a stable parser API. This is a
manual inspection on the tested FFprobe version. Substitute the input filename to inspect its layout
before deciding whether remuxing is needed. An already optimized input still produces a new file
when you run the wrapper.
Check the selected tracks, then decode their complete contents to catch errors that a header probe can miss:
ffprobe -v error -show_entries stream=codec_type,codec_name -of compact output_optimized.mp4 &&
ffmpeg -nostdin -v error -xerror -i output_optimized.mp4 -map 0:v:0 -map '0:a?' -f null -
Expect one video stream and the same number of audio streams as in the input. The decoder prints nothing on an error-free run and exits with status zero. Treat any error diagnostics as a failed check even if your FFmpeg build returns zero. These checks establish layout and FFmpeg decodability, not support in every player; try the result in your intended player before serving it.
Common pitfalls and troubleshooting
- Incompatible codecs: stream copying cannot turn an unsupported codec into a supported one. A MOV can contain audio or video that the MP4 muxer refuses. Transcode deliberately in a separate workflow when needed; this wrapper never falls back to encoding.
- Missing output directory or hard-link support: create or select a writable directory on a filesystem that supports hard links. A publication error leaves the requested destination uncreated by this program; it does not replace a file someone else created there.
- Disk space and time: keep room for the original and another MP4 of roughly similar size. Fast-start adds a second pass over the output, and the wrapper caps FFmpeg execution at five minutes. A timeout requires fixing the cause or deliberately adjusting that limit.
- Damaged input: stream copying does not decode or repair the source. A successful remux is not proof that every packet was intact. Start with valid media and inspect decoding diagnostics.
- Interrupted process: deferred cleanup runs when the Go function returns. An abrupt process
termination can leave a
.faststart-*directory behind; inspect it before removing it.
Fast-start prepares a single file for progressive delivery. HLS and DASH use manifests and media
segments; moving this MP4’s moov box does not create an adaptive streaming ladder. Keep that task
separate from this non-fragmented MP4 remux.
For other container options, consult the FFmpeg muxer reference. For untrusted uploads, run media processing in an isolated worker; the example’s timeout alone is not a sandbox.
