@hadialmarzooq/agent-media-ffmpeg 0.1.0 → 0.2.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/README.md +92 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +60 -7
- package/package.json +5 -3
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @hadialmarzooq/agent-media-ffmpeg
|
|
2
|
+
|
|
3
|
+
Safe FFmpeg execution, progress reporting, and verified media workflows for software agents.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
The FFmpeg backend for [Agent Media](https://github.com/HadiAlMarzooq/agent-media). Compiles semantic Media IR plans into deterministic FFmpeg invocations, executes them with progress reporting, and inspects outputs for verification.
|
|
8
|
+
|
|
9
|
+
### Five high-level workflows
|
|
10
|
+
|
|
11
|
+
All workflows share the same contract: **inspect → plan → serialize → execute → verify**. Each returns `{ source, plan, serializedPlan, output, verification }`.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
makeVertical,
|
|
16
|
+
optimizeForWeb,
|
|
17
|
+
normalize,
|
|
18
|
+
extractAudio,
|
|
19
|
+
extractFrame,
|
|
20
|
+
} from '@hadialmarzooq/agent-media-ffmpeg';
|
|
21
|
+
|
|
22
|
+
// 9:16 vertical, H.264/yuv420p, faststart, size-constrained
|
|
23
|
+
const vertical = await makeVertical({
|
|
24
|
+
input: 'demo.mp4',
|
|
25
|
+
output: 'vertical.mp4',
|
|
26
|
+
maxSizeMB: 25,
|
|
27
|
+
onProgress: ({ phase, percent }) => console.error(`${phase}: ${percent}%`),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Web-optimized: balanced quality, H.264, faststart
|
|
31
|
+
const web = await optimizeForWeb({
|
|
32
|
+
input: 'demo.mp4',
|
|
33
|
+
output: 'web.mp4',
|
|
34
|
+
maxSizeMB: 10,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Normalized high-compatibility copy
|
|
38
|
+
const norm = await normalize({ input: 'demo.mp4', output: 'normalized.mp4' });
|
|
39
|
+
|
|
40
|
+
// Extract audio
|
|
41
|
+
const audio = await extractAudio({ input: 'demo.mp4', output: 'audio.m4a' });
|
|
42
|
+
|
|
43
|
+
// Extract a still frame
|
|
44
|
+
const frame = await extractFrame({ input: 'demo.mp4', output: 'frame.jpg', atSeconds: 2 });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Explicit plan and replay
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { inspectMedia, executePlan, getCapabilities } from '@hadialmarzooq/agent-media-ffmpeg';
|
|
51
|
+
import { planMedia, serializePlan, parsePlan, verifyMedia } from '@hadialmarzooq/agent-media-core';
|
|
52
|
+
|
|
53
|
+
const source = await inspectMedia('demo.mp4');
|
|
54
|
+
const plan = planMedia({
|
|
55
|
+
source,
|
|
56
|
+
capabilities: await getCapabilities(),
|
|
57
|
+
goals: { aspectRatio: '9:16', compatibility: 'high', maxSizeMB: 25 },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Save and replay
|
|
61
|
+
const json = serializePlan(plan);
|
|
62
|
+
const replayed = parsePlan(json);
|
|
63
|
+
const result = await executePlan(replayed, { output: 'vertical.mp4' });
|
|
64
|
+
const output = await inspectMedia(result.output);
|
|
65
|
+
const report = verifyMedia(output, replayed.expectations);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install @hadialmarzooq/agent-media-core @hadialmarzooq/agent-media-ffmpeg
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Prerequisites: Node.js 22+, `ffmpeg` and `ffprobe` on `PATH`.
|
|
75
|
+
|
|
76
|
+
## Safety
|
|
77
|
+
|
|
78
|
+
- Rejects source overwrite, output collisions, and directory escape
|
|
79
|
+
- Cancellation and timeout controls with partial cleanup
|
|
80
|
+
- Concatenation preflight rejects incompatible streams before execution
|
|
81
|
+
- Progress is monotonic and isolated — UI callbacks can't change execution semantics
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
- [Full docs](https://github.com/HadiAlMarzooq/agent-media/tree/main/docs)
|
|
86
|
+
- [Workflows](https://github.com/HadiAlMarzooq/agent-media/blob/main/docs/workflows.md)
|
|
87
|
+
- [API reference](https://github.com/HadiAlMarzooq/agent-media/blob/main/docs/api.md)
|
|
88
|
+
- [Reliability](https://github.com/HadiAlMarzooq/agent-media/blob/main/docs/reliability.md)
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,9 @@ interface ExtractFrameOptions extends WorkflowOptions {
|
|
|
80
80
|
atSeconds?: number;
|
|
81
81
|
format?: 'jpg' | 'png';
|
|
82
82
|
}
|
|
83
|
+
interface ConcatenateOptions extends WorkflowOptions {
|
|
84
|
+
inputs: string[];
|
|
85
|
+
}
|
|
83
86
|
interface WorkflowResult {
|
|
84
87
|
source: MediaMetadata;
|
|
85
88
|
plan: MediaPlan;
|
|
@@ -110,5 +113,9 @@ declare function extractAudio(options: ExtractAudioOptions): Promise<WorkflowRes
|
|
|
110
113
|
* Inspect, plan, execute, and verify a still frame extraction from a video source.
|
|
111
114
|
*/
|
|
112
115
|
declare function extractFrame(options: ExtractFrameOptions): Promise<WorkflowResult>;
|
|
116
|
+
/**
|
|
117
|
+
* Inspect, plan, execute, and verify concatenation of multiple media sources.
|
|
118
|
+
*/
|
|
119
|
+
declare function concatenate(options: ConcatenateOptions): Promise<WorkflowResult>;
|
|
113
120
|
|
|
114
|
-
export { type CompiledOperation, type ExecuteOptions, type ExecutionResult, type ExtractAudioOptions, type ExtractFrameOptions, type FfmpegOptions, type MakeVerticalOptions, type MediaProgress, type MediaProgressPhase, type NormalizeOptions, type OptimizeForWebOptions, type ProgressCallback, type WorkflowOptions, type WorkflowResult, compilePlan, executePlan, extensionForPlan, extractAudio, extractFrame, getCapabilities, inspectMedia, makeVertical, normalize, optimizeForWeb };
|
|
121
|
+
export { type CompiledOperation, type ConcatenateOptions, type ExecuteOptions, type ExecutionResult, type ExtractAudioOptions, type ExtractFrameOptions, type FfmpegOptions, type MakeVerticalOptions, type MediaProgress, type MediaProgressPhase, type NormalizeOptions, type OptimizeForWebOptions, type ProgressCallback, type WorkflowOptions, type WorkflowResult, compilePlan, concatenate, executePlan, extensionForPlan, extractAudio, extractFrame, getCapabilities, inspectMedia, makeVertical, normalize, optimizeForWeb };
|
package/dist/index.js
CHANGED
|
@@ -254,12 +254,12 @@ function extensionForPlan(plan) {
|
|
|
254
254
|
|
|
255
255
|
// src/executor.ts
|
|
256
256
|
import { access, constants, rm } from "fs/promises";
|
|
257
|
-
import { dirname, relative, resolve as resolve2 } from "path";
|
|
257
|
+
import { dirname, extname as extname2, relative, resolve as resolve2 } from "path";
|
|
258
258
|
import { MediaError as MediaError4, validatePlan } from "@hadialmarzooq/agent-media-core";
|
|
259
259
|
|
|
260
260
|
// src/inspect.ts
|
|
261
261
|
import { stat } from "fs/promises";
|
|
262
|
-
import {
|
|
262
|
+
import { resolve } from "path";
|
|
263
263
|
import { MediaError as MediaError3 } from "@hadialmarzooq/agent-media-core";
|
|
264
264
|
async function inspectMedia(input, options = {}) {
|
|
265
265
|
const path = resolve(input);
|
|
@@ -269,7 +269,7 @@ async function inspectMedia(input, options = {}) {
|
|
|
269
269
|
} catch {
|
|
270
270
|
throw new MediaError3({
|
|
271
271
|
code: "UNSUPPORTED_INPUT",
|
|
272
|
-
message: `The input file does not exist: ${
|
|
272
|
+
message: `The input file does not exist: ${path}.`,
|
|
273
273
|
context: { input: path },
|
|
274
274
|
suggestedActions: ["Check the source path and permissions."]
|
|
275
275
|
});
|
|
@@ -442,6 +442,14 @@ function parseSpeed(value) {
|
|
|
442
442
|
async function executePlan(planInput, options) {
|
|
443
443
|
const plan = validatePlan(planInput);
|
|
444
444
|
const output = resolve2(options.output);
|
|
445
|
+
const release = await acquireOutputLock(output);
|
|
446
|
+
try {
|
|
447
|
+
return await executePlanInternal(plan, options, output);
|
|
448
|
+
} finally {
|
|
449
|
+
releaseOutputLock(output, release);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function executePlanInternal(plan, options, output) {
|
|
445
453
|
if (output === resolve2(plan.source.path)) {
|
|
446
454
|
throw new MediaError4({
|
|
447
455
|
code: "PATH_NOT_ALLOWED",
|
|
@@ -466,6 +474,25 @@ async function executePlan(planInput, options) {
|
|
|
466
474
|
suggestedActions: ["Choose a different output path or explicitly enable overwrite."]
|
|
467
475
|
});
|
|
468
476
|
}
|
|
477
|
+
const outputDir = dirname(output);
|
|
478
|
+
if (!await exists(outputDir)) {
|
|
479
|
+
throw new MediaError4({
|
|
480
|
+
code: "OUTPUT_DIR_MISSING",
|
|
481
|
+
message: "The output directory does not exist.",
|
|
482
|
+
context: { output, directory: outputDir },
|
|
483
|
+
suggestedActions: ["Create the output directory before execution."]
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
const expectedExt = extensionForPlan(plan);
|
|
487
|
+
const actualExt = extname2(output);
|
|
488
|
+
if (expectedExt && actualExt && expectedExt !== actualExt) {
|
|
489
|
+
throw new MediaError4({
|
|
490
|
+
code: "OUTPUT_EXTENSION_MISMATCH",
|
|
491
|
+
message: `The output extension "${actualExt}" does not match the plan's expected "${expectedExt}".`,
|
|
492
|
+
context: { output, expectedExtension: expectedExt, actualExtension: actualExt },
|
|
493
|
+
suggestedActions: [`Use a "${expectedExt}" output extension, or adjust the plan.`]
|
|
494
|
+
});
|
|
495
|
+
}
|
|
469
496
|
const sourceMetadata = options.sourceMetadata ?? await inspectMedia(plan.source.path, {
|
|
470
497
|
...options.ffprobePath === void 0 ? {} : { ffprobePath: options.ffprobePath },
|
|
471
498
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
@@ -551,10 +578,10 @@ function executionDuration(plan, source) {
|
|
|
551
578
|
return source.durationSeconds === void 0 ? void 0 : Math.max(0, source.durationSeconds - trim.startSeconds);
|
|
552
579
|
}
|
|
553
580
|
async function preflightConcatenation(plan, source, options) {
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
581
|
+
const concatenate2 = plan.steps.find((step) => step.operation === "concatenate");
|
|
582
|
+
if (concatenate2?.operation !== "concatenate") return;
|
|
556
583
|
const metadata = await Promise.all(
|
|
557
|
-
|
|
584
|
+
concatenate2.inputs.map(async (input, index) => {
|
|
558
585
|
if (index === 0) return source;
|
|
559
586
|
return inspectMedia(input, {
|
|
560
587
|
...options.ffprobePath === void 0 ? {} : { ffprobePath: options.ffprobePath },
|
|
@@ -572,7 +599,7 @@ async function preflightConcatenation(plan, source, options) {
|
|
|
572
599
|
code: "UNSUPPORTED_INPUT",
|
|
573
600
|
message: "Concatenation inputs have incompatible stream layouts.",
|
|
574
601
|
context: {
|
|
575
|
-
input:
|
|
602
|
+
input: concatenate2.inputs[index],
|
|
576
603
|
inputIndex: index,
|
|
577
604
|
incompatibleFields
|
|
578
605
|
},
|
|
@@ -630,6 +657,24 @@ function isWithin(path, directory) {
|
|
|
630
657
|
const pathRelative = relative(directory, path);
|
|
631
658
|
return pathRelative === "" || !pathRelative.startsWith("..") && !pathRelative.includes("..\\");
|
|
632
659
|
}
|
|
660
|
+
var outputLocks = /* @__PURE__ */ new Map();
|
|
661
|
+
async function acquireOutputLock(output) {
|
|
662
|
+
const prev = outputLocks.get(output) ?? Promise.resolve();
|
|
663
|
+
let release = () => void 0;
|
|
664
|
+
const next = new Promise((resolve3) => {
|
|
665
|
+
release = resolve3;
|
|
666
|
+
});
|
|
667
|
+
outputLocks.set(
|
|
668
|
+
output,
|
|
669
|
+
prev.then(() => next)
|
|
670
|
+
);
|
|
671
|
+
await prev;
|
|
672
|
+
return release;
|
|
673
|
+
}
|
|
674
|
+
function releaseOutputLock(output, release) {
|
|
675
|
+
release();
|
|
676
|
+
outputLocks.delete(output);
|
|
677
|
+
}
|
|
633
678
|
|
|
634
679
|
// src/workflows.ts
|
|
635
680
|
import { MediaError as MediaError5, planMedia, serializePlan, verifyMedia } from "@hadialmarzooq/agent-media-core";
|
|
@@ -689,6 +734,13 @@ async function extractFrame(options) {
|
|
|
689
734
|
});
|
|
690
735
|
return executeAndVerify(options, source, plan, "Frame extraction is verified and ready.");
|
|
691
736
|
}
|
|
737
|
+
async function concatenate(options) {
|
|
738
|
+
const source = await inspectPhase(options, "concatenation");
|
|
739
|
+
const plan = await planningPhase(options, "concatenation", source, {
|
|
740
|
+
concatenate: options.inputs
|
|
741
|
+
});
|
|
742
|
+
return executeAndVerify(options, source, plan, "Concatenation is verified and ready.");
|
|
743
|
+
}
|
|
692
744
|
function verticalDimensions(width, height) {
|
|
693
745
|
if (width === void 0 !== (height === void 0)) {
|
|
694
746
|
throw new MediaError5({
|
|
@@ -762,6 +814,7 @@ function emit(onProgress, phase, percent, message) {
|
|
|
762
814
|
}
|
|
763
815
|
export {
|
|
764
816
|
compilePlan,
|
|
817
|
+
concatenate,
|
|
765
818
|
executePlan,
|
|
766
819
|
extensionForPlan,
|
|
767
820
|
extractAudio,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadialmarzooq/agent-media-ffmpeg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Safe FFmpeg execution, progress, and verified media workflows for software agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"media",
|
|
@@ -32,13 +32,15 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
|
-
"dist"
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
36
38
|
],
|
|
37
39
|
"publishConfig": {
|
|
38
40
|
"access": "public"
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
41
|
-
"@hadialmarzooq/agent-media-core": "0.
|
|
43
|
+
"@hadialmarzooq/agent-media-core": "0.2.0"
|
|
42
44
|
},
|
|
43
45
|
"scripts": {
|
|
44
46
|
"build": "tsup src/index.ts --format esm --dts",
|