# Bulk FFmpeg Video Converter - MP4, MP3, GIF & Clips (`automa-flow/bulk-ffmpeg-media-processor`) Actor

Convert up to 100 video or audio URLs per run with FFmpeg: H.264 MP4 for the web or social uploads, MP3, Opus, M4A or WAV audio, GIFs, trimmed clips, thumbnails and 16 kHz speech audio, plus probe and remux. One status row per file; a failed file never stops the batch.

- **URL**: https://apify.com/automa-flow/bulk-ffmpeg-media-processor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Automation, Developer tools, Videos
- **Features:** Limited permissions
- **Stats:** 7 total users, 3 monthly users, 97.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 media transformeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bulk FFmpeg Video Converter - MP4, MP3, GIF & Clips

Process up to 100 independent video or audio URLs in one Actor run. Probe, convert audio to MP3, Opus, M4A or WAV, cut a clip, make a GIF, re-encode a web or social MP4, extract speech audio, create thumbnails, remux, or build lightweight analysis proxies with per-file status and no local FFmpeg server.

**100 Files: Trim, MP3, GIF, Web MP4.** Each preset takes a time window, so one run can turn a list of recordings into short clips, podcasts into MP3s, or screen captures into GIFs.

Paste direct `https://` links to files you are allowed to process. The run writes one Dataset row per file. A successful file also leaves an artifact in the run Key-Value Store. A file that fails stays on its own row and does not cancel the others.

The prefilled input probes one short CC0 flower video published by MDN. You should see one `SUCCESS` row and a probe JSON record. If that sample URL is withdrawn, replace it with a file you can fetch. Run the same operation again when the next batch of URLs arrives: a schedule, an n8n/Make step, or a fresh dataset from the Actor upstream of this one.

Event prices below were set from private staging measurements on 2026-09-24. Update history is in [CHANGELOG.md](CHANGELOG.md).

### Batch example

```json
{
  "operation": "audio16k",
  "items": [
    {"id": "a", "source": "https://example.com/a.mp4"},
    {"id": "b", "source": "https://example.com/b.mp4"}
  ]
}
```

`id` is optional. When you omit it, the row id is `item-0000`, `item-0001`, and so on. Send `items` or a dataset, not both.

### Operations

| Operation | What you get | Options |
| --- | --- | --- |
| `probe` | ffprobe metadata stored as JSON: duration, container, codecs, dimensions, fps, bitrate | none |
| `audio16k` | mono 16 kHz WAV for speech pipelines | `format` must be `wav` when set |
| `thumbnail` | one JPEG or WebP frame | `atSeconds` or `atPercent` (default 10), `imageFormat` `jpeg` or `webp` |
| `remux` | the same audio and video in a new container, no re-encode. MP4 keeps video and audio; MKV also keeps subtitles and attachments. Data tracks such as a camera timecode are left out | `container` `mp4` (default) or `mkv` |
| `proxy` | low-resolution MP4, width capped at `maxWidth` (160-640, default 640) and checked again after encode | `maxWidth`, `trim` |
| `audio` | the first audio track as MP3, Opus (`.ogg`), M4A (AAC) or WAV | `format`, `bitrateKbps`, `sampleRate`, `channels`, `trim` |
| `gif` | an animated GIF of at most 30 seconds, palette built from the clip | `fps`, `maxWidth`, `trim` |
| `web_mp4` | H.264 MP4 for a web page, height capped at 480 or 720, never upscaled | `maxHeight`, `trim` |
| `social_mp4` | H.264 High MP4 for short-video uploads: at most 1080 wide and 30 fps, AAC 48 kHz, aspect kept | `trim` |

`audio16k` also takes `trim`. There is one operation per run. Top-level `options` apply to every file; a file with its own `options` uses those instead. Unknown option names in a file's own `options` are rejected for that file, and invalid top-level `options` stop the run before anything is downloaded.

### Trim a clip

`trim` cuts the same window from every file, or a different one per file:

```json
{
  "operation": "web_mp4",
  "items": [
    {"id": "intro", "source": "https://example.com/talk.mp4", "options": {"trim": {"startSeconds": 0, "endSeconds": 45}}},
    {"id": "answer", "source": "https://example.com/talk.mp4", "options": {"trim": {"startSeconds": 312.5, "durationSeconds": 60}}}
  ]
}
```

