Skip to content

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.