# Milestone 2 — FFmpeg Core Runtime

## Status

Implemented for `v0.1.0-alpha.2`.

## Purpose

Milestone 2 creates the single execution boundary used by future media-domain commands. No video, audio, conversion, composition, repair, diagnostic, or streaming command is allowed to invoke FFmpeg or FFprobe directly.

```text
CLI / Skill scripts / package API
          │
          ▼
     domain operation
          │
          ▼
 runFFmpeg / runFFprobe
          │
          ▼
     runCommand()
          │
          ▼
 node:child_process.spawn
          │
          ▼
   ffmpeg / ffprobe
```

## Runtime modules

```text
src/core/
├── binary-resolver.ts
├── cancellation.ts
├── capabilities.ts
├── command-result.ts
├── errors.ts
├── ffmpeg-runner.ts
├── ffprobe-runner.ts
├── progress.ts
├── temp-files.ts
└── index.ts
```

## Binary resolution

Resolution order is deterministic:

1. explicit API/CLI override (`--ffmpeg-path`, `--ffprobe-path`);
2. `FFMPEG_PATH` / `FFPROBE_PATH` environment override;
3. `PATH` lookup.

Resolved values are verified as executable files. No shell `which`, `where`, `command -v`, or equivalent is used.

## Execution safety

`runCommand()` is the only production source module allowed to import `node:child_process`.

All invocations use:

```ts
spawn(binary, args, {
  shell: false,
  ...
});
```

The runtime never uses:

```text
eval
exec
execFile
shell interpolation
concatenated command strings for execution
```

A shell-like command string can be rendered for diagnostics, but it is display-only and is never executed.

## Structured execution result

```ts
interface CommandExecution {
  binary: string;
  args: string[];
  cwd?: string;
  exitCode: number | null;
  signal?: NodeJS.Signals;
  durationMs: number;
  stdout?: string;
  stderr?: string;
  executed: boolean;
  stdoutTruncated: boolean;
  stderrTruncated: boolean;
}
```

Non-zero exit codes are intentionally returned by the generic runtime. The FFmpeg and FFprobe adapters classify them as:

- `E_FFMPEG_EXECUTION_FAILED`;
- `E_PROBE_FAILED`.

This keeps process mechanics separate from media-domain semantics.

## Output capture

stdout and stderr are captured incrementally with bounded tail buffers.

Default per-stream capture limit:

```text
4 MiB
```

If a stream exceeds the limit, the oldest bytes are discarded and the result marks:

```text
stdoutTruncated: true
stderrTruncated: true
```

Callers may additionally subscribe to chunks or tee them to the terminal without disabling capture.

## Dry run

`dryRun: true` performs binary resolution and invocation construction but does not spawn the child process.

The returned result contains:

```json
{
  "exitCode": 0,
  "executed": false
}
```

This is the execution primitive behind the public CLI `--dry-run` contract.

## Cancellation

Every runner accepts an `AbortSignal`.

On abort:

1. send `SIGINT` to the child (`SIGTERM` on Windows);
2. wait a configurable grace interval (default 1500 ms);
3. use `SIGKILL` if the process remains alive;
4. reject with `E_ABORTED`.

CLI entrypoints can bridge operating-system signals through `createProcessSignalController()`.

## Temporary workspace

`TemporaryWorkspace` creates isolated directories under the operating system temp directory and rejects path traversal outside the workspace.

By default it cleans recursively. `keep: true` implements the future `--keep-temp` behavior.

## Progress parser

`FFmpegProgressParser` incrementally parses machine-readable output generated by:

```text
-progress pipe:N
```

The parser is transport-independent and can consume arbitrary chunks without assuming that one chunk equals one line.

## Capability foundation

Milestone 2 includes version-line parsing and shared capability types. Actual capability discovery (`-encoders`, `-decoders`, `-filters`, hardware acceleration) belongs to Milestone 3.

## Dependency decision

Milestone 1 provisionally included `execa`. Milestone 2 replaced it with native `spawn` to provide a smaller and more auditable process boundary with explicit streaming and cancellation control. See ADR 0002.