Set `startSeconds` (default 0) and either `endSeconds` or `durationSeconds`, or only `startSeconds` to keep the rest of the file. An end past the file's end stops at the end. A start at or past the end is `INVALID_INPUT` with `error.code` `TRIM_OUT_OF_RANGE`, uncharged. The row's `output.trim` shows the window that was encoded, and the charge counts the minutes of that window, not of the source. `maxDurationSeconds` still applies to the source file.

### Audio, GIF, web and social presets

#### `audio`: MP3, Opus, M4A or WAV

```json
{
  "operation": "audio",
  "options": {"format": "mp3", "bitrateKbps": 128},
  "items": [
    {"id": "episode-12", "source": "https://example.com/episode-12.mp4"}
  ]
}
```

`format` is `mp3` (default), `opus`, `m4a` or `wav`. `bitrateKbps` is 32-320 (default 128) and is not used for WAV. `sampleRate` (8000-48000; Opus takes 8000, 12000, 16000, 24000 or 48000) and `channels` (1 or 2) keep the source when left out. A file without an audio track is `UNSUPPORTED_MEDIA` with `NO_AUDIO_STREAM`. Use `audio16k` instead when a speech model wants 16 kHz mono WAV.

#### `gif`: animated GIF

```json
{
  "operation": "gif",
  "options": {"fps": 10, "maxWidth": 480, "trim": {"startSeconds": 4, "durationSeconds": 8}},
  "items": [
    {"id": "demo", "source": "https://example.com/screen-capture.mp4"}
  ]
}
```

`fps` is 1-15 (default 10) and `maxWidth` 120-640 (default 480); a narrower source is not enlarged. A GIF is at most 30 seconds: a longer source without `trim` keeps its first 30 seconds, and `output.trim.capped` is `true` on that row. One GIF is one `media-transformed` event.

#### `web_mp4`: H.264 for a web page

```json
{
  "operation": "web_mp4",
  "options": {"maxHeight": 720},
  "items": [
    {"id": "hero", "source": "https://example.com/hero-4k.mov"}
  ]
}
```

H.264 `yuv420p` at CRF 23 (`veryfast`), AAC 128 kbps stereo, and `faststart` so playback starts before the download ends. `maxHeight` is 480 or 720 (default). The width follows the aspect ratio, both sides are even, and a smaller source keeps its size.

#### `social_mp4`: upload-safe MP4 for short-video platforms

```json
{
  "operation": "social_mp4",
  "items": [
    {"id": "reel-1", "source": "https://example.com/phone-clip.mov"}
  ]
}
```

