Ahosting Logo
Knowledge Base

How to Edit Metadata and Chapters with FFmpeg

Metadata travels whether you meant it to or notWhat files routinely carry· a title, often from the source· the encoding software name and version· creation dates· sometimes device or location informationWhat to do· strip metadata when publishing· set the fields you actually want· and add chapters, which players expose as navigationWhy it mattersThe default is to copy everything through, so a published file can describe your workflow andyour equipment to anyone who opens it.

Every video file carries a set of tags alongside the picture and sound. Most people never look at them, which is precisely why they are worth looking at before publishing anything.

See what is there

ffprobe -v error -show_format -show_streams input.mp4 | grep -i -A20 TAG

Typical contents: the encoding software and its version, the camera or phone model, a creation timestamp, and occasionally location coordinates or the full path of the project on someone's computer.

None of it is needed for playback. All of it is published with the file.

Strip everything, then add back what you want

ffmpeg -i input.mp4 -map_metadata -1 -c copy clean.mp4

-map_metadata -1 removes the lot. -c copy means no frame is touched, so this completes in about a second regardless of file length.

Then set only what belongs there:

ffmpeg -i clean.mp4 -metadata title="Product Tour" \
 -metadata comment="example.com" -c copy final.mp4

Strip-then-add is more reliable than trying to remove specific fields, because you do not have to know in advance what a particular camera or editor decided to write.

Common fields

title, artist, album, date, comment, description. Containers differ in what they keep: MKV accepts almost anything, MP4 is stricter, and a tag silently dropped is normal instead of an error.

Streams can be tagged individually, which is how you label multiple audio or subtitle tracks so a player can present them properly:

ffmpeg -i input.mkv -map 0 -c copy \
 -metadata:s:a:0 language=eng -metadata:s:a:0 title="English" \
 -metadata:s:a:1 language=tur -metadata:s:a:1 title="Türkçe" out.mkv

Language tags are the ones that matter in practice: without them a player shows "Track 1" and "Track 2" and the viewer guesses. Working with audio tracks explains the rest.

Chapters

Chapters let a viewer jump within a long video, and they are stored as metadata rather than as anything in the picture.

Export what the file has:

ffmpeg -i input.mp4 -f ffmetadata chapters.txt

The format is plain text:

;FFMETADATA1
[CHAPTER]
TIMEBASE=1/1000
START=0
END=125000
title=Introduction

[CHAPTER]
TIMEBASE=1/1000
START=125000
END=302000
title=Setting up

TIMEBASE=1/1000 means the numbers are milliseconds, so 125000 is two minutes and five seconds. Getting this line wrong is the usual reason chapters land in absurd places.

Write them in:

ffmpeg -i input.mp4 -i chapters.txt -map_metadata 1 -c copy out.mp4

Finding the boundaries by hand is the tedious part, and it can be automated, detecting scenes and silence produces a usable first draft of the timestamps.

Support is uneven

MKV handles chapters well. MP4 support is real but inconsistent across players, and browsers largely ignore embedded chapters entirely.

If chapters are for a website, the reliable route is to keep the list beside the video and have the player seek to a timestamp, rather than relying on the container. Embedded chapters are for files people download.

Make stripping part of the pipeline

Any site accepting video uploads is accepting whatever metadata those files carry, and republishing it.

Adding -map_metadata -1 to the processing step costs nothing and removes an entire category of accidental disclosure, device models, timestamps, home locations. That nobody involved intended to publish.

Handling video uploads from a web application goes over where in the pipeline it belongs.

Container support is not uniform

Metadata is written differently by each container, and a field that survives in one is silently dropped in another.

MKV accepts almost any tag you invent. MP4 has a defined set and discards the rest without complaint. WebM is stricter still.

ffmpeg -i in.mkv -map_metadata 0 -c copy out.mp4
ffprobe -v error -show_format out.mp4 | grep -i TAG

Comparing before and after is the only reliable way to know what survived. A tag that disappears during a container change is normal in place of an error, and it is why titles set on an MKV vanish when it becomes an MP4.

Where a field matters and the container will not hold it, keep it in your own database alongside the file rather than relying on the file to carry it.

Creation time is the field that misleads

Players and file browsers display a creation timestamp, and it comes from the container rather than from the filesystem.

A converted file therefore shows the date of the conversion, not of the original recording, which reorders a library sorted by date and is frequently reported as files having been altered.

ffmpeg -i in.mp4 -map_metadata 0 -c copy -metadata creation_time="2026-03-14T10:00:00" out.mp4

Preserve it deliberately when converting an archive, or set it from a known value. And note that stripping metadata removes it entirely, which is usually what you want when publishing and rarely what you want when archiving: the two purposes need different commands.

Reading chapters back reliably

Confirming that chapters were written is worth doing, because a malformed metadata file produces no error and no chapters.

ffprobe -v error -show_chapters -of default=noprint_wrappers=1 out.mp4 | head -20

If nothing is listed, the usual cause is the time base: a file declaring milliseconds while the numbers are in seconds produces chapters at absurd positions or none at all.

Check the first chapter's start and the last chapter's end against the file's duration. Those two numbers catch every scaling mistake in one glance. Using ffprobe deals with reading the duration.

Batch stripping without touching anything else

for f in *.mp4; do
 ffmpeg -nostdin -v error -i "$f" -map_metadata -1 -c copy "clean/$f" && echo "ok $f"
done

Because it is a copy operation, a directory of files processes in seconds rather than hours, and nothing about the picture or sound changes.

The -nostdin matters in a loop: without it FFmpeg consumes the shell's input and only the first file is processed; a failure that looks like the loop being broken. Building a batch pipeline deals with the rest of that pattern.

Metadata leaves the file even when you strip it

Removing the fields from one file does nothing about the copies that already exist.

ffprobe -v error -show_entries format_tags -of json input.mp4
ffprobe -v error -show_entries stream_tags -of json input.mp4 | head -20

Read both. Fields exist at the container level and again per stream, and a command that clears one leaves the other in place.

The stream level fields are the ones people miss, because the usual check shows the container and reports nothing. A file that appears clean can still carry an encoder name, a language tag or a handler string identifying the software and occasionally the device.

Location data is the field worth checking

Video from a phone frequently carries where it was recorded, and publishing it is a decision people make without knowing they made it.

ffprobe -v error -show_entries format_tags=location,location-eng,com.apple.quicktime.location.ISO6709 \
  -of default=nw=1 input.mp4

If anything is returned, the coordinates are in the file and travel with it to whoever downloads it.

Strip it deliberately rather than relying on a general clear, and confirm afterwards rather than assuming. This is the one field where the consequence of missing it is about a person rather than about tidiness, which makes it worth its own check in any pipeline handling material from customers.

Verify the result rather than trusting the command

A strip that ran without error can still have left fields behind, and comparing before and after takes one command.

for f in input.mp4 output.mp4; do
  printf '%-14s ' "$f"
  ffprobe -v error -show_entries format_tags -of json "$f" | tr -d '\n ' | head -c 120
  echo
done

The output for the cleaned file should contain no fields beyond what you added deliberately. Anything remaining means the strip applied to the container and not the streams, or the container format kept a field it defines itself.

Some formats always record something. Where that matters, changing container is the answer rather than more aggressive stripping, since a field the format requires cannot be removed and still produce a valid file. Remuxing and stream copy covers changing it without re-encoding.