---
title: "Edit MP4, MKV and MP3 tags in C#"
url: https://recursiveai.org/onemedia/cookbook/edit-video-tags/
description: "Edit MP4, MKV and MP3 tags in C#: set the title, artist and comment with OneMedia, save to a new file or replace the original, remove a tag from an MP3 in place, read them back with ffprobe, and know the limits of saving in place. Also FLAC, Ogg, WAV, AIFF and WMA."
version: "0.3.0-preview.2"
---

# Edit MP4, MKV and MP3 tags in C#

> **Privacy fix in 0.3.0-preview.2.** From the project’s changelog: “In 0.3.0-preview.1, removing frames from the end of an ID3v2 tag in an MP3 in place could leave the removed frames’ bytes in the file as padding, so a removed location, comment or serial number could survive. Fixed: an edit that removes trailing frames now overwrites them. **If you removed tags from MP3 files with 0.3.0-preview.1, save those files again with this version.** No other format was affected (checked on MP3, AIFF, WAV, FLAC, Opus, Ogg, MP4 and MKV).”
>
> What to do: update the packages to 0.3.0-preview.2, then save each affected file again with the removal repeated (set the field to `null`, then save). In our check, a save with no edit left the old value in place; repeating the removal took it out. [Remove a tag from an MP3 in place](#remove-a-tag-from-an-mp3-in-place) below shows the removal and a byte search for the removed value.

Set a field on `media.Tags` and save: OneMedia writes it into every tag system the file has (iTunes `ilst` in MP4, Matroska `Tags`, ID3v2 in MP3…) and copies the audio and video bytes unchanged. The same code edits MP4/MOV, MKV/WebM, MP3, FLAC, Ogg, WAV, AIFF and WMA files. The inputs are the Sintel trailer (CC BY 3.0, © copyright Blender Foundation | durian.blender.org) and copies of it made with FFmpeg as MKV (stream copy) and MP3; each carries FFmpeg’s tags from the trailer.

```shell
dotnet new console -o EditTags && cd EditTags
dotnet add package RecursiveAI.OneMedia --version 0.3.0-preview.2
curl -L -o sintel.mp4 https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4
x="-v error -i sintel.mp4 -metadata major_brand= -metadata minor_version= -metadata compatible_brands="
ffmpeg $x -c copy sintel.mkv
ffmpeg $x -vn -c:a libmp3lame -q:a 4 sintel.mp3
```

Put this in `Program.cs` and run `dotnet run`:

```csharp
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Tags;

foreach (string path in new[] { "sintel.mp4", "sintel.mkv", "sintel.mp3" })
{
    string tagged = Path.GetFileNameWithoutExtension(path) + "-tagged" + Path.GetExtension(path);

    // 1. Edit and save to a new file. The original is not touched.
    using (MediaFile media = Media.Open(path))
    {
        Console.WriteLine($"{path}: \"{media.Tags.Title}\" by {media.Tags.Artist}");
        media.Tags.Title = "Sintel (trailer)";
        media.Tags.Artist = "Blender Foundation";
        media.Tags.Comment = "CC BY 3.0";
        TagWriteReport report = media.SaveAs(tagged);
        foreach (SystemWriteReport system in report.Systems)
            Console.WriteLine($"  {system.System}: wrote {string.Join(", ", system.Written)}");
        foreach (string warning in report.Warnings) Console.WriteLine($"  warning: {warning}");
    }

    // 2. A small follow-up edit in place: it fits in the padding the first save left.
    using (MediaFile media = Media.Open(tagged))
    {
        media.Tags.Title = "Sintel, the trailer";
        TagWriteReport report = media.SaveInPlace();
        WriteReport? write = report.Write;
        Console.WriteLine($"  in place: {write?.BytesWritten} bytes written, {write?.PaddingAvailable} bytes of padding available");
    }

    // 3. The original has no room for a longer title, so an in-place save refuses and changes nothing.
    using (MediaFile media = Media.Open(path))
    {
        media.Tags.Title = "Sintel, a short film by the Blender Foundation (trailer)";
        try { media.SaveInPlace(); }
        catch (PaddingRequiredException e) { Console.WriteLine($"  original: {e.Message}"); }
    }
}
```

```text
sintel.mp4: "Sintel Trailer" by Durian Open Movie Team
  ilst: wrote Title, Artist, Comment
  in place: 307 bytes written, 1024 bytes of padding available
  original: The edit does not fit in the existing padding: 42 more byte(s) of padding are needed (0 available). Save to a new file instead.
sintel.mkv: "Sintel Trailer" by Durian Open Movie Team
  mkv: wrote Title, Artist, Comment
  mkv-info: wrote Title
  in place: 65 bytes written, 2048 bytes of padding available
  original: The edit does not fit in the existing padding: 112 more byte(s) of padding are needed (0 available). Save to a new file instead.
sintel.mp3: "Sintel Trailer" by Durian Open Movie Team
  id3v2: wrote Title, Artist, Comment
  in place: 165 bytes written, 1024 bytes of padding available
  original: The edit does not fit in the existing padding: 31 more byte(s) of padding are needed (10 available). Save to a new file instead.
```

`SaveAs` writes a new file; `Save()` replaces the original (both below). Each system gets the field under its own key (`©nam` in MP4, `TITLE` in Matroska, `TIT2` in ID3v2; Matroska’s Segment title too). A growing edit leaves 1,024 bytes of padding behind the tags, which is why the second, in-place edit fits; it writes only the bytes that differ. `PaddingRequiredException` tells you how many bytes were missing when it doesn’t.

## Read them back with ffprobe

```shell
for f in sintel-tagged.mp4 sintel-tagged.mkv sintel-tagged.mp3; do echo "$f"; ffprobe -v error -show_entries format_tags=title,artist,comment -of default=nw=1 $f; done
```

```text
sintel-tagged.mp4
TAG:title=Sintel, the trailer
TAG:artist=Blender Foundation
TAG:comment=CC BY 3.0
sintel-tagged.mkv
TAG:ARTIST=Blender Foundation
TAG:TITLE=Sintel, the trailer
TAG:COMMENT=CC BY 3.0
sintel-tagged.mp3
TAG:title=Sintel, the trailer
TAG:artist=Blender Foundation
TAG:comment=CC BY 3.0
```

The video and audio packets are untouched; ffprobe gives the same payload hashes for the original and the tagged MP4:

```shell
for f in sintel.mp4 sintel-tagged.mp4; do ffprobe -v error -show_entries packet=stream_index,pts,size,flags -show_data_hash MD5 -of csv $f | md5sum; done
```

```text
f2def6fafaed6682de882044ace566e5  -
f2def6fafaed6682de882044ace566e5  -
```

## Remove a tag from an MP3 in place

Set the field to `null` and save. This program takes the comment written above out of `sintel-tagged.mp3` in place, then searches every byte of the file for the removed text:

```csharp
using System.Text;
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Tags;

const string path = "sintel-tagged.mp3";
byte[] removed = Encoding.UTF8.GetBytes("CC BY 3.0");
Console.WriteLine($"before: \"CC BY 3.0\" found {Count(path, removed)} time(s)");

using (MediaFile media = Media.Open(path))
{
    media.Tags.Comment = null;
    TagWriteReport report = media.SaveInPlace();
    foreach (SystemWriteReport system in report.Systems)
        Console.WriteLine($"{system.System}: removed {string.Join(", ", system.Removed)}");
    Console.WriteLine($"in place: {report.Write?.BytesWritten} bytes written");
}

Console.WriteLine($"after: \"CC BY 3.0\" found {Count(path, removed)} time(s)");

// Counts every occurrence of the bytes anywhere in the file, padding included.
static int Count(string path, byte[] needle)
{
    ReadOnlySpan<byte> data = File.ReadAllBytes(path);
    int count = 0;
    for (int i; (i = data.IndexOf(needle)) >= 0; data = data[(i + needle.Length)..]) count++;
    return count;
}
```

```text
before: "CC BY 3.0" found 1 time(s)
id3v2: removed Comment
in place: 24 bytes written
after: "CC BY 3.0" found 0 time(s)
```

The comment is gone from the tag and from the padding behind it: the removed frame’s bytes are overwritten, not just left out of the tag’s frame list. ffprobe reads no comment back:

```shell
ffprobe -v error -show_entries format_tags=title,artist,comment -of default=nw=1 sintel-tagged.mp3
```

```text
TAG:title=Sintel, the trailer
TAG:artist=Blender Foundation
```

For comparison, the same steps on 0.3.0-preview.1 printed `in place: 0 bytes written` and `after: "CC BY 3.0" found 1 time(s)`, and ffprobe still read `TAG:comment=CC BY 3.0` from the file. That is the case the notice at the top is about.

## Saving: which call to use

The project’s [writer review](https://recursiveai.org/onemedia/docs/writer-review/) sets the terms: “Editing can be advertised for `Save` / `SaveAs`, with the limits below. `SaveInPlace` can be advertised as ‘padding-only, not atomic’.”

- **`SaveAs(path)`** writes a new file; it won’t overwrite an existing file unless you pass `overwrite: true`.
- **`Save()`** writes a temporary file and renames it over the original. The review’s verdict on “`Save` is atomic” is “Holds against a killed process, with listed limits”: in 500 `kill -9` rounds no half-written file was left. The limits are replace semantics (the result is a new file: new inode, the saving user’s owner and group; extended attributes, ACLs and hard links of the original are not kept), and a killed save leaves its temporary file behind (`.name.onemedia-<host>-<pid>-<random>.tmp`), which the next save of the same file removes. In the review’s words, “Power loss was not tested, and the folder is not `fsync`ed after the rename (the BCL cannot), so the rename may not be durable after one.” OneMedia makes no claim about a power loss during or just after a save.
- **`SaveInPlace()`** is “padding-only, not atomic”. It writes only inside existing padding and otherwise refuses with the file untouched, as above. It makes no claim about a crash or power loss: review finding 9 reads “`SaveInPlace` can tear on a kill or power loss for ranges that take more than one `write`”, and the review measured “3 torn of 300 with the two-pass writer”. Use it for files you can afford to lose or have a copy of; use `Save` otherwise.

## Limits

- **Formats.** Tag editing covers MP4/MOV/3GP (`ilst`, `udta`, `mdta`, XMP), Matroska/WebM, MP3 (ID3v2, ID3v1, APEv2), FLAC, Ogg Vorbis/Opus/FLAC, WAV/RF64 (`LIST INFO`, `bext`, iXML, ID3), AIFF and ASF/WMA/WMV. Not AVI `LIST INFO`, W64, CAF, MPEG-TS, ASF Metadata/Library objects, or XMP outside ISO files.
- **Fields a system lacks** are listed in `SystemWriteReport.NotRepresentable` and not written (MP4’s `mdta` has no track number, for example).
- **Dates.** Setting `Date` writes one field per system; other date tags in the file stay and are named in `report.Warnings`.
- **Pictures.** `media.Tags.SetPicture(bytes, "image/jpeg", PictureType.FrontCover, "")` adds cover art; FLAC picture blocks are written with width and height 0.
- **The open file is stale after a save.** Open the saved file again to read it. Per-system rules: [Tag writing](https://recursiveai.org/onemedia/docs/tag-writing/).

To read every tag a file has, see [Read MP4 metadata](https://recursiveai.org/onemedia/cookbook/read-mp4-metadata/) and [MP3, FLAC and WAV tags](https://recursiveai.org/onemedia/cookbook/get-audio-file-tags-duration/).