H.264 High `yuv420p` at CRF 21, at most 1080 pixels wide and 30 fps, a fixed frame rate, AAC 128 kbps stereo at 48 kHz, the index at the front of the file and no edit list. Those settings follow [Meta's Instagram Reels video specs](https://www.facebook.com/business/ads-guide/update/video/instagram-reels) (MP4, H.264, fixed frame rate, stereo AAC at 128 kbps or more) and [YouTube's recommended upload encoding settings](https://support.google.com/youtube/answer/1722171) (H.264 High, 4:2:0, AAC at 48 kHz, index at the front, no edit lists). YouTube recommends keeping the recorded frame rate, so upload a 60 fps original to YouTube as it is. The frame is not cropped or padded: a 16:9 video stays 16:9, so reframe to 9:16 before this step if the platform needs a vertical video.

A preset row has five extra `output` fields, measured on the stored file:

```json
{
  "kvs_key": "artifacts-demo-0000-gif.gif",
  "url": "https://api.apify.com/v2/key-value-stores/STORE/records/artifacts-demo-0000-gif.gif",
  "bytes": 1843200,
  "sha256": "…",
  "content_type": "image/gif",
  "format": "gif",
  "duration_seconds": 8.0,
  "width": 480,
  "height": 270,
  "trim": {"start_seconds": 4.0, "end_seconds": 12.0, "capped": false}
}
```

`trim` is `null` when the whole file was encoded. Rows of `probe`, `thumbnail`, `remux`, and an untrimmed `audio16k` or `proxy` keep their earlier shape without these fields.

### Dataset input

Point the run at a dataset from a previous Actor when the media URL lives on each row:

```json
{
  "operation": "probe",
  "datasetId": "aaaaaaaaaaaaaaaaa",
  "sourceField": "videoUrl",
  "idField": "clipId"
}
```

Pass the dataset id in top-level `datasetId`. In an integration, set that field to `{{resource.defaultDatasetId}}` so the platform substitutes the id before the run starts. A raw `payload.resource.defaultDatasetId` is not read and ends FAILED with an `INVALID_INPUT` summary. `sourceField` is the top-level column that contains the file URL. The run reads at most `maxItems` rows (default 100), starting at `datasetOffset` (default 0). When the dataset has more rows, `RUN_SUMMARY` reports `datasetItemsTotal` and `nextDatasetOffset`, and `nextAction` tells you the offset for the next run, so a 300-row dataset is three runs with offsets 0, 100 and 200. `source_index` is the row's position in the dataset, so generated ids stay stable across those runs. A row with an empty URL becomes `INVALID_INPUT` and the other rows continue. An `idField` value may be text or a number. Characters other than letters, digits, dots, underscores and hyphens become hyphens; a value with nothing usable left gets a generated id. Top-level `options` apply to every row, for example `{"container": "mkv"}` for a remux. Under limited permissions, read access is requested for `datasetId` only.

This mode reads a dataset in your Apify account. It does not read another customer's storage. `kvs://record-name` reads a binary record from this run's own Key-Value Store only.

### Output row

Field names are snake_case. The values below are illustrative, not a recorded customer run.

```json
{
  "source": "bulk-ffmpeg-media-processor",
  "source_id": "a",
  "source_url": "https://example.com/a.mp4",
  "source_index": 0,
  "id": "a",
  "status": "SUCCESS",
  "operation": "audio16k",
  "input": {"bytes": 1200, "sha256": "…", "duration_seconds": 8.0, "container": "mp4"},
  "output": {
    "kvs_key": "artifacts-a-0000-audio16k.wav",
    "url": "https://api.apify.com/v2/key-value-stores/STORE/records/artifacts-a-0000-audio16k.wav",
    "bytes": 256000,
    "sha256": "…",
    "content_type": "audio/wav"
  },
  "output_url": "https://api.apify.com/v2/key-value-stores/STORE/records/artifacts-a-0000-audio16k.wav",
  "processing_ms": 900,
  "error": null,
  "error_code": null,
  "billed_units": 1,
  "scraped_at": "2026-09-23T12:00:00Z",
  "fingerprint": "…",
  "schema_version": 1
}
```

`output_url` repeats `output.url` so the table view can link to the file. Key-Value Store keys cannot contain slashes, so the prefix is `artifacts-` rather than a folder path. `fingerprint` covers the semantic fields and skips the observation time, which makes a later comparison a hash check. `scraped_at` is when this run finished the file. `input.sha256` is the hash of the downloaded file, so the same file behind a new signed URL can be recognized; it is not part of `fingerprint`, which keeps rows comparable with earlier runs. Without your own `id`, rows are named by position (`item-0000`), so pass a stable `id` when you compare runs.

`RUN_SUMMARY` in the Key-Value Store has the overall status, per-status counts, the FFmpeg version line, charged event counts, and `nextAction`. Read that before you fetch a large dataset. `BILLING_RECEIPT` is the charge ledger for the run.

### Related automa-flow tools

- **Keep the original files:** [Parallel Bulk File Downloader](https://apify.com/automa-flow/parallel-bulk-file-downloader) stores the source media from the same URL list or Dataset, with a manifest row per file, so the originals stay next to the processed outputs.
- **Prepare the source Dataset first:** [Dataset Cleaner](https://apify.com/automa-flow/dataset-filter-transform) filters, dedupes and renames rows. Keep only the rows with a media URL, then pass the new Dataset ID as `datasetId` with the same `sourceField`.

Each Actor runs and charges separately. This Actor does not call them.

### Pricing

These event prices come from private staging runs on 2026-09-24 and are the current tariff on this Actor.

| Event | When it is charged | Price |
| --- | --- | --- |
| `apify-actor-start` | Actor start, once per GB of memory (minimum one) | $0.00005 |
| `media-probed` | probe metadata stored | $0.003 |
| `media-transformed` | one started minute of a stored audio16k, audio, thumbnail, GIF, remux, proxy, web MP4 or social MP4 file | $0.01 |
| `media-transformed-hd` | one started minute of a web MP4 or social MP4 made from a source frame larger than 1280x720 (up to 1920x1080) | $0.05 |

A transform bills one event per started minute of the input, with a minimum of one. An 8 second file is one event ($0.01). A 10 minute file is ten events ($0.10). With `trim`, the minutes of the trimmed window count instead: 90 seconds cut from a one-hour recording are two events. A GIF is at most 30 seconds, so it is one event. In staging, re-encoding a 1080p source used four to six times the compute per minute of a 360p one, so a `web_mp4` or `social_mp4` file from a source larger than 1280x720 is charged `media-transformed-hd` per started minute instead; a 1080p phone clip of 45 seconds is $0.05. Until that event appears on this Actor's pricing tab, those files are charged as `media-transformed`. `BILLING_RECEIPT` names the event charged for each file. `billed_units` on the row is that count. A file with no duration is not transformed and is not charged. A failed file, a rejected URL, a retry and a missing artifact are not charged. The artifact is stored before the event is charged. Set `maxTotalChargeUsd` high enough for the minutes you expect. When the limit cannot cover the next file, that file is `BUDGET_EXCEEDED` and files that already succeeded stay in the dataset.

These prices are the event charges. This Actor does not add a separate platform-usage line on top of them. Compute, transfer and storage still cost the publisher; they are not a second bill to you unless the pricing page says pay per event plus usage.

The smallest `maxTotalChargeUsd` the platform accepts for this Actor is $0.0031. A one-file probe at 1024 MB is $0.00305 in events, and at 2048 MB it is $0.00310 because the start event is billed once per GB. Both fit under a $0.0031 cap. A cap of $0.00305 is rejected before the run starts. A 100-file transform of one-minute files is about $1.00005 in events at 1024 MB.

### Limits

- 1-100 files per run (`maxItems`)
- concurrency 1-4, and the operation may use fewer workers (`audio16k`, `audio`, `gif` and `proxy` use at most 2, `web_mp4` and `social_mp4` one)
- a GIF is at most 30 seconds; `social_mp4` keeps the aspect ratio and does not reframe to 9:16
- re-encoding is CPU-bound. In staging at 1024 MB, a 1080p source took about 2.5 minutes per output minute for `web_mp4` or `social_mp4`, and a 360p source under one. The default `processingTimeoutSeconds` (600) therefore fits about 3 minutes of output from a 1080p file. For longer HD files, raise it (up to 3600) or cut them with `trim`. More memory did not make these runs cheaper or reliably faster
- `web_mp4` and `social_mp4` accept sources up to 1920x1080 (2,073,600 pixels, either orientation); a larger frame is `TOO_LARGE` with `RESOLUTION_CAP`, uncharged
- direct file URLs only: no YouTube, TikTok or other page URLs
- 256 MiB per file by default, 1 GiB maximum
- a stored output is held in memory three times during the upload, so it can be at most about 225 MB at the default 1024 MB and about 565 MB at 2048 MB. A bigger output (usually a long remux, web MP4 or social MP4) is `TOO_LARGE` with `error.code` `OUTPUT_TOO_LARGE`, uncharged; run it with 2048 MB memory
- 2 GiB total download by default
- a transform of media longer than `maxDurationSeconds` (default 1 hour, maximum 2 hours) is `TOO_LARGE`; `probe` reports any duration
- download ports are 80 and 443
- one operation per run
- no ZIP of the outputs

Memory defaults to 1024 MB and cannot be raised above 2048 MB. The run timeout default is 1 hour. Near the run timeout, downloads and ffmpeg get only the time that is left, and files that cannot start in time come back as `TIMEOUT` with `error.code` `RUN_DEADLINE`, uncharged, so the run still finishes with its summary. The time kept for that final delivery is 90 seconds on a long run and shrinks on a short one (15 seconds on a 100 second run), so a short run still processes files. Send those ids again with a longer timeout.

### API, n8n and Make

After the Actor is in your account, start it with the Apify run API and the JSON above. In n8n or Make, use the Apify node or an HTTP POST to the run endpoint, wait for the run, then read the dataset items. Keep rows where `status` is `SUCCESS` and pass `output.url` to the next step.

In n8n, a typical chain is a scraper Actor that collects video URLs, then this Actor on its Dataset. Set `datasetId` with an expression that returns the upstream run's `defaultDatasetId`; the body it sends looks like this:

```json
{
  "operation": "audio",
  "datasetId": "aaaaaaaaaaaaaaaaa",
  "sourceField": "videoUrl",
  "idField": "postId",
  "options": {"format": "mp3", "trim": {"startSeconds": 0, "durationSeconds": 60}}
}
```

Every row then has an MP3 of the first minute, named by the upstream `postId`.

Create a schedule in the Apify Console for the dataset input above when an upstream run refreshes that dataset. Keep the same `sourceField`. A daily schedule is the usual second run. Do not schedule the prefilled flower sample. It is only the first-run check. In an integration, set `datasetId` to `{{resource.defaultDatasetId}}`. The platform substitutes the id before the run, and that top-level field is the one this Actor can read. A raw `payload.resource.defaultDatasetId` is not read and ends FAILED with an `INVALID_INPUT` summary. Retry ids whose `status` is not `SUCCESS`, so stored files are not charged again.

Ask an agent: "Probe these direct media URLs and return one status row per file." The direct MCP endpoint is <https://mcp.apify.com?tools=automa-flow/bulk-ffmpeg-media-processor>. The agent should call this for direct file URLs and a bounded batch. It should not call it for YouTube, TikTok or other page URLs. Execution uses the caller's Apify account. Read `RUN_SUMMARY` first. If `status` is `PARTIAL` or `ALL_FAILED`, inspect `error.code` before retrying. Retry the failed ids only, so successful files are not charged again.

### Failure semantics

`SUCCESS` means the expected artifact exists, is non-empty, passed an output check, and was stored. Thumbnails must match JPEG or WebP bytes. Speech audio must be mono 16 kHz. Remux and proxy files must be readable by ffprobe, and a proxy must not be wider than `maxWidth`. An `audio` file must hold the requested codec and, when set, sample rate and channels. A GIF must be a GIF within `maxWidth`; a web or social MP4 must be H.264 with even dimensions within its height, width and frame-rate limits. Any other status has `output` null and an `error` object. An HTML page, even with HTTP 200, is `SOURCE_FAILED` (`HTML_BODY`), not a successful media file. A 404 or 410 is `SOURCE_NOT_FOUND`. A private or metadata address is `INVALID_INPUT` (`SSRF_REJECTED`) when it is caught before the download, or `SOURCE_BLOCKED` when a redirect points there. A timeout is `TIMEOUT`. Those are different outcomes: an unreachable host is not the same as a file that was fetched and contains no matching media.

| Status | Meaning |
| --- | --- |
| `SUCCESS` | Artifact stored |
| `INVALID_INPUT` | Bad id, duplicate id, unknown option, blocked URL shape, or a `trim` start at or past the end of the file (`TRIM_OUT_OF_RANGE`) |
| `SOURCE_NOT_FOUND` | HTTP 404 or 410, or a missing `kvs://` record |
| `SOURCE_BLOCKED` | HTTP 401/403, or a redirect to a blocked address |
| `SOURCE_FAILED` | Other HTTP errors, DNS failure, HTML body, network error |
| `TOO_LARGE` | Byte cap, duration cap, output too large to store (`OUTPUT_TOO_LARGE`) or a re-encode source above 1920x1080 (`RESOLUTION_CAP`) |
| `UNSUPPORTED_MEDIA` | ffprobe could not read a container or streams, or the file lacks the stream the operation needs (`NO_AUDIO_STREAM`, `NO_VIDEO_STREAM`), or the FFmpeg build lacks the encoder (`ENCODER_MISSING`) |
| `PROCESSING_FAILED` | ffmpeg failed or the output failed validation |
| `TIMEOUT` | Download or ffmpeg exceeded its timeout, or the run timeout came first (`RUN_DEADLINE`) |
| `BUDGET_EXCEEDED` | `maxTotalChargeUsd` cannot cover another success |

The run finishes successfully when item failures are the only problem. It fails the whole run when FFmpeg is missing or built with `--enable-nonfree`, when the Key-Value Store or dataset cannot be written, or when a charge call returns an uncertain result. It also fails, after delivering every row, when no file was useful because the run timeout stopped all of them (`globalFailure` `RUN_DEADLINE`); nothing is charged for those files. Download failures, even on every file, stay per-file statuses under a successful run, because dead or mistyped hosts are the files' problem, not the run's. A restarted process does not replay charges for a delivery that already started.

### Security

Media URLs are downloaded with a normal HTTP client and only then passed to FFmpeg as local files. FFmpeg does not receive the URL, a shell string, or a custom filter graph.

The client allows `http` and `https` on ports 80 and 443. It rejects other schemes, embedded passwords, localhost, link-local and metadata names, private and reserved IP ranges, IPv6 loopback, unique-local and link-local addresses, and non-canonical IP spellings. DNS is checked before the request and again when the connection opens, including after every redirect. The body is streamed and cut at the byte cap even when `Content-Length` is missing or too small. Query values are redacted in logs. The client does not send cookies or an `Authorization` header.

You supply the media. This Actor does not extract videos from web pages, does not run yt-dlp, and does not accept raw FFmpeg arguments.

### Licensing and your media

You need the rights or permission to process the files you submit.

The container installs Debian's unmodified `ffmpeg` package from the Actor base image and refuses a build that contains `--enable-nonfree`. Debian's FFmpeg is typically GPL-2 or later because optional GPL components such as libx264 are enabled. The image keeps Debian's copyright file and the installed package version. Source for that binary is the corresponding Debian source package (`apt-get source ffmpeg` on the same Debian release). This Actor does not add its own FFmpeg patches.

Encoders are the native WAV, JPEG, AAC and GIF paths, stream copy for remux, WebP when libwebp is present, libmp3lame for MP3, libopus for Opus, libx264 for web and social MP4, and libx264 or MPEG-4 for the proxy. A preset whose encoder is missing from the build is `UNSUPPORTED_MEDIA` with `ENCODER_MISSING`, never a silent substitute. The measured image is Debian `ffmpeg 7.1.5-0+deb13u1` with `--enable-gpl`, `--enable-libmp3lame`, `--enable-libopus`, `--enable-libx264` and without `--enable-nonfree`. The owner accepted the build on 2026-09-24. Codec patents are not claimed to be exhausted. This section is the license note shipped with the Actor.

FFmpeg is a trademark of the FFmpeg project. This Actor is not affiliated with, endorsed by, or sponsored by FFmpeg. The name is used to describe command-line compatibility.

### Technical details

Python 3.12 on the Apify Python image. Downloads use `httpx` with a pinned DNS backend. FFmpeg and ffprobe run as argument arrays against temporary files, with a timeout and a kill on expiry. Temporary files are removed after each item is stored or fails. There is no browser, no proxy product, and no external database.

The startup log records the FFmpeg version line and whether the build enabled GPL components.

# Changelog

This Actor's version history is a separate document: https://apify.com/automa-flow/bulk-ffmpeg-media-processor/changelog.md

# Actor input Schema

## `operation` (type: `string`):

probe reads metadata. audio16k writes 16 kHz mono WAV for speech tools. thumbnail saves one JPEG or WebP frame. remux copies streams into MP4 or MKV. proxy builds a low-resolution H.264 or MPEG-4 MP4. audio converts to MP3, Opus, M4A or WAV. gif makes an animated GIF of up to 30 seconds. web_mp4 re-encodes to an H.264 MP4 for web pages (480p or 720p). social_mp4 makes an upload-safe H.264 MP4 for short-video platforms (up to 1080 wide, 30 fps). web_mp4 and social_mp4 accept sources up to 1920x1080.

## `items` (type: `array`):

1-100 direct media files. Use this or a dataset, not both. id is optional. source is an http(s) file URL or kvs://record in this run's Key-Value Store.

## `options` (type: `object`):

Operation options for every file, in items or dataset mode. A file with its own options uses those instead. audio: format mp3 (default), opus, m4a or wav; bitrateKbps 32-320 (default 128, not used for wav); sampleRate 8000-48000 (Opus: 8000, 12000, 16000, 24000 or 48000) and channels 1 or 2, both left out to keep the source. gif: fps 1-15 (default 10), maxWidth 120-640 (default 480). web_mp4: maxHeight 480 or 720 (default). trim for audio, gif, web_mp4, social_mp4, audio16k and proxy: {"startSeconds": 5, "endSeconds": 20} or {"startSeconds": 5, "durationSeconds": 15}; billing counts the trimmed minutes. audio16k: format wav. thumbnail: atSeconds or atPercent, and imageFormat jpeg or webp. remux: container mp4 or mkv. proxy: maxWidth from 160 to 640. Invalid options stop the run before any file is downloaded.

## `datasetId` (type: `string`):

Apify dataset to read instead of items. The run requests read access to this field. In an integration, set it to {{resource.defaultDatasetId}}. A nested resource id is not read.

## `sourceField` (type: `string`):

Top-level field on each dataset row that holds the media URL. Required in dataset mode.

## `idField` (type: `string`):

Optional top-level field used as the item id. Rows without it get item-0000 style ids.

## `datasetOffset` (type: `integer`):

Dataset mode only. Skip this many rows, then read up to maxItems. When rows remain, RUN_SUMMARY gives nextDatasetOffset for the next run. Default 0.

## `resource` (type: `object`):

Optional webhook object. This Actor does not read defaultDatasetId. Put the id in datasetId.

## `payload` (type: `object`):

Optional webhook wrapper. A nested resource.defaultDatasetId is not read. Put the id in datasetId.

## `concurrency` (type: `integer`):

Upper bound on files processed at once (1-4). The operation may use fewer workers. Default 2.

## `maxItems` (type: `integer`):

Maximum files to accept from items or a dataset. 1-100. Default 100.

## `maxInputBytesPerItem` (type: `integer`):

Reject a file whose Content-Length or streamed body exceeds this size. Default 256 MiB. Maximum 1 GiB. A remux or proxy output above about 225 MB at 1024 MB memory (565 MB at 2048 MB) is not stored.

## `maxTotalInputBytes` (type: `integer`):

Stop accepting further files once the downloaded bytes in this run would pass this total. Default 2 GiB. Must be at least the per-file cap.

## `maxDurationSeconds` (type: `integer`):

Do not transform media whose probed duration is longer than this. A probe still reports any duration. Default 3600. Maximum 7200.

## `downloadTimeoutSeconds` (type: `integer`):

Total time allowed to download one file, including redirects and the one retry. Default 120.

## `processingTimeoutSeconds` (type: `integer`):

Maximum time ffprobe or ffmpeg may run for one file. The process is killed when this expires. Default 600.

## Actor input object example

```json
{
  "operation": "probe",
  "items": [
    {
      "id": "flower",
      "source": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
    }
  ],
  "datasetOffset": 0,
  "concurrency": 2,
  "maxItems": 100,
  "maxInputBytesPerItem": 268435456,
  "maxTotalInputBytes": 2147483648,
  "maxDurationSeconds": 3600,
  "downloadTimeoutSeconds": 120,
  "processingTimeoutSeconds": 600
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

## `billingReceipt` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "operation": "probe",
    "items": [
        {
            "id": "flower",
            "source": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
        }
    ],
    "concurrency": 2
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/bulk-ffmpeg-media-processor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "operation": "probe",
    "items": [{
            "id": "flower",
            "source": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4",
        }],
    "concurrency": 2,
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/bulk-ffmpeg-media-processor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "operation": "probe",
  "items": [
    {
      "id": "flower",
      "source": "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
    }
  ],
  "concurrency": 2
}' |
apify call automa-flow/bulk-ffmpeg-media-processor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/bulk-ffmpeg-media-processor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/xx3kXcNjdzma9Rodb/builds/cjg8PXR9kSS34RTyW/openapi.json
