fix(studio-server): decode authored audio attributes for loudness (#5289)

This commit is contained in:
Abdullah M. Nazif
2026-10-24 04:11:25 +00:00
committed by GitHub
parent bd0be1bdae
commit 0aa728b1aa
2 changed files with 43 additions and 1 deletions
@@ -95,6 +95,40 @@ describe("audioTags with video", () => {
});
});
describe("audioTags with HTML character references", () => {
it.each([
{ src: "Q&A.wav", expected: "Q&A.wav" },
{ src: "Q&A.wav", expected: "Q&A.wav" },
{ src: "Q&A.wav", expected: "Q&A.wav" },
{ src: "an's "reply".wav", expected: 'an\'s "reply".wav' },
{ src: "Q&A.wav", expected: "Q&A.wav" },
])("decodes $src once for local file lookup", ({ src, expected }) => {
const html = `<audio id="vo" src="${src}"></audio>`;
expect(audioTags(html)[0]?.src).toBe(expected);
});
it("patches an entity-encoded id while preserving every other authored byte", () => {
const html = `<audio id="voice&#45;over" src="Q&amp;A.wav" data-volume="&#49;"></audio>`;
expect(audioTags(html)[0]).toEqual(expect.objectContaining({ id: "voice-over", volume: 1 }));
expect(updateAudioVolume(html, "voice-over", 0.5)).toBe(
`<audio id="voice&#45;over" src="Q&amp;A.wav" data-volume="0.5"></audio>`,
);
});
it("decodes video audibility and timing attributes before interpreting them", () => {
const html = `<video id="vo" src="Q&amp;A.mp4" data-has-audio="tr&#117;e" data-media-start="&#50;" data-duration="&#51;" data-playback-rate="&#49;"></video>`;
expect(audioTags(html)[0]).toEqual(
expect.objectContaining({
id: "vo",
src: "Q&A.mp4",
mediaStart: 2,
duration: 3,
playbackRate: 1,
}),
);
});
});
describe("loudnessMeasureArgs with a playback rate", () => {
it("measures the source span a sped-up clip actually plays", () => {
const args = loudnessMeasureArgs("clip.mp4", { mediaStart: 0, duration: 4, playbackRate: 2 });
@@ -3,6 +3,7 @@ import { isAbsolute, relative, resolve } from "node:path";
import { promisify } from "node:util";
import { isAudibleVideoElement } from "@hyperframes/core/audible-video";
import { formatAudioGain, MAX_AUDIO_GAIN_DB } from "@hyperframes/core/audio-gain";
import { decodeAuthoredAttribute } from "@hyperframes/parsers";
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
import { requestedProjectPath } from "./requestSubPath.js";
@@ -101,7 +102,14 @@ function attributeFromMatch(
const whole = match[0];
const localValueStart = attributeValueStart(whole);
const valueStart = absoluteOffset + match.index + localValueStart;
return [name, { value, valueStart, valueEnd: valueStart + value.length }];
return [
name,
{
value: decodeAuthoredAttribute(value),
valueStart,
valueEnd: valueStart + value.length,
},
];
}
function matchedAttributeValue(match: RegExpMatchArray): string | undefined {