Bulk FFmpeg Media Processor – 100 URLs/Run avatar

Bulk FFmpeg Media Processor – 100 URLs/Run

Pricing

from $10.00 / 1,000 media transformeds

Go to Apify Store
Bulk FFmpeg Media Processor – 100 URLs/Run

Bulk FFmpeg Media Processor – 100 URLs/Run

Process up to 100 direct video or audio file URLs in one run. Probe metadata, extract 16 kHz speech audio, create a thumbnail, remux, or build a lightweight analysis proxy, with one status row per file.

Pricing

from $10.00 / 1,000 media transformeds

Rating

0.0

(0)

Developer

Vadim Bezrukov

Vadim Bezrukov

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

3

Monthly active users

6 days ago

Last modified

Share

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.

Batch example

{
"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

OperationWhat you getOptions
probeffprobe metadata stored as JSON: duration, container, codecs, dimensions, fps, bitratenone
audio16kmono 16 kHz WAV for speech pipelinesformat must be wav when set
thumbnailone JPEG or WebP frameatSeconds or atPercent (default 10), imageFormat jpeg or webp
remuxthe 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 outcontainer mp4 (default) or mkv
proxylow-resolution MP4, width capped at maxWidth (160-640, default 640) and checked again after encodemaxWidth, trim
audiothe first audio track as MP3, Opus (.ogg), M4A (AAC) or WAVformat, bitrateKbps, sampleRate, channels, trim
gifan animated GIF of at most 30 seconds, palette built from the clipfps, maxWidth, trim
web_mp4H.264 MP4 for a web page, height capped at 480 or 720, never upscaledmaxHeight, trim
social_mp4H.264 High MP4 for short-video uploads: at most 1080 wide and 30 fps, AAC 48 kHz, aspect kepttrim

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:

{
"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

{
"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

{
"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

{
"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

{
"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 (MP4, H.264, fixed frame rate, stereo AAC at 128 kbps or more) and YouTube's recommended upload encoding settings (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:

{
"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:

{
"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.

{
"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.

  • Keep the original files: 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 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.

EventWhen it is chargedPrice
apify-actor-startActor start, once per GB of memory (minimum one)$0.00005
media-probedprobe metadata stored$0.003
media-transformedone started minute of a stored audio16k, audio, thumbnail, GIF, remux, proxy, web MP4 or social MP4 file$0.01
media-transformed-hdone 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:

{
"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.

StatusMeaning
SUCCESSArtifact stored
INVALID_INPUTBad 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_FOUNDHTTP 404 or 410, or a missing kvs:// record
SOURCE_BLOCKEDHTTP 401/403, or a redirect to a blocked address
SOURCE_FAILEDOther HTTP errors, DNS failure, HTML body, network error
TOO_LARGEByte cap, duration cap, output too large to store (OUTPUT_TOO_LARGE) or a re-encode source above 1920x1080 (RESOLUTION_CAP)
UNSUPPORTED_MEDIAffprobe 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_FAILEDffmpeg failed or the output failed validation
TIMEOUTDownload or ffmpeg exceeded its timeout, or the run timeout came first (RUN_DEADLINE)
BUDGET_EXCEEDEDmaxTotalChargeUsd 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.