fix(media): m4a/AAC audio files from non-Apple sources are misidentified as video/mp4 and excluded from audio workflows (#111177)

* fix(media): preserve audio hints for isom-brand m4a/AAC files misidentified as video/mp4

* fix(media): correct ambiguous M4A aliases

Co-authored-by: 潘晓波0668000512 <pan.xiaobo@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
YangManBOBO
2026-07-19 22:18:23 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent bdf71d7d98
commit e45ebac97a
2 changed files with 57 additions and 6 deletions
+51 -3
View File
@@ -26,6 +26,12 @@ async function makeOoxmlZip(opts: { mainMime: string; partPath: string }): Promi
return await zip.generateAsync({ type: "nodebuffer" });
}
// file-type classifies this generic ISO-BMFF brand as video/mp4 without track metadata.
const ISOM_BRAND_BUFFER = Buffer.from(
"0000001c6674797069736f6d0000000069736f6d0000000000000000",
"hex",
);
describe("mime detection", () => {
async function expectDetectedMime(params: {
input: Parameters<typeof detectMime>[0];
@@ -221,10 +227,51 @@ describe("mime detection", () => {
});
});
it("preserves audio metadata when an MP4 extension cannot identify track kind", async () => {
await expectDetectedMime({
input: { filePath: "voice.mp4", headerMime: "audio/mp4" },
it.each([
{
name: "audio/mp4 header",
filePath: "voice.mp4",
headerMime: "audio/mp4",
expected: "audio/mp4",
},
{
name: "audio/x-m4a header",
filePath: "voice.m4a",
headerMime: "audio/x-m4a",
expected: "audio/x-m4a",
},
{
name: "audio/m4a header",
filePath: "voice.m4a",
headerMime: "audio/m4a",
expected: "audio/m4a",
},
{
name: "m4a extension",
filePath: "voice.m4a",
headerMime: undefined,
expected: "audio/x-m4a",
},
{
name: "mp4 extension without an audio hint",
filePath: "clip.mp4",
headerMime: undefined,
expected: "video/mp4",
},
{
name: "audio/aac elementary-stream metadata",
filePath: "voice.aac",
headerMime: "audio/aac",
expected: "video/mp4",
},
] as const)("resolves ambiguous isom-brand bytes from $name", async (testCase) => {
await expectDetectedMime({
input: {
buffer: ISOM_BRAND_BUFFER,
filePath: testCase.filePath,
headerMime: testCase.headerMime,
},
expected: testCase.expected,
});
});
@@ -381,6 +428,7 @@ describe("extensionForMime", () => {
{ mime: "audio/x-wav", expected: ".wav" },
{ mime: "audio/webm", expected: ".webm" },
{ mime: "audio/x-m4a", expected: ".m4a" },
{ mime: "audio/m4a", expected: ".m4a" },
{ mime: "audio/mp4", expected: ".m4a" },
{ mime: "video/x-msvideo", expected: ".avi" },
{ mime: "video/mp4", expected: ".mp4" },
+6 -3
View File
@@ -28,6 +28,7 @@ const EXT_BY_MIME: Record<string, string> = {
"audio/opus": ".opus",
"audio/webm": ".webm",
"audio/x-m4a": ".m4a",
"audio/m4a": ".m4a",
"audio/mp4": ".m4a",
"audio/x-caf": ".caf",
"video/x-msvideo": ".avi",
@@ -88,6 +89,8 @@ const MIME_BY_EXT: Record<string, string> = {
const AMBIGUOUS_VIDEO_MIME_BY_AUDIO_MIME: Readonly<Record<string, string>> = {
"audio/mp4": "video/mp4",
"audio/x-m4a": "video/mp4",
"audio/m4a": "video/mp4",
"audio/webm": "video/webm",
};
@@ -243,9 +246,9 @@ export async function detectMime(opts: {
: (sniffed ?? extMime);
// file-type defaults these containers to video without parsing their tracks.
// Preserve a concrete audio hint only for those documented ambiguous results.
const audioContainerHint = mimeHints.find(
(mime) => AMBIGUOUS_VIDEO_MIME_BY_AUDIO_MIME[mime] === inferred,
);
const audioContainerHint =
mimeHints.find((mime) => AMBIGUOUS_VIDEO_MIME_BY_AUDIO_MIME[mime] === inferred) ??
(extMime && AMBIGUOUS_VIDEO_MIME_BY_AUDIO_MIME[extMime] === inferred ? extMime : undefined);
if (audioContainerHint) {
return audioContainerHint;
}