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.
dotnet new console -o EditTags && cd EditTags dotnet add package RecursiveAI.OneMedia --version 0.3.0-preview.1 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:
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}"); } } }
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: 1055 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
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; donesintel-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:
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; donef2def6fafaed6682de882044ace566e5 - f2def6fafaed6682de882044ace566e5 -
Saving: which call to use
The project’s 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 passoverwrite: true.Save()writes a temporary file and renames it over the original. The review’s verdict on “Saveis atomic” is “Holds with listed limits”: in 500 kill 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, “The folder is notfsynced after the rename”; 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 “SaveInPlacecan tear on a kill or power loss for ranges that take more than onewrite”, and in the kill test 3 of 300 runs left a torn file. Use it for files you can afford to lose or have a copy of; useSaveotherwise.
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 AVILIST INFO, W64, CAF, MPEG-TS, ASF Metadata/Library objects, or XMP outside ISO files. - Fields a system lacks are listed in
SystemWriteReport.NotRepresentableand not written (MP4’smdtahas no track number, for example). - Dates. Setting
Datewrites one field per system; other date tags in the file stay and are named inreport.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.
To read every tag a file has, see Read MP4 metadata and MP3, FLAC and WAV tags.