Skip to content

Commit

Permalink
M4A / MP4 : Only generate dynamic pic when it's needed + unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Zeugma440 committed Jan 25, 2025
1 parent 870fdfc commit fc74b20
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 15 deletions.
170 changes: 170 additions & 0 deletions ATL.unit-test/IO/MetaData/MP4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,176 @@ public void TagIO_RW_MP4_Chapters_QT_Pic_Warnings()
if (Settings.DeleteAfterSuccess) File.Delete(testFileLocation);
}

// Test behaviour when creating empty chapter pics
// Cases
// A: No chapter track should be created if there's no chapter
// B: No chapter picture track should be created if none of the chapters have an attached picture
// C: No empty chapter picture should be generated if all chapters that contain a picture are contiguous (=no gap > 1ms)
// D: An empty chapter picture should be generated for any chapter without pictures that's included between chapters that have one
[TestMethod]
public void TagIO_RW_MP4_Chapters_QT_Pic_DynamicPics()
{
new ConsoleLogger();
ArrayLogger log = new ArrayLogger();

// Source : file without 'chpl' atom
string testFileLocation = TestUtils.CopyAsTempTestFile("MP4/empty.m4a");
AudioDataManager theFile = new AudioDataManager(AudioDataIOFactory.GetInstance().GetFromPath(testFileLocation));

Assert.IsTrue(theFile.ReadFromFile());

Assert.IsNotNull(theFile.getMeta(tagType));
Assert.IsFalse(theFile.getMeta(tagType).Exists);

// Case A
TagData theTag = new TagData();
Assert.IsTrue(theFile.UpdateTagInFileAsync(theTag, MetaDataIOFactory.TagType.NATIVE).GetAwaiter().GetResult());
using (FileStream s = new FileStream(testFileLocation, FileMode.Open, FileAccess.Read))
{
Assert.IsFalse(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter titles")));
}

// Case B
theTag = new TagData();

theTag.Chapters = new List<ChapterInfo>();

ChapterInfo ch = new ChapterInfo();
ch.StartTime = 111;
ch.Title = "aaa";

theTag.Chapters.Add(ch);

Assert.IsTrue(theFile.UpdateTagInFileAsync(theTag, MetaDataIOFactory.TagType.NATIVE).GetAwaiter().GetResult());
using (FileStream s = new FileStream(testFileLocation, FileMode.Open, FileAccess.Read))
{
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter titles")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsFalse(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter pictures")));
}


// Case C
var dynamicPicSegment = new byte[] { 0xff, 0xdb, 0x00, 0x43, 0x00, 0x01 };
theTag = new TagData();

theTag.Chapters = new List<ChapterInfo>();

ch = new ChapterInfo();
ch.StartTime = 111;
ch.Title = "aaa";
ch.Picture = fromBinaryData(File.ReadAllBytes(TestUtils.GetResourceLocationRoot() + "_Images/pic1.jpeg"));
ch.Picture.ComputePicHash();

theTag.Chapters.Add(ch);

ch = new ChapterInfo();
ch.StartTime = 222;
ch.Title = "bbb";
ch.Picture = fromBinaryData(File.ReadAllBytes(TestUtils.GetResourceLocationRoot() + "_Images/pic2.jpeg"));
ch.Picture.ComputePicHash();

theTag.Chapters.Add(ch);

// Two chapters with pictures => Shouldn't create a dynamic pic
Assert.IsTrue(theFile.UpdateTagInFileAsync(theTag, MetaDataIOFactory.TagType.NATIVE).GetAwaiter().GetResult());
using (FileStream s = new FileStream(testFileLocation, FileMode.Open, FileAccess.Read))
{
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter titles")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter pictures")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsFalse(StreamUtils.FindSequence(s, dynamicPicSegment));
}

ch = new ChapterInfo();
ch.StartTime = 333;
ch.Title = "ccc";
theTag.Chapters.Add(ch);

// Three chapters; the first two with pictures => Shouldn't create a dynamic pic
Assert.IsTrue(theFile.UpdateTagInFileAsync(theTag, MetaDataIOFactory.TagType.NATIVE).GetAwaiter().GetResult());
using (FileStream s = new FileStream(testFileLocation, FileMode.Open, FileAccess.Read))
{
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter titles")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter pictures")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsFalse(StreamUtils.FindSequence(s, dynamicPicSegment));
}

// Case D
theTag = new TagData();

theTag.Chapters = new List<ChapterInfo>();

ch = new ChapterInfo();
ch.StartTime = 111;
ch.Title = "aaa";

theTag.Chapters.Add(ch);

ch = new ChapterInfo();
ch.StartTime = 222;
ch.Title = "bbb";
ch.Picture = fromBinaryData(File.ReadAllBytes(TestUtils.GetResourceLocationRoot() + "_Images/pic2.jpeg"));
ch.Picture.ComputePicHash();

theTag.Chapters.Add(ch);

// Two chapters; the first without picture, the 2nd with a picture => Should create a dynamic pic
Assert.IsTrue(theFile.UpdateTagInFileAsync(theTag, MetaDataIOFactory.TagType.NATIVE).GetAwaiter().GetResult());
using (FileStream s = new FileStream(testFileLocation, FileMode.Open, FileAccess.Read))
{
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter titles")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter pictures")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsTrue(StreamUtils.FindSequence(s, dynamicPicSegment));
}


theTag = new TagData();

theTag.Chapters = new List<ChapterInfo>();

ch = new ChapterInfo();
ch.StartTime = 111;
ch.Title = "aaa";
ch.Picture = fromBinaryData(File.ReadAllBytes(TestUtils.GetResourceLocationRoot() + "_Images/pic1.jpeg"));
ch.Picture.ComputePicHash();

theTag.Chapters.Add(ch);

ch = new ChapterInfo();
ch.StartTime = 222;
ch.Title = "bbb";

theTag.Chapters.Add(ch);

ch = new ChapterInfo();
ch.StartTime = 333;
ch.Title = "ccc";
ch.Picture = fromBinaryData(File.ReadAllBytes(TestUtils.GetResourceLocationRoot() + "_Images/pic2.jpeg"));
ch.Picture.ComputePicHash();

theTag.Chapters.Add(ch);

// Three chapters; 1 and 3 with a picture, 2 without => Should create a dynamic pic
Assert.IsTrue(theFile.UpdateTagInFileAsync(theTag, MetaDataIOFactory.TagType.NATIVE).GetAwaiter().GetResult());
using (FileStream s = new FileStream(testFileLocation, FileMode.Open, FileAccess.Read))
{
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter titles")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsTrue(StreamUtils.FindSequence(s, Utils.Latin1Encoding.GetBytes("Chapter pictures")));
s.Seek(0, SeekOrigin.Begin);
Assert.IsTrue(StreamUtils.FindSequence(s, dynamicPicSegment));
}

// Get rid of the working copy
if (Settings.DeleteAfterSuccess) File.Delete(testFileLocation);
}

[TestMethod]
public void TagIO_RW_MP4_Chapters_CapNero()
{
Expand Down
34 changes: 20 additions & 14 deletions ATL/AudioData/IO/MP4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2293,11 +2293,10 @@ private static int writeQTChaptersData(BinaryWriter w, ICollection<ChapterInfo>
w.Write(StreamUtils.EncodeBEInt32(256));
}

bool anyChapterPic = anyWithPicture(chapters);
foreach (var chapter in chapters)
foreach (ChapterInfo chapter in chapters)
{
if (chapter.Picture != null) w.Write(chapter.Picture.PictureData);
else if (anyChapterPic) w.Write(Properties.Resources._1px_black);
else if (shouldCreateDynamicPicFor(chapters, chapter)) w.Write(Properties.Resources._1px_black);
}

return 1;
Expand All @@ -2318,9 +2317,10 @@ private int writeQTChaptersTrack(BinaryWriter w, int trackNum, IList<ChapterInfo
{
foreach (ChapterInfo chapter in workingChapters)
{
var canGoDynamic = shouldCreateDynamicPicFor(chapters, chapter);
byte[] pictureData = chapter.Picture != null
? chapter.Picture.PictureData
: Properties.Resources._1px_black;
: canGoDynamic ? Properties.Resources._1px_black : Array.Empty<byte>();
ImageProperties props = ImageUtils.GetImageProperties(pictureData);
maxWidth = (short)Math.Min(Math.Max(props.Width, maxWidth), short.MaxValue);
maxHeight = (short)Math.Min(Math.Max(props.Height, maxHeight), short.MaxValue);
Expand Down Expand Up @@ -2597,20 +2597,13 @@ private int writeQTChaptersTrack(BinaryWriter w, int trackNum, IList<ChapterInfo
}
if (!isText)
{
bool anyChapterPic = anyWithPicture(chapters);
foreach (ChapterInfo chapter in workingChapters)
{
var canGoDynamic = shouldCreateDynamicPicFor(chapters, chapter);
byte[] pictureData = chapter.Picture != null
? chapter.Picture.PictureData
: Properties.Resources._1px_black;
if (anyChapterPic)
{
w.Write(StreamUtils.EncodeBEUInt32((uint)pictureData.Length));
}
else
{
w.Write(0);
}
: canGoDynamic ? Properties.Resources._1px_black : Array.Empty<byte>();
w.Write(StreamUtils.EncodeBEUInt32((uint)pictureData.Length));
}
}
finalFramePos = w.BaseStream.Position;
Expand Down Expand Up @@ -2764,6 +2757,19 @@ private static bool anyWithPicture(ICollection<ChapterInfo> chapters)
return chapters.Any(ch => ch.Picture != null && ch.Picture.PictureData.Length > 0);
}

private static bool shouldCreateDynamicPicFor(ICollection<ChapterInfo> chapters, ChapterInfo chapter)
{
if (chapter.Picture != null && chapter.Picture.PictureData.Length > 0) return false;
var isFirst = !chapters.Any(ch => ch.StartTime < chapter.StartTime);
if (!isFirst)
{
var chapterBefore = chapters.FirstOrDefault(ch => ch.StartTime < chapter.StartTime && ch.Picture != null && ch.Picture.PictureData.Length > 0);
if (null == chapterBefore) return false;
}
var chapterAfter = chapters.FirstOrDefault(ch => ch.StartTime > chapter.StartTime && ch.Picture != null && ch.Picture.PictureData.Length > 0);
return chapterAfter != null;
}

// reduce the useful MDAT to a few Kbs (for dev purposes only)

#pragma warning disable S125 // Sections of code should not be commented out
Expand Down
2 changes: 1 addition & 1 deletion ATL/Utils/ImageUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public static ImageProperties GetImageProperties(byte[] imageData, ImageFormat f

if (ImageFormat.Undefined.Equals(format) && imageData.Length > 11) format = GetImageFormatFromPictureHeader(imageData);

if (format.Equals(ImageFormat.Unsupported)) return props;
if (format.Equals(ImageFormat.Unsupported) || format.Equals(ImageFormat.Undefined)) return props;

props.NumColorsInPalette = 0;
props.Format = format;
Expand Down

0 comments on commit fc74b20

Please sign in to comment.