@zfadhli/tokrec 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +2 -2
- package/dist/cli.mjs.map +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -4
- package/dist/config.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/recorder/normalize.mjs +5 -4
- package/dist/recorder/normalize.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -22,8 +22,8 @@ function parseArgs(argv = process.argv) {
|
|
|
22
22
|
cmd.option("-s, --segment-minutes <minutes>", "Segment duration in minutes (default: disabled). Split recording into segments of this length.");
|
|
23
23
|
cmd.option("--normalize", "Normalize audio loudness (EBU R128)");
|
|
24
24
|
cmd.option("--normalize-loudness <num>", "Target loudness in LUFS (default: -14)");
|
|
25
|
-
cmd.option("--normalize-codec <name>", "Audio codec for normalized output (default:
|
|
26
|
-
cmd.option("--normalize-bitrate <str>", "Audio bitrate for normalized output (default:
|
|
25
|
+
cmd.option("--normalize-codec <name>", "Audio codec for normalized output (default: libopus)");
|
|
26
|
+
cmd.option("--normalize-bitrate <str>", "Audio bitrate for normalized output (default: 96k)");
|
|
27
27
|
cmd.option("--rate <n>", "Max API requests per second (default: 5). Set 0 for unlimited.");
|
|
28
28
|
cmd.option("--debug", "Show API debug logging on stderr for troubleshooting");
|
|
29
29
|
cmd.action((opts) => {
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["pkg.version"],"sources":["../src/cli.ts"],"sourcesContent":["/**\n * CLI argument parser — uses koko-cli's createCLI (wraps cac).\n * Exports a single parseArgs() function.\n */\n\nimport { createCLI } from \"@zfadhli/koko-cli\"\nimport pkg from \"../package.json\"\nimport type { LogLevel, RecorderConfig } from \"./config\"\nimport { TikTokError } from \"./config\"\nimport { sanitizeUser } from \"./utils\"\n\nexport function parseArgs(argv: string[] = process.argv): RecorderConfig {\n let config: RecorderConfig | undefined\n let error: string | undefined\n\n const cli = createCLI(\"tiktok-live-recorder\", pkg.version).description(\n \"Minimal TikTok live stream recorder — Bun + TypeScript\",\n )\n\n cli.command(\"\", \"Record a TikTok live stream\", (cmd) => {\n cmd.option(\"-u, --user <name>\", \"TikTok username (or pass as first argument)\")\n cmd.option(\"-o, --output <dir>\", \"Output directory\", {\n default: \"./recordings\",\n })\n cmd.option(\"-i, --interval <minutes>\", \"Polling interval in minutes\", {\n default: \"3\",\n })\n cmd.option(\"-d, --duration <minutes>\", \"Max recording duration in minutes\")\n cmd.option(\"-p, --proxy <url>\", \"HTTP proxy (e.g. http://127.0.0.1:8080)\")\n cmd.option(\"-l, --log-level <level>\", \"Log level: debug | info | warn | error\")\n cmd.option(\"-c, --cookies <path>\", \"Path to cookies.json\")\n cmd.option(\n \"-s, --segment-minutes <minutes>\",\n \"Segment duration in minutes (default: disabled). Split recording into segments of this length.\",\n )\n cmd.option(\"--normalize\", \"Normalize audio loudness (EBU R128)\")\n cmd.option(\"--normalize-loudness <num>\", \"Target loudness in LUFS (default: -14)\")\n cmd.option(\"--normalize-codec <name>\", \"Audio codec for normalized output (default:
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["pkg.version"],"sources":["../src/cli.ts"],"sourcesContent":["/**\n * CLI argument parser — uses koko-cli's createCLI (wraps cac).\n * Exports a single parseArgs() function.\n */\n\nimport { createCLI } from \"@zfadhli/koko-cli\"\nimport pkg from \"../package.json\"\nimport type { LogLevel, RecorderConfig } from \"./config\"\nimport { TikTokError } from \"./config\"\nimport { sanitizeUser } from \"./utils\"\n\nexport function parseArgs(argv: string[] = process.argv): RecorderConfig {\n let config: RecorderConfig | undefined\n let error: string | undefined\n\n const cli = createCLI(\"tiktok-live-recorder\", pkg.version).description(\n \"Minimal TikTok live stream recorder — Bun + TypeScript\",\n )\n\n cli.command(\"\", \"Record a TikTok live stream\", (cmd) => {\n cmd.option(\"-u, --user <name>\", \"TikTok username (or pass as first argument)\")\n cmd.option(\"-o, --output <dir>\", \"Output directory\", {\n default: \"./recordings\",\n })\n cmd.option(\"-i, --interval <minutes>\", \"Polling interval in minutes\", {\n default: \"3\",\n })\n cmd.option(\"-d, --duration <minutes>\", \"Max recording duration in minutes\")\n cmd.option(\"-p, --proxy <url>\", \"HTTP proxy (e.g. http://127.0.0.1:8080)\")\n cmd.option(\"-l, --log-level <level>\", \"Log level: debug | info | warn | error\")\n cmd.option(\"-c, --cookies <path>\", \"Path to cookies.json\")\n cmd.option(\n \"-s, --segment-minutes <minutes>\",\n \"Segment duration in minutes (default: disabled). Split recording into segments of this length.\",\n )\n cmd.option(\"--normalize\", \"Normalize audio loudness (EBU R128)\")\n cmd.option(\"--normalize-loudness <num>\", \"Target loudness in LUFS (default: -14)\")\n cmd.option(\"--normalize-codec <name>\", \"Audio codec for normalized output (default: libopus)\")\n cmd.option(\"--normalize-bitrate <str>\", \"Audio bitrate for normalized output (default: 96k)\")\n cmd.option(\"--rate <n>\", \"Max API requests per second (default: 5). Set 0 for unlimited.\")\n cmd.option(\"--debug\", \"Show API debug logging on stderr for troubleshooting\")\n\n cmd.action((opts: Record<string, unknown>) => {\n try {\n const user = opts.user as string | undefined\n if (!user) {\n throw new TikTokError(\"config-error\", \"<user> is required (positional or --user)\")\n }\n\n const parsed: RecorderConfig = {\n user: sanitizeUser(user),\n }\n\n if (opts.output) parsed.outputDir = opts.output as string\n if (opts.interval !== undefined) {\n const n = Number(opts.interval)\n if (!Number.isFinite(n) || n < 1) {\n throw new TikTokError(\"config-error\", \"--interval must be a number >= 1\")\n }\n parsed.interval = n\n }\n if (opts.duration !== undefined) {\n const n = Number(opts.duration)\n if (!Number.isFinite(n) || n < 0) {\n throw new TikTokError(\"config-error\", \"--duration must be a number >= 0 (minutes)\")\n }\n parsed.duration = n * 60\n }\n if (opts.proxy) parsed.proxy = opts.proxy as string\n if (opts.logLevel) {\n const levels = [\"debug\", \"info\", \"warn\", \"error\"]\n if (!levels.includes(opts.logLevel as string)) {\n throw new TikTokError(\n \"config-error\",\n `--log-level must be one of: ${levels.join(\", \")}`,\n )\n }\n parsed.logLevel = opts.logLevel as LogLevel\n }\n if (opts.cookies) parsed.cookiesPath = opts.cookies as string\n if (opts.segmentMinutes !== undefined) {\n const n = Number(opts.segmentMinutes)\n if (!Number.isFinite(n) || n < 0) {\n throw new TikTokError(\"config-error\", \"--segment-minutes must be a number >= 0\")\n }\n parsed.segmentMinutes = n\n }\n\n if (opts.normalize) parsed.normalizeAudio = true\n if (opts.normalizeLoudness !== undefined) {\n const n = Number(opts.normalizeLoudness)\n if (!Number.isFinite(n)) {\n throw new TikTokError(\"config-error\", \"--normalize-loudness must be a number\")\n }\n parsed.normalizeLoudness = n\n }\n if (opts.normalizeCodec) parsed.normalizeCodec = opts.normalizeCodec as string\n if (opts.normalizeBitrate) parsed.normalizeBitrate = opts.normalizeBitrate as string\n if (opts.rate !== undefined) {\n const n = Number(opts.rate)\n if (!Number.isFinite(n) || n < 0) {\n throw new TikTokError(\"config-error\", \"--rate must be a number >= 0\")\n }\n parsed.ratePerSecond = n\n }\n if (opts.debug) parsed.debug = true\n\n config = parsed\n } catch (err) {\n error = err instanceof Error ? err.message : String(err)\n }\n })\n })\n\n // If the first bare argument (not a flag or flag value) looks like a username,\n // convert it to --user for cac (which only handles options on the default command).\n const positionalUser = extractPositional(argv)\n if (positionalUser) {\n argv.push(\"--user\", positionalUser)\n }\n\n cli.parse(argv)\n\n if (error) {\n throw new TikTokError(\"config-error\", error)\n }\n\n if (!config) {\n throw new TikTokError(\n \"config-error\",\n \"<user> is required (positional or --user). Use --help for usage.\",\n )\n }\n\n return config\n}\n\n/**\n * Scan argv for the first bare argument that is neither a flag nor a flag's value.\n * Returns the value and removes it from the array, or null if none found.\n */\nfunction extractPositional(argv: string[]): string | null {\n for (let i = 2; i < argv.length; i++) {\n const arg = argv[i]!\n // -- signals end of options; anything after is a positional\n if (arg === \"--\") {\n const pos = argv.splice(i, 1)[0]!\n return pos\n }\n // If this arg is a flag (-*) the next arg is its value\n if (arg.startsWith(\"-\")) {\n // Skip the value if this flag takes one (not a boolean flag like --help/--version)\n const next = argv[i + 1]\n if (next && !next.startsWith(\"-\") && arg !== \"--help\" && arg !== \"--version\") {\n i++ // skip the value\n }\n continue\n }\n // Found a bare positional — remove it and return\n argv.splice(i, 1)\n return arg\n }\n return null\n}\n"],"mappings":"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"}
|
package/dist/config.d.mts
CHANGED
|
@@ -35,9 +35,9 @@ interface RecorderConfig {
|
|
|
35
35
|
normalizeAudio?: boolean;
|
|
36
36
|
/** Target loudness in LUFS (default: -14). */
|
|
37
37
|
normalizeLoudness?: number;
|
|
38
|
-
/** Audio codec for normalized output (default: "
|
|
38
|
+
/** Audio codec for normalized output (default: peaknorm's default, "libopus"). */
|
|
39
39
|
normalizeCodec?: string;
|
|
40
|
-
/** Audio bitrate for normalized output (default: "
|
|
40
|
+
/** Audio bitrate for normalized output (default: peaknorm's default, "96k"). */
|
|
41
41
|
normalizeBitrate?: string;
|
|
42
42
|
/**
|
|
43
43
|
* Max API requests per second (default: 5).
|
package/dist/config.mjs
CHANGED
|
@@ -18,8 +18,6 @@ const DEFAULTS = {
|
|
|
18
18
|
segmentMinutes: 0,
|
|
19
19
|
normalizeAudio: true,
|
|
20
20
|
normalizeLoudness: -14,
|
|
21
|
-
normalizeCodec: "aac",
|
|
22
|
-
normalizeBitrate: "128k",
|
|
23
21
|
ratePerSecond: 5,
|
|
24
22
|
debug: false
|
|
25
23
|
};
|
|
@@ -37,8 +35,8 @@ function normalizeConfig(config) {
|
|
|
37
35
|
segmentMinutes: config.segmentMinutes ?? DEFAULTS.segmentMinutes,
|
|
38
36
|
normalizeAudio: config.normalizeAudio ?? DEFAULTS.normalizeAudio,
|
|
39
37
|
normalizeLoudness: config.normalizeLoudness ?? DEFAULTS.normalizeLoudness,
|
|
40
|
-
normalizeCodec: config.normalizeCodec
|
|
41
|
-
normalizeBitrate: config.normalizeBitrate
|
|
38
|
+
normalizeCodec: config.normalizeCodec,
|
|
39
|
+
normalizeBitrate: config.normalizeBitrate,
|
|
42
40
|
ratePerSecond: config.ratePerSecond ?? DEFAULTS.ratePerSecond,
|
|
43
41
|
debug: config.debug ?? DEFAULTS.debug
|
|
44
42
|
};
|
package/dist/config.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Config — types, defaults, and normalization for the recorder.\n * The single source of truth for every user-facing option.\n */\n\n/**\n * TikTok cookie key-value pairs extracted from Firefox or cookies.json.\n * Includes session cookies (sessionid_ss, tt-target-idc) and any WAF\n * bypass cookies needed to avoid Slardar challenges on www.tiktok.com.\n */\nexport type CookieAuth = Record<string, string>\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface RecorderConfig {\n /** TikTok username (required) */\n user: string\n /** Output directory for recordings (default: ./recordings) */\n outputDir?: string\n /** Polling interval in minutes (default: 3) */\n interval?: number\n /** Max recording duration in seconds (default: 0 — unlimited). CLI accepts minutes, converted internally. */\n duration?: number\n /** HTTP proxy URL (e.g. http://127.0.0.1:8080) */\n proxy?: string\n /** Path to cookies.json (default: ./cookies.json) */\n cookiesPath?: string\n /** Cookie auth loaded from cookies.json */\n cookies?: CookieAuth\n /** Log level (default: info) */\n logLevel?: LogLevel\n /** Whether to print logs to console (default: true). Set false when using Display-based UI. */\n logConsole?: boolean\n /** Segment duration in minutes (default: 0 — disabled). When >= 1, the recording is split into segments of this length. */\n segmentMinutes?: number\n /** Normalize audio loudness via EBU R128 (default: false). */\n normalizeAudio?: boolean\n /** Target loudness in LUFS (default: -14). */\n normalizeLoudness?: number\n /** Audio codec for normalized output (default: \"
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Config — types, defaults, and normalization for the recorder.\n * The single source of truth for every user-facing option.\n */\n\n/**\n * TikTok cookie key-value pairs extracted from Firefox or cookies.json.\n * Includes session cookies (sessionid_ss, tt-target-idc) and any WAF\n * bypass cookies needed to avoid Slardar challenges on www.tiktok.com.\n */\nexport type CookieAuth = Record<string, string>\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface RecorderConfig {\n /** TikTok username (required) */\n user: string\n /** Output directory for recordings (default: ./recordings) */\n outputDir?: string\n /** Polling interval in minutes (default: 3) */\n interval?: number\n /** Max recording duration in seconds (default: 0 — unlimited). CLI accepts minutes, converted internally. */\n duration?: number\n /** HTTP proxy URL (e.g. http://127.0.0.1:8080) */\n proxy?: string\n /** Path to cookies.json (default: ./cookies.json) */\n cookiesPath?: string\n /** Cookie auth loaded from cookies.json */\n cookies?: CookieAuth\n /** Log level (default: info) */\n logLevel?: LogLevel\n /** Whether to print logs to console (default: true). Set false when using Display-based UI. */\n logConsole?: boolean\n /** Segment duration in minutes (default: 0 — disabled). When >= 1, the recording is split into segments of this length. */\n segmentMinutes?: number\n /** Normalize audio loudness via EBU R128 (default: false). */\n normalizeAudio?: boolean\n /** Target loudness in LUFS (default: -14). */\n normalizeLoudness?: number\n /** Audio codec for normalized output (default: peaknorm's default, \"libopus\"). */\n normalizeCodec?: string\n /** Audio bitrate for normalized output (default: peaknorm's default, \"96k\"). */\n normalizeBitrate?: string\n /**\n * Max API requests per second (default: 5).\n * Prevents TikTok WAF triggering on rapid polling or URL refresh loops.\n * Set to 0 for unlimited (not recommended).\n */\n ratePerSecond?: number\n /** Show API debug logging on stderr (default: false). */\n debug?: boolean\n}\n\nexport interface RecorderController {\n /** Start the polling loop */\n start(): Promise<void>\n /** Gracefully stop (joins active recording, then exits) */\n stop(): Promise<void>\n /** Snapshot of current state */\n getStatus(): RecorderStatus\n /** Subscribe to events */\n on<E extends keyof RecorderEventHandler>(event: E, handler: RecorderEventHandler[E]): void\n /** Unsubscribe a previously registered event handler */\n off<E extends keyof RecorderEventHandler>(event: E, handler: RecorderEventHandler[E]): void\n}\n\nexport interface RecorderStatus {\n state: \"idle\" | \"polling\" | \"recording\" | \"converting\" | \"stopped\"\n user: string\n currentFile?: string\n sessionDuration?: number\n lastPollTime?: string\n lastError?: string\n}\n\nexport type RecorderEvent = keyof RecorderEventHandler\n\nexport interface RecorderEventHandler {\n checking: (info: { user: string }) => void\n tick: (info: { user: string; isLive: boolean; roomId?: string }) => void\n \"recording:start\": (info: { user: string; file: string }) => void\n \"download:progress\": (info: { bytes: number; elapsed: number; speed: number }) => void\n \"download:end\": (info: { file: string; duration: number; size: number }) => void\n \"recording:end\": (info: { file: string; duration: number; size: number }) => void\n \"segmenting:start\": (info: { input: string; outputPattern: string }) => void\n \"segmenting:end\": (info: { segments: number }) => void\n \"converting:start\": (info: { input: string }) => void\n converted: (info: { input: string; output: string }) => void\n \"normalize:start\": (info: { file: string }) => void\n \"normalize:progress\": (info: {\n file: string\n percent: number\n phase: \"analyzing\" | \"normalizing\"\n }) => void\n \"normalize:end\": (info: { input: string; output: string }) => void\n \"normalize:error\": (info: { input: string; error: string }) => void\n error: (err: TikTokError) => void\n}\n\nexport type AppErrorKind =\n | \"user-not-live\"\n | \"room-not-found\"\n | \"stream-url-not-found\"\n | \"waf-blocked\"\n | \"country-blocked\"\n | \"network-error\"\n | \"ffmpeg-not-found\"\n | \"ffmpeg-error\"\n | \"aborted\"\n | \"config-error\"\n | \"unknown\"\n\nexport class TikTokError extends Error {\n constructor(\n public readonly kind: AppErrorKind,\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message)\n this.name = \"TikTokError\"\n }\n}\n\nconst DEFAULTS = {\n outputDir: \"./recordings\",\n interval: 3,\n duration: 0,\n logLevel: \"info\" as LogLevel,\n logConsole: true,\n segmentMinutes: 0,\n normalizeAudio: true,\n normalizeLoudness: -14,\n ratePerSecond: 5,\n debug: false,\n}\n\nexport function normalizeConfig(\n config: RecorderConfig,\n): Required<\n Omit<RecorderConfig, \"cookies\" | \"cookiesPath\" | \"proxy\" | \"normalizeCodec\" | \"normalizeBitrate\">\n> &\n Pick<\n RecorderConfig,\n \"cookies\" | \"cookiesPath\" | \"proxy\" | \"normalizeCodec\" | \"normalizeBitrate\"\n > {\n return {\n user: config.user,\n outputDir: config.outputDir ?? DEFAULTS.outputDir,\n interval: config.interval ?? DEFAULTS.interval,\n duration: config.duration ?? DEFAULTS.duration,\n logLevel: config.logLevel ?? DEFAULTS.logLevel,\n logConsole: config.logConsole ?? DEFAULTS.logConsole,\n proxy: config.proxy,\n cookies: config.cookies,\n cookiesPath: config.cookiesPath,\n segmentMinutes: config.segmentMinutes ?? DEFAULTS.segmentMinutes,\n normalizeAudio: config.normalizeAudio ?? DEFAULTS.normalizeAudio,\n normalizeLoudness: config.normalizeLoudness ?? DEFAULTS.normalizeLoudness,\n normalizeCodec: config.normalizeCodec,\n normalizeBitrate: config.normalizeBitrate,\n ratePerSecond: config.ratePerSecond ?? DEFAULTS.ratePerSecond,\n debug: config.debug ?? DEFAULTS.debug,\n }\n}\n\nexport function validateConfig(config: RecorderConfig): void {\n if (!config.user || config.user.trim().length === 0) {\n throw new TikTokError(\"config-error\", \"--user is required\")\n }\n if (config.interval !== undefined && config.interval < 1) {\n throw new TikTokError(\"config-error\", \"--interval must be >= 1 minute\")\n }\n if (config.duration !== undefined && config.duration < 0) {\n throw new TikTokError(\"config-error\", \"--duration must be >= 0 seconds\")\n }\n}\n"],"mappings":";AAgHA,IAAa,cAAb,cAAiC,MAAM;CAEnB;CAEA;CAHlB,YACE,MACA,SACA,OACA;EACA,MAAM,OAAO;EAJG,KAAA,OAAA;EAEA,KAAA,QAAA;EAGhB,KAAK,OAAO;CACd;AACF;AAEA,MAAM,WAAW;CACf,WAAW;CACX,UAAU;CACV,UAAU;CACV,UAAU;CACV,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;CAChB,mBAAmB;CACnB,eAAe;CACf,OAAO;AACT;AAEA,SAAgB,gBACd,QAOE;CACF,OAAO;EACL,MAAM,OAAO;EACb,WAAW,OAAO,aAAa,SAAS;EACxC,UAAU,OAAO,YAAY,SAAS;EACtC,UAAU,OAAO,YAAY,SAAS;EACtC,UAAU,OAAO,YAAY,SAAS;EACtC,YAAY,OAAO,cAAc,SAAS;EAC1C,OAAO,OAAO;EACd,SAAS,OAAO;EAChB,aAAa,OAAO;EACpB,gBAAgB,OAAO,kBAAkB,SAAS;EAClD,gBAAgB,OAAO,kBAAkB,SAAS;EAClD,mBAAmB,OAAO,qBAAqB,SAAS;EACxD,gBAAgB,OAAO;EACvB,kBAAkB,OAAO;EACzB,eAAe,OAAO,iBAAiB,SAAS;EAChD,OAAO,OAAO,SAAS,SAAS;CAClC;AACF;AAEA,SAAgB,eAAe,QAA8B;CAC3D,IAAI,CAAC,OAAO,QAAQ,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,GAChD,MAAM,IAAI,YAAY,gBAAgB,oBAAoB;CAE5D,IAAI,OAAO,aAAa,KAAA,KAAa,OAAO,WAAW,GACrD,MAAM,IAAI,YAAY,gBAAgB,gCAAgC;CAExE,IAAI,OAAO,aAAa,KAAA,KAAa,OAAO,WAAW,GACrD,MAAM,IAAI,YAAY,gBAAgB,iCAAiC;AAE3E"}
|
package/dist/package.mjs
CHANGED
|
@@ -5,15 +5,16 @@ function createAudioNormalizer(opts, logger) {
|
|
|
5
5
|
logger?.info(`Normalizing audio: ${file}`);
|
|
6
6
|
opts.onStart?.(file);
|
|
7
7
|
try {
|
|
8
|
-
const
|
|
8
|
+
const normalizeOpts = {
|
|
9
9
|
loudness: opts.loudness,
|
|
10
|
-
audioCodec: opts.audioCodec,
|
|
11
|
-
audioBitrate: opts.audioBitrate,
|
|
12
10
|
backup: false,
|
|
13
11
|
onFileProgress: (f, percent, phase) => {
|
|
14
12
|
opts.onProgress?.(f, percent, phase);
|
|
15
13
|
}
|
|
16
|
-
}
|
|
14
|
+
};
|
|
15
|
+
if (opts.audioCodec !== void 0) normalizeOpts.audioCodec = opts.audioCodec;
|
|
16
|
+
if (opts.audioBitrate !== void 0) normalizeOpts.audioBitrate = opts.audioBitrate;
|
|
17
|
+
const result = await normalizeFile(file, normalizeOpts);
|
|
17
18
|
logger?.info(`Audio normalized: ${result.output}`);
|
|
18
19
|
opts.onComplete?.(result);
|
|
19
20
|
return result;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize.mjs","names":[],"sources":["../../src/recorder/normalize.ts"],"sourcesContent":["/**\n * Audio normalizer — wraps peaknorm's EBU R128 loudness normalization.\n * Runs after conversion/segmenting to normalize the final MP4 audio.\n */\n\nimport type { NormalizeResult } from \"peaknorm\"\nimport { normalizeFile } from \"peaknorm\"\nimport type { Logger } from \"../logger\"\n\nexport interface AudioNormalizer {\n /** Normalize a single audio/video file. Returns the normalize result. */\n normalize(file: string): Promise<NormalizeResult>\n}\n\nexport interface AudioNormalizerOptions {\n /** Target loudness in LUFS (default: -14). */\n loudness: number\n /** Audio codec for output (default: \"
|
|
1
|
+
{"version":3,"file":"normalize.mjs","names":[],"sources":["../../src/recorder/normalize.ts"],"sourcesContent":["/**\n * Audio normalizer — wraps peaknorm's EBU R128 loudness normalization.\n * Runs after conversion/segmenting to normalize the final MP4 audio.\n */\n\nimport type { NormalizeOptions, NormalizeResult } from \"peaknorm\"\nimport { normalizeFile } from \"peaknorm\"\nimport type { Logger } from \"../logger\"\n\nexport interface AudioNormalizer {\n /** Normalize a single audio/video file. Returns the normalize result. */\n normalize(file: string): Promise<NormalizeResult>\n}\n\nexport interface AudioNormalizerOptions {\n /** Target loudness in LUFS (default: -14). */\n loudness: number\n /** Audio codec for output (default: peaknorm's default, \"libopus\"). */\n audioCodec?: string\n /** Audio bitrate for output (default: peaknorm's default, \"96k\"). */\n audioBitrate?: string\n /** Called when a file starts normalization. */\n onStart?: (file: string) => void\n /** Called with progress updates (percent 0-100, phase \"analyzing\"|\"normalizing\"). */\n onProgress?: (file: string, percent: number, phase: string) => void\n /** Called when a file completes normalization successfully. */\n onComplete?: (result: NormalizeResult) => void\n /** Called when a file errors during normalization. */\n onError?: (file: string, error: Error) => void\n}\n\nexport function createAudioNormalizer(\n opts: AudioNormalizerOptions,\n logger?: Logger,\n): AudioNormalizer {\n return {\n async normalize(file: string): Promise<NormalizeResult> {\n logger?.info(`Normalizing audio: ${file}`)\n opts.onStart?.(file)\n\n try {\n const normalizeOpts: NormalizeOptions = {\n loudness: opts.loudness,\n backup: false,\n onFileProgress: (f, percent, phase) => {\n opts.onProgress?.(f, percent, phase)\n },\n }\n if (opts.audioCodec !== undefined) normalizeOpts.audioCodec = opts.audioCodec\n if (opts.audioBitrate !== undefined) normalizeOpts.audioBitrate = opts.audioBitrate\n const result = await normalizeFile(file, normalizeOpts)\n\n logger?.info(`Audio normalized: ${result.output}`)\n opts.onComplete?.(result)\n return result\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err))\n logger?.error(`Audio normalization failed: ${error.message}`)\n opts.onError?.(file, error)\n throw error\n }\n },\n }\n}\n"],"mappings":";;AA+BA,SAAgB,sBACd,MACA,QACiB;CACjB,OAAO,EACL,MAAM,UAAU,MAAwC;EACtD,QAAQ,KAAK,sBAAsB,MAAM;EACzC,KAAK,UAAU,IAAI;EAEnB,IAAI;GACF,MAAM,gBAAkC;IACtC,UAAU,KAAK;IACf,QAAQ;IACR,iBAAiB,GAAG,SAAS,UAAU;KACrC,KAAK,aAAa,GAAG,SAAS,KAAK;IACrC;GACF;GACA,IAAI,KAAK,eAAe,KAAA,GAAW,cAAc,aAAa,KAAK;GACnE,IAAI,KAAK,iBAAiB,KAAA,GAAW,cAAc,eAAe,KAAK;GACvE,MAAM,SAAS,MAAM,cAAc,MAAM,aAAa;GAEtD,QAAQ,KAAK,qBAAqB,OAAO,QAAQ;GACjD,KAAK,aAAa,MAAM;GACxB,OAAO;EACT,SAAS,KAAK;GACZ,MAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GAChE,QAAQ,MAAM,+BAA+B,MAAM,SAAS;GAC5D,KAAK,UAAU,MAAM,KAAK;GAC1B,MAAM;EACR;CACF,EACF;AACF"}
|