Remux without re-encoding (stream copy)¶
What ffmpeg -i in.mp4 -c copy out.mkv does on the command line (-c:v copy -c:a copy: copy
the streams, change only the container), as an afmpeg job:
cmd := afmpeg.Command{
Inputs: []afmpeg.Input{{Path: "in.mp4"}},
Outputs: []afmpeg.Output{{
Path: "out.mkv",
Map: []string{"0:v", "0:a"},
VideoCodec: afmpeg.CodecCopy,
AudioCodec: afmpeg.CodecCopy,
}},
}
_, err := rt.RunJob(ctx, fs, cmd)
Most container changes don't need a transcode. Moving mp4 → mkv, or joining segments that
already share a codec, is a stream copy: read the demuxed packets, optionally fix their
bitstream framing, and mux them straight back out, with no decode and no encode. It's fast, lossless,
and needs no codec at all, so it works for any codec in either licence variant, including
streams the engine can't itself decode (spec 0013).
The mechanism: name the input streams to copy in Map with unbracketed specifiers
("0:v", "0:a:0", "0:0": an input index, a media type, an optional per-type index), and set
the codec to the CodecCopy sentinel. Bracketed labels ("[vout]") still mean graph pads
that get encoded, so copy and transcode mix freely in one job.
Change the container, keep the bytes¶
In the job above the video and audio are copied through byte-for-byte; only the wrapper changes. Any bitstream filter the target container requires (for example H.264's NAL framing differs between mp4 and MPEG-TS) is auto-inserted by the muxer, so you don't have to reason about it for the common case.
Copy one stream, re-encode another¶
Because an unbracketed Map entry means copy and a bracketed one means a graph pad, a single
output can do both: here the video is remuxed untouched while the audio is normalised and
re-encoded:
cmd := afmpeg.Command{
Inputs: []afmpeg.Input{{Path: "in.mp4"}},
FilterComplex: "[0:a]loudnorm[aout]",
Outputs: []afmpeg.Output{{
Path: "out.mp4",
Map: []string{"0:v", "[aout]"},
VideoCodec: afmpeg.CodecCopy, // copied
AudioCodec: "aac", // encoded from the [aout] pad
}},
}
Join like-codec segments (concat)¶
WithConcatInput presents a playlist of like-codec files as one continuous input via the
concat demuxer, a stream-copy join distinct from the concat filter (which decodes and
re-encodes):
cmd := afmpeg.NewCommand(
afmpeg.WithConcatInput("part0.ts", "part1.ts", "part2.ts"),
afmpeg.WithOutput("joined.ts",
afmpeg.Map("0:v"), afmpeg.Map("0:a"),
afmpeg.VideoCodec(afmpeg.CodecCopy), afmpeg.AudioCodec(afmpeg.CodecCopy)),
)
The segments must share codec and parameters. That's the demuxer's requirement, and a mismatch surfaces as a typed error. MPEG-TS is the natural concat-copy source (its packets carry continuous timestamps); joining mp4 segments with audio can hit mp4's priming-sample timestamp discontinuity at a boundary, so concat-copy is happiest on stream-friendly containers.
Override or disable the bitstream filter¶
The muxer's auto-insertion is right for the common case. When you need to force a specific
filter (or force none), set it per copied stream, keyed by the Map entry:
afmpeg.WithOutput("out.ts",
afmpeg.Map("0:v"), afmpeg.VideoCodec(afmpeg.CodecCopy),
afmpeg.BitstreamFilter("0:v", "h264_mp4toannexb"), // explicit; "none" disables
)
Notes¶
- Copy can only cut on keyframes. Trimming a copied stream to an exact frame needs a decode
the copy path skips; for a frame-accurate cut re-encode with
SeekAccurateTo(extract a clip, spec 0014). - Container reach. Copy lands on mp4/mkv/webm in the lean profile; MPEG-TS, HLS/DASH segmenting, and fragmented MP4 are available in the intermediate profile (spec 0015); see package for streaming.
- Licensing. Copy touches no codec library, so it stays in the LGPL default and even reaches codecs the engine doesn't ship a decoder or encoder for.