From 70eb13fe4f175cc3243ec5e69e2b8b129cdfa414 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 2 Apr 2026 14:35:31 -0500 Subject: [PATCH 1/3] Auditing mpeg4 box types --- src/mpeg4/mpeg4BoxType.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/mpeg4/mpeg4BoxType.ts b/src/mpeg4/mpeg4BoxType.ts index 6855f42c..a52ff18f 100644 --- a/src/mpeg4/mpeg4BoxType.ts +++ b/src/mpeg4/mpeg4BoxType.ts @@ -1,8 +1,11 @@ import {ByteVector, StringType} from "../byteVector"; /** - * Provides references to different box types used by the library. This class is used to severely reduce the number - * of times these types are created in {@link AppleTag,} greatly improving the speed at which warm files are read. + * Provides references to different box types used by the library. This class is used to severely + * reduce the number of times these types are created in {@link AppleTag,} greatly improving the + * speed at which warm files are read. + * + * These box types were cross-referenced with FFMPEG source and Exiftool database. */ export default class Mpeg4BoxType { /** QuickTime album artist box */ @@ -13,8 +16,11 @@ export default class Mpeg4BoxType { public static readonly ART = this.getType("©ART"); /** QuickTime comment box */ public static readonly CMT = this.getType("©cmt"); - /** QuickTime conductor box? @TODO: Verify this works should not be ©con */ - public static readonly COND = this.getType("cond"); + /** + * QuickTime conductor box + * @remarks "cond" in .NET source, but Exiftool says should be ©con. + */ + public static readonly COND = this.getType("©con"); /** QuickTime cover art box */ public static readonly COVR = this.getType("covr"); /** ISO 64-bit chunk offset box */ @@ -91,19 +97,25 @@ export default class Mpeg4BoxType { public static readonly STCO = this.getType("stco"); /** ISO sample description box */ public static readonly STSD = this.getType("stsd"); - /** Subtitle box? @TODO: There's no record of this one */ - public static readonly SUBT = this.getType("Subt"); + /** + * QuickTime subtitle box + * @remarks "Subt" in .NET source, but this appears to be for subtitle tracks, not metadata. + */ + public static readonly SUBT = this.getType("©st3"); /** Alias text box? @TODO: There's no record of this one */ public static readonly TEXT = this.getType("text"); /** QuickTime BPM box */ public static readonly TMPO = this.getType("tmpo"); /** ISO track container box */ public static readonly TRAK = this.getType("trak"); - /** QuickTime track number box */ + /** QuickTime track number box @TODO: What about ©TRK as per FFMPEG source? */ public static readonly TRKN = this.getType("trkn"); /** ISO User data box */ public static readonly UDTA = this.getType("udta"); - /** Alias URL box? @TODO: There's no record of this one */ + /** + * Alias URL box? + * @remarks Specified in FFMPEG source but no in Exiftool. + */ public static readonly URL = this.getType("©url"); /** ISO user extension box */ public static readonly UUID = this.getType("uuid"); From 6f2d497779896cf3a7f51458f682af612bc2f527 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 2 Apr 2026 23:49:07 -0500 Subject: [PATCH 2/3] An letter --- src/mpeg4/mpeg4BoxType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mpeg4/mpeg4BoxType.ts b/src/mpeg4/mpeg4BoxType.ts index a52ff18f..1d04d94e 100644 --- a/src/mpeg4/mpeg4BoxType.ts +++ b/src/mpeg4/mpeg4BoxType.ts @@ -114,7 +114,7 @@ export default class Mpeg4BoxType { public static readonly UDTA = this.getType("udta"); /** * Alias URL box? - * @remarks Specified in FFMPEG source but no in Exiftool. + * @remarks Specified in FFMPEG source but not in Exiftool. */ public static readonly URL = this.getType("©url"); /** ISO user extension box */ From 929067cc833d4f2da90300899b1fbf7fb2de70a3 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Sat, 4 Apr 2026 14:48:52 -0500 Subject: [PATCH 3/3] Backwards compat and more tests --- src/mpeg4/appleTag.ts | 14 ++++-- src/mpeg4/mpeg4BoxType.ts | 22 +++++++--- test-unit/mpeg4/appleTagTests.ts | 75 ++++++++++++++++++-------------- 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/mpeg4/appleTag.ts b/src/mpeg4/appleTag.ts index bdca9a63..dc36cfca 100644 --- a/src/mpeg4/appleTag.ts +++ b/src/mpeg4/appleTag.ts @@ -55,9 +55,12 @@ export default class AppleTag extends Tag { public set title(v: string) { this.setQuickTimeString(Mpeg4BoxType.NAM, v); } /** @inheritDoc */ - public get subtitle(): string { return this.getFirstQuickTimeString(Mpeg4BoxType.SUBT); } + public get subtitle(): string { + return this.getFirstQuickTimeString(Mpeg4BoxType.SUBT) // @TODO: Backwards compat for pre-6.0.2 releases + || this.getFirstQuickTimeString(Mpeg4BoxType.ST3); + } /** @inheritDoc */ - public set subtitle(v: string) { this.setQuickTimeString(Mpeg4BoxType.SUBT, v); } + public set subtitle(v: string) { this.setQuickTimeString(Mpeg4BoxType.ST3, v); } /** @inheritDoc */ public get description(): string { return this.getFirstQuickTimeString(Mpeg4BoxType.DESC); } @@ -209,9 +212,12 @@ export default class AppleTag extends Tag { } /** @inheritDoc */ - public get conductor(): string { return this.getFirstQuickTimeString(Mpeg4BoxType.COND); } + public get conductor(): string { + return this.getFirstQuickTimeString(Mpeg4BoxType.COND) // @TODO: Backwards compat for pre-6.0.2 releases + || this.getFirstQuickTimeString(Mpeg4BoxType.CON); + } /** @inheritDoc */ - public set conductor(v: string) { this.setQuickTimeString(Mpeg4BoxType.COND, v); } + public set conductor(v: string) { this.setQuickTimeString(Mpeg4BoxType.CON, v); } /** @inheritDoc */ public get copyright(): string { return this.getFirstQuickTimeString(Mpeg4BoxType.CPRT); } diff --git a/src/mpeg4/mpeg4BoxType.ts b/src/mpeg4/mpeg4BoxType.ts index 1d04d94e..207d78d1 100644 --- a/src/mpeg4/mpeg4BoxType.ts +++ b/src/mpeg4/mpeg4BoxType.ts @@ -17,10 +17,15 @@ export default class Mpeg4BoxType { /** QuickTime comment box */ public static readonly CMT = this.getType("©cmt"); /** - * QuickTime conductor box - * @remarks "cond" in .NET source, but Exiftool says should be ©con. + * QuickTime conductor box. This is listed in the FFMPEG source and Exiftool. */ - public static readonly COND = this.getType("©con"); + public static readonly CON = this.getType("©con"); + /** + * Conductor box from original .NET source. This is not listed anywhere in the Exiftool or + * FFMPEG docs. + * @TODO: Remove this when backwards compat time has ended. + */ + public static readonly COND = this.getType("cond"); /** QuickTime cover art box */ public static readonly COVR = this.getType("covr"); /** ISO 64-bit chunk offset box */ @@ -98,10 +103,15 @@ export default class Mpeg4BoxType { /** ISO sample description box */ public static readonly STSD = this.getType("stsd"); /** - * QuickTime subtitle box - * @remarks "Subt" in .NET source, but this appears to be for subtitle tracks, not metadata. + * QuickTime subtitle box. This is listed in the FFMPEG source and Exiftool. + */ + public static readonly ST3 = this.getType("©st3") + /** + * Subtitle box from original .NET source. This is not listed anywhere in the Exiftool or + * FFMPEG docs. + * @TODO: Remove this when backwards compat time has ended. */ - public static readonly SUBT = this.getType("©st3"); + public static readonly SUBT = this.getType("Subt"); /** Alias text box? @TODO: There's no record of this one */ public static readonly TEXT = this.getType("text"); /** QuickTime BPM box */ diff --git a/test-unit/mpeg4/appleTagTests.ts b/test-unit/mpeg4/appleTagTests.ts index 16699f61..11b35ceb 100644 --- a/test-unit/mpeg4/appleTagTests.ts +++ b/test-unit/mpeg4/appleTagTests.ts @@ -81,7 +81,17 @@ import {TagTypes} from "../../src/tag"; @test public subtitle() { - this.testQuickTimeString((t, v) => t.subtitle = v, (t) => t.subtitle, Mpeg4BoxType.SUBT); + this.testQuickTimeString((t, v) => t.subtitle = v, (t) => t.subtitle, Mpeg4BoxType.ST3); + } + + @test + public subtitle_existingSubt() { + // Arrange + const box1 = this.getQuickTimeBox(Mpeg4BoxType.SUBT, "foobarbaz"); + const testTag = this.getEmptyTag([box1.box]); + + // Act / Assert + assert.strictEqual(testTag.tag.subtitle, "foobarbaz"); } @test @@ -169,7 +179,7 @@ import {TagTypes} from "../../src/tag"; ByteVector.fromShort(0), AppleDataBoxFlagType.ContainsData ); - const box4 = this.getQuickTimeBox(Mpeg4BoxType.GNRE, ByteVector.fromString("foo", StringType.UTF8)); + const box4 = this.getQuickTimeBox(Mpeg4BoxType.GNRE, "foo"); const tag = this.getEmptyTag([box3.box, box4.box, box1.box, box2.box]); // Act @@ -210,7 +220,7 @@ import {TagTypes} from "../../src/tag"; // Arrange const box1 = this.getQuickTimeBox(Mpeg4BoxType.GNRE, ByteVector.fromShort(1)); const value2 = "foo; bar; baz"; - const box2 = this.getQuickTimeBox(Mpeg4BoxType.GEN, ByteVector.fromString(value2, StringType.UTF8)); + const box2 = this.getQuickTimeBox(Mpeg4BoxType.GEN, value2); const tag = this.getEmptyTag([box1.box, box2.box]); // Act @@ -265,9 +275,9 @@ import {TagTypes} from "../../src/tag"; @test public year_multipleBoxes() { // Arrange - const box1 = this.getQuickTimeBox(Mpeg4BoxType.DAY, ByteVector.fromString("asdf", StringType.UTF8)); - const box2 = this.getQuickTimeBox(Mpeg4BoxType.DAY, ByteVector.fromString("123456", StringType.UTF8)); - const box3 = this.getQuickTimeBox(Mpeg4BoxType.DAY, ByteVector.fromString("234", StringType.UTF8)); + const box1 = this.getQuickTimeBox(Mpeg4BoxType.DAY, "asdf"); + const box2 = this.getQuickTimeBox(Mpeg4BoxType.DAY, "123456"); + const box3 = this.getQuickTimeBox(Mpeg4BoxType.DAY, "234"); const testTag = this.getEmptyTag([box1.box, box2.box, box3.box]); // Act / Assert @@ -277,7 +287,7 @@ import {TagTypes} from "../../src/tag"; @test public year_setMultipleBoxes() { // Arrange - const box1 = this.getQuickTimeBox(Mpeg4BoxType.DAY, ByteVector.fromString("1234", StringType.UTF8)); + const box1 = this.getQuickTimeBox(Mpeg4BoxType.DAY, "1234"); const testTag = this.getEmptyTag([box1.box, box1.box, box1.box]); // Act / Assert @@ -289,7 +299,7 @@ import {TagTypes} from "../../src/tag"; @test public year_setZero_clearsMultiple() { // Arrange - const box1 = this.getQuickTimeBox(Mpeg4BoxType.DAY, ByteVector.fromString("1234", StringType.UTF8)); + const box1 = this.getQuickTimeBox(Mpeg4BoxType.DAY, "1234"); const testTag = this.getEmptyTag([box1.box, box1.box, box1.box]); // Act / Assert @@ -429,7 +439,17 @@ import {TagTypes} from "../../src/tag"; @test public conductor() { - this.testQuickTimeString((t, v) => t.conductor = v, (t) => t.conductor, Mpeg4BoxType.COND); + this.testQuickTimeString((t, v) => t.conductor = v, (t) => t.conductor, Mpeg4BoxType.CON); + } + + @test + public conductor_existingCondBox() { + // Arrange + const box1 = this.getQuickTimeBox(Mpeg4BoxType.COND, "foobarbaz"); + const testTag = this.getEmptyTag([box1.box]); + + // Act / Assert + assert.strictEqual(testTag.tag.conductor, "foobarbaz"); } @test @@ -464,18 +484,9 @@ import {TagTypes} from "../../src/tag"; @test public dateTagged_multipleBoxes() { // Arrange - const box1 = this.getQuickTimeBox( - Mpeg4BoxType.DTAG, - ByteVector.fromString("asdf", StringType.UTF8) - ); - const box2 = this.getQuickTimeBox( - Mpeg4BoxType.DTAG, - ByteVector.fromString("2023-10-21T10:46:00", StringType.UTF8) - ); - const box3 = this.getQuickTimeBox( - Mpeg4BoxType.DTAG, - ByteVector.fromString("2023-10-21", StringType.UTF8) - ); + const box1 = this.getQuickTimeBox(Mpeg4BoxType.DTAG, "asdf"); + const box2 = this.getQuickTimeBox(Mpeg4BoxType.DTAG, "2023-10-21T10:46:00"); + const box3 = this.getQuickTimeBox(Mpeg4BoxType.DTAG, "2023-10-21"); const testTag = this.getEmptyTag([box1.box, box2.box, box3.box]); // Act / Assert @@ -485,10 +496,7 @@ import {TagTypes} from "../../src/tag"; @test public dateTagged_setMultipleBoxes() { // Arrange - const box1 = this.getQuickTimeBox( - Mpeg4BoxType.DTAG, - ByteVector.fromString("1900-10-21T10:56:00", StringType.UTF8) - ); + const box1 = this.getQuickTimeBox(Mpeg4BoxType.DTAG, "1900-10-21T10:56:00"); const testTag = this.getEmptyTag([box1.box, box1.box, box1.box]); const testValue = new Date("2023-10-21 10:59:00"); @@ -504,10 +512,7 @@ import {TagTypes} from "../../src/tag"; @test public dateTagged_setZero_clearsMultiple() { // Arrange - const box1 = this.getQuickTimeBox( - Mpeg4BoxType.DTAG, - ByteVector.fromString("2021-10-21T11:04:00", StringType.UTF8) - ); + const box1 = this.getQuickTimeBox(Mpeg4BoxType.DTAG, "2021-10-21T11:04:00"); const testTag = this.getEmptyTag([box1.box, box1.box, box1.box]); // Act / Assert @@ -1187,9 +1192,9 @@ import {TagTypes} from "../../src/tag"; // TEST CASE 3: Multiple boxes return all valid instances ---------- // Valid box type and flags - const box1 = this.getQuickTimeBox(boxType, ByteVector.fromString("foo", StringType.UTF8)); + const box1 = this.getQuickTimeBox(boxType, "foo"); // Valid box type and flags - const box2 = this.getQuickTimeBox(boxType, ByteVector.fromString("bar", StringType.UTF8)); + const box2 = this.getQuickTimeBox(boxType, "bar"); // Valid box type, invalid flags const box3 = this.getQuickTimeBox( boxType, @@ -1202,7 +1207,7 @@ import {TagTypes} from "../../src/tag"; box4.addChild(box2.dataBox); box4.addChild(box3.dataBox); // Multiple values in single data box - const box5 = this.getQuickTimeBox(boxType, ByteVector.fromString("fux; bux; quxx", StringType.UTF8)); + const box5 = this.getQuickTimeBox(boxType, "fux; bux; quxx"); const testTag2 = this.getEmptyTag([box1.box, box2.box, box3.box, box4, box5.box]); @@ -1454,10 +1459,14 @@ import {TagTypes} from "../../src/tag"; private getQuickTimeBox( boxType: ByteVector, - value: ByteVector, + value: ByteVector|string, flags: AppleDataBoxFlagType = AppleDataBoxFlagType.ContainsText ): {box: AppleAnnotationBox, dataBox: AppleDataBox} { + value = typeof(value) === "string" + ? ByteVector.fromString(value, StringType.UTF8) + : value; const dataBox = AppleDataBox.fromDataAndFlags(value, flags); + const box = AppleAnnotationBox.fromType(boxType); box.addChild(dataBox);