Lesson 8 of 10

Merge and Combine Inputs

Send multiple inputs in one job to replace an audio track, or combine separate video and audio files.

Follow along with a live API key. 100 free minutes, no credit card.

Get free API key

One job, many inputs

Every job so far has taken a single input. But the inputs array accepts up to ten items, and FFmpeg Micro feeds them all into the same command. That's how you replace an audio track, combine a separate video and audio file, or mix a soundtrack over silent footage, all in one request, without stitching files together yourself first.

You already have createTranscode, waitForJob, and getDownloadUrl from Lesson 3. In this lesson you just call createTranscode with more than one input.

Attach an audio track to a video

Pass the video first and the audio second. The API maps the first video stream and the first audio stream into the output by default, so order is what tells it which input is which. Copy the video through untouched and encode the new audio track:

const job = await createTranscode({
  inputs: [
    { url: videoUrl },
    { url: audioUrl },
  ],
  outputFormat: 'mp4',
  options: [
    { option: '-c:v', argument: 'copy' },
    { option: '-c:a', argument: 'aac' },
  ],
})
  • -c:v copy avoids re-encoding the video. It's fast and lossless. The argument value copy is allowed.
  • -c:a aac encodes the new audio track into the MP4.
  • The first video stream and first audio stream are mapped by default, so you don't have to.

A reusable helper

Wrap it up so you can merge any video and audio pair. Adding -shortest is a safe default: it ends the output when the shorter input runs out, which keeps mismatched lengths from producing trailing silence or a truncated track. Then compose the lifecycle you already know. Submit, poll, download:

// merge.mjs
async function mergeAudioVideo(videoUrl, audioUrl) {
  const job = await createTranscode({
    inputs: [
      { url: videoUrl },
      { url: audioUrl },
    ],
    outputFormat: 'mp4',
    options: [
      { option: '-c:v', argument: 'copy' },
      { option: '-c:a', argument: 'aac' },
      { option: '-shortest' },
    ],
  })

  const done = await waitForJob(job.id)
  return getDownloadUrl(done.id)
}

const url = await mergeAudioVideo(
  'https://example.com/silent-clip.mp4',
  'https://example.com/soundtrack.mp3',
)
console.log('Merged file:', url)

Run it the same way as every script in this course:

node --env-file=.env merge.mjs

Choosing streams explicitly

The defaults are right most of the time, but when a source has multiple streams, or you want the audio from input 1 over the video from input 0, reach for -map. It takes an argument like 0:v:0 (first video stream of the first input) or 1:a:0 (first audio stream of the second input) and puts exactly that stream into the output:

options: [
  { option: '-map', argument: '0:v:0' },
  { option: '-map', argument: '1:a:0' },
  { option: '-c:v', argument: 'copy' },
  { option: '-c:a', argument: 'aac' },
]

What could go wrong: If the audio and video lengths differ, you'll get trailing silence when the audio is shorter, or a truncated audio track when the video is shorter. Add -shortest or trim the inputs to match. If a source has multiple streams and the defaults pick the wrong one, use -map to select the stream explicitly. And remember inputs is capped at ten items.

Key takeaways

  • Pass multiple items in inputs to combine sources in one job, up to ten.
  • -c:v copy skips re-encoding the video: fast and lossless.
  • -c:a aac encodes the new audio track into the output.
  • -shortest ends the output at the shorter input, handling length mismatches.
  • -map gives you explicit control over which input and stream lands in the output.

Next up: run many jobs at once with batch processing at scale, a concurrency limiter, and per-item error handling.

Build this into your app. No FFmpeg install

One call kicks off a job. No local FFmpeg, no servers, no worker queue to run.

const res = await fetch('https://api.ffmpeg-micro.com/v1/transcodes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.FFMPEG_MICRO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    inputs: [{ url: 'gs://your-bucket/input.mp4' }],
    outputFormat: 'mp4',
    preset: { quality: 'medium', resolution: '1080p' },
  }),
})
const job = await res.json() // { id, status: 'pending' }