# Media Toolkit (FFmpeg, ExifTool) (`jetlaunch/media-toolkit-ffmpeg-exiftool`) Actor

Probe, thumbnail, trim, extract audio, convert images, and read or scrub EXIF metadata for media files by URL. Fixed typed operations only.

- **URL**: https://apify.com/jetlaunch/media-toolkit-ffmpeg-exiftool.md
- **Developed by:** [Chris O'Byrne](https://apify.com/jetlaunch) (community)
- **Categories:** Developer tools, Videos, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 media probes

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

## Media Toolkit (FFmpeg, ExifTool)

Media Toolkit runs seven fixed media operations on files you give it by URL: **probe**, **thumbnail**, **trim**, **extract-audio**, **image-convert**, **exif-dump** and **exif-scrub**. It is built for AI agents, automations and developers who need FFmpeg or ExifTool results without running a media server. Every operation is typed and validated. There is no command string, no filter graph and no free-form option, so nothing you send can change what program runs or how.

Try it with the default input: it probes a small public sample video and takes one frame from it. A run takes a few seconds.

### What you can do

| Operation | What it returns |
| --- | --- |
| `probe` | Container, duration, overall bit rate, and every stream with codec, resolution, frame rate, sample rate, channels and language |
| `thumbnail` | One frame at a time offset (seconds, clock time or percentage) as JPEG or PNG, optionally scaled |
| `trim` | A clip between a start and an end time, copied without re-encoding (fast, keeps quality) |
| `extract-audio` | The audio track as MP3 (64 to 320 kbps) or 16-bit WAV, optionally one range, a sample rate and mono or stereo |
| `image-convert` | Resize, convert between JPEG, PNG, WebP, AVIF, TIFF and GIF, set quality, apply EXIF rotation, keep or drop metadata |
| `exif-dump` | All metadata ExifTool can read, as JSON with group names, plus a summary (has GPS, has camera info, has author info) |
| `exif-scrub` | The file with all metadata removed, or with location (GPS) data only removed, plus a before and after report |

You can list several operations in one run. Each URL is downloaded once and all chosen operations run on it, in the order you list them.

### Who it is for

- AI agents and MCP clients that need a safe, bounded media step (get duration, grab a poster frame, pull audio for transcription).
- Developers who want thumbnails, clips or format conversion behind an API call.
- Teams that publish photos and need to strip location data before upload, with proof of what was removed.

### Input

The two required fields are `operations` and `mediaUrls`. The other fields tune individual operations and are ignored by the others.

```json
{
  "operations": ["probe", "thumbnail"],
  "mediaUrls": ["https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"],
  "thumbnailTime": "1",
  "thumbnailFormat": "jpeg",
  "thumbnailWidth": 640
}
```

Useful fields: `thumbnailTime` (`12.5`, `00:01:30` or `25%`), `startTime` with `endTime` or `clipDuration` (trim and extract-audio), `audioFormat`, `audioBitrateKbps`, `audioSampleRate` (16000 suits speech-to-text), `imageFormat`, `width`, `height`, `fit`, `quality`, `keepMetadata`, `scrubMode` (`all` or `location`). The input form on this page describes each one.

### Output

The default dataset has one record per URL and operation. Files an operation produces (thumbnail, clip, audio, converted image, scrubbed file) are saved in the run's default key-value store and the record carries a link in `outputUrl`. Failed items stay in the dataset with an `errorCode` and a plain-language message, and are never charged.

```json
{
  "index": 1,
  "operation": "probe",
  "status": "success",
  "container": "mov,mp4,m4a,3gp,3g2,mj2",
  "durationSeconds": 5.055,
  "videoCodec": "h264",
  "audioCodec": "aac",
  "width": 960,
  "height": 540,
  "bitRate": 1785756,
  "billedEvent": "media-probe"
}
```

A thumbnail record looks like this (the link is shortened here):

```json
{
  "index": 1,
  "operation": "thumbnail",
  "status": "success",
  "durationSeconds": 5.055,
  "outputUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/thumbnail-001-a1b2c3.jpg?signature=...",
  "outputBytes": 14801,
  "outputContentType": "image/jpeg",
  "billedEvent": "media-clip"
}
```

Common error codes: `HTTP_404`, `FILE_TOO_LARGE`, `URL_ADDRESS_NOT_ALLOWED`, `NOT_AUDIO_VIDEO`, `UNREADABLE_MEDIA`, `TIME_OUT_OF_RANGE`, `DURATION_LIMIT`, `NO_AUDIO_STREAM`, `NO_VIDEO_STREAM`, `UNSUPPORTED_IMAGE`, `FORMAT_NOT_WRITABLE`, `PROCESSING_TIMEOUT`.

### Pricing

Pay per event. You pay only for operations that succeed; failed or empty results are free.

| Event | Price | Charged for |
| --- | --- | --- |
| Media probe (`media-probe`) | $0.01 | One successful `probe` |
| Media clip operation (`media-clip`) | $0.03 | One successful `thumbnail`, `trim` or `extract-audio` |
| Image convert (`image-convert`) | $0.03 | One successful `image-convert` |
| EXIF dump or scrub (`exif-op`) | $0.02 | One successful `exif-dump` or `exif-scrub` |

There is also a tiny start fee per run (about $0.00005) charged by the platform. Example: probing and thumbnailing 100 videos costs 100 x $0.01 + 100 x $0.03 = $4.00. Set a maximum cost per run in the run options; the Actor stops cleanly when it is reached and marks the remaining items `skipped`.

### Safety, limits and licences

- **Fixed operations only.** Inputs are validated against typed fields. Nothing you type is passed to a shell or used as an FFmpeg filter or option string.
- **SSRF protection.** Only `http` and `https` on ports 80, 443, 8080 and 8443 are fetched. URLs with embedded credentials are refused. A URL is refused if its host resolves to a private, loopback, link-local, carrier-grade NAT, multicast, reserved or cloud-metadata address, including IPv6 and IPv4-mapped forms. The check runs when the connection is made and again for every redirect (at most 5).
- **Limits.** 500 MB per input file (100 MB and 80 megapixels for images), at most 15 minutes (900 seconds) of media processed per operation, 5 minutes hard timeout per FFmpeg call, 4 minutes per download, 100 URLs per run. Each output file is capped at 250 MB, and exif-scrub accepts files up to 150 MB because the cleaned copy is as large as the original.
- **Files are detected from their content**, not their name, and only audio and video containers on an allow list are passed to FFmpeg with a fixed demuxer. Playlists, scripts and SVG are refused.
- **Privacy.** Media is downloaded to temporary storage of the run, processed, and deleted after each item. The Actor never logs media content or metadata. Outputs live in the run's key-value store under your Apify account and follow your plan's data retention.
- **FFmpeg is an LGPL build.** The Actor compiles FFmpeg 7.1.1 from the official source with `--disable-gpl --disable-nonfree` and the only external codec library is LAME (LGPL), and it refuses to start if FFmpeg reports a GPL or non-free configuration. Because x264 and x265 are GPL, this build cannot encode H.264 or H.265. It can read them, take frames from them, and copy them (trim). FFmpeg is used unmodified, as command-line programs. Source: https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.xz (SHA-256 `733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1`). The build options are listed in the section below, so you can rebuild the same binaries. ExifTool (Artistic License or GPL, run as a separate program), sharp and libvips (Apache-2.0 and LGPL-3.0) and the other components are listed in the `THIRD_PARTY_NOTICES` file.
- **Not affiliated.** This Actor is not affiliated with or endorsed by the FFmpeg project, Phil Harvey (ExifTool), the sharp or libvips projects, or any other third party named here. Names are used only to say what the software does.

#### FFmpeg build options (LGPL)

```
./configure --disable-gpl --disable-nonfree --disable-version3 --disable-autodetect --disable-doc --disable-debug \
  --disable-ffplay --disable-network --disable-avdevice --disable-hwaccels \
  --disable-protocols --enable-protocol=file,pipe \
  --disable-encoders --enable-encoder=mjpeg,png,pcm_s16le,libmp3lame \
  --disable-muxers --enable-muxer=mp4,mov,ipod,3gp,matroska,webm,avi,ogg,flv,mpegts,mp3,wav,flac,adts,mpeg1system,asf,aiff,amr,image2,null \
  --disable-demuxer=hls,dash,concat,ffconcat,sdp,rtsp,rtp,image2,image2pipe,image_jpeg_pipe,image_png_pipe,mpegtsraw,data,lavfi,pipe \
  --disable-filters --enable-filter=scale,format,aresample,aformat,null,anull,buffer,abuffer,buffersink,abuffersink,transpose,hflip,vflip,copy,setpts,asetpts,trim,atrim,fps \
  --enable-libmp3lame --enable-zlib
```

`ffmpeg -L` in the running image reports the GNU Lesser General Public License, and the licence texts are kept in the image under `/usr/local/share/licenses/ffmpeg/`. FFmpeg's networking is compiled out: the Actor downloads files itself, with the address checks above, and gives FFmpeg only a local file.

#### Copyright and your responsibility

You are responsible for having the rights to every file you process, and for how you use the results. Apify and the author do not check ownership, licences or consent. Do not use the Actor to process media you are not allowed to copy or modify, or to remove metadata in order to misrepresent where a file came from.

### FAQ

**Which formats work?** Audio and video containers that the Actor detects and accepts: MP4, MOV, M4A, 3GP, Matroska and WebM, AVI, Ogg, FLV, MPEG-TS and MPEG-PS, MP3, WAV, FLAC, AAC, AIFF, ASF and AMR. MP4, Matroska, WebM, MP3 and AVI are covered by this Actor's own tests; the others are less tested. Images for `image-convert`: JPEG, PNG, GIF, WebP, TIFF, AVIF and JPEG 2000. `exif-dump` reads any format ExifTool understands. HEIC and SVG are not accepted for conversion.

**Why does trim not cut exactly on the frame?** Trim copies the compressed streams, so it is fast and lossless but can start only at a keyframe. The cut snaps to the keyframe before your start time, and the record reports the real output duration. Re-encoding is not offered because the LGPL build has no H.264 encoder.

**Which files can exif-scrub rewrite?** ExifTool rewrites JPEG, PNG, TIFF, WebP, GIF, MP4, MOV, M4A, PDF and several others. For MP3, Matroska, WebM, AVI and similar audio and video files, the Actor copies the streams with FFmpeg and drops all container metadata. Other types return `FORMAT_NOT_WRITABLE` and are not charged. The record lists identifying tags that remain (it is empty when nothing identifying is left) and confirms whether GPS data is gone. The scrub keeps the Orientation tag and colour profile by default so photos still look right.

**Can it download from sites that need a login?** No. Use a public URL, or a pre-signed URL from your own storage.

**How do I call it from code or an agent?** Use the Apify API or the Apify MCP server, pass the input shown above, and read the dataset items. The output link in each record can be downloaded directly.

### Limitations

- One download per URL per run; very slow servers (under 300 KB/s after the first 30 seconds) are cut off.
- Animated GIF and WebP inputs keep only the first frame in `image-convert`.
- No H.264 or H.265 encoding, no merging, overlays, subtitles or filters.
- Duration cap: the 15 minute limit applies to `trim` and `extract-audio` (the selected range). `probe` and `thumbnail` are limited only by the 500 MB file size.
- Transcoding (re-encoding video) is not offered in this version.
- Output links belong to the run's key-value store and expire with your plan's data retention. Download what you need.

### Support

Questions, bugs and feature requests: use the Issues tab on this Actor's page. Include the run ID and the error code.

# Actor input Schema

## `operations` (type: `array`):

Operations to run on every URL, in this order: probe (media facts), thumbnail (one frame), trim (clip between two times), extract-audio (MP3 or WAV), image-convert (resize, convert, re-compress), exif-dump (read metadata), exif-scrub (remove metadata). Each successful operation is one billable event.

## `mediaUrls` (type: `array`):

Public http or https URLs of the files, one per line. Up to 100 per run, 500 MB per file. URLs that resolve to private, loopback, link-local or cloud-metadata addresses are refused, including after redirects. Only ports 80, 443, 8080 and 8443 are used. You are responsible for having the rights to process these files.

## `thumbnailTime` (type: `string`):

Used by thumbnail. Where to take the frame: seconds (12.5), a clock time (00:01:30.5), or a percentage of the duration (25%).

## `thumbnailFormat` (type: `string`):

Used by thumbnail. JPEG is smaller; PNG is lossless.

## `thumbnailWidth` (type: `integer`):

Used by thumbnail. Optional. Scales the frame down to this width and keeps the aspect ratio. Never enlarges.

## `startTime` (type: `string`):

Used by trim and extract-audio. Start of the range in seconds (12.5) or as a clock time (00:01:30). Default 0. Trim copies the streams without re-encoding, so the cut snaps to the preceding keyframe.

## `endTime` (type: `string`):

Used by trim and extract-audio. End of the range in seconds or as a clock time. Required for trim unless you set a clip duration. Use either End time or Clip duration.

## `clipDuration` (type: `number`):

Used by trim and extract-audio. Length of the range in seconds, counted from the start time. Maximum 900.

## `audioFormat` (type: `string`):

Used by extract-audio. MP3 is compressed (LAME encoder); WAV is uncompressed 16-bit PCM and large.

## `audioBitrateKbps` (type: `integer`):

Used by extract-audio with MP3. One of 64, 96, 128, 160, 192, 256 or 320.

## `audioSampleRate` (type: `integer`):

Used by extract-audio. Optional. One of 8000, 16000, 22050, 32000, 44100 or 48000. Leave empty to keep the source rate. 16000 suits speech-to-text.

## `audioChannels` (type: `integer`):

Used by extract-audio. Optional. 1 for mono or 2 for stereo. Leave empty to keep the source layout.

## `audioTrack` (type: `integer`):

Used by extract-audio. Zero-based index of the audio track to extract when a file has several.

## `imageFormat` (type: `string`):

Used by image-convert. Inputs accepted: JPEG, PNG, GIF, WebP, TIFF, AVIF, JPEG 2000. SVG and HEIC are not accepted. Animated images keep only the first frame.

## `width` (type: `integer`):

Used by image-convert. Optional target width, 1 to 8192. Set width, height or both.

## `height` (type: `integer`):

Used by image-convert. Optional target height, 1 to 8192.

## `fit` (type: `string`):

Used by image-convert with width and height. inside keeps the aspect ratio within the box; cover fills the box and crops; contain pads to the box; fill stretches; outside keeps the aspect ratio and covers the box.

## `withoutEnlargement` (type: `boolean`):

Used by image-convert. When on, images smaller than the target are not scaled up.

## `background` (type: `string`):

Used by image-convert. Hex colour such as #ffffff. Fills padding for the contain mode and replaces transparency when saving as JPEG.

## `quality` (type: `integer`):

Used by thumbnail (JPEG) and image-convert (JPEG, WebP, AVIF, TIFF). Higher is better and larger. PNG and GIF ignore it.

## `autoOrient` (type: `boolean`):

Used by image-convert. Rotates the pixels to match the EXIF orientation so the result looks right without the tag.

## `keepMetadata` (type: `boolean`):

Used by image-convert. By default metadata (EXIF, GPS, XMP) is dropped from the converted image. Turn on to keep it.

## `exifNumeric` (type: `boolean`):

Used by exif-dump. Return raw numeric values (for example GPS in decimal degrees and Orientation as a number) instead of print-formatted text.

## `scrubMode` (type: `string`):

Used by exif-scrub. all removes all metadata; location removes GPS data only and keeps the rest.

## `keepOrientation` (type: `boolean`):

Used by exif-scrub in all mode. Keeps the Orientation tag so rotated photos still display upright.

## `keepColorProfile` (type: `boolean`):

Used by exif-scrub in all mode. Keeps the embedded ICC colour profile so colours do not shift.

## Actor input object example

```json
{
  "operations": [
    "probe",
    "thumbnail"
  ],
  "mediaUrls": [
    "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
  ],
  "thumbnailTime": "1",
  "thumbnailFormat": "jpeg",
  "thumbnailWidth": 640,
  "audioFormat": "mp3",
  "audioBitrateKbps": 192,
  "audioTrack": 0,
  "imageFormat": "webp",
  "fit": "inside",
  "withoutEnlargement": true,
  "background": "#ffffff",
  "quality": 85,
  "autoOrient": true,
  "keepMetadata": false,
  "exifNumeric": false,
  "scrubMode": "all",
  "keepOrientation": true,
  "keepColorProfile": true
}
```

# Actor output Schema

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

One record per URL and operation, with the media facts, the error code if it failed, and a link to the output file where one is produced.

## `files` (type: `string`):

Thumbnails, clips, audio files, converted images and scrubbed files produced by the run.

# 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 = {
    "operations": [
        "probe",
        "thumbnail"
    ],
    "mediaUrls": [
        "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
    ],
    "thumbnailTime": "1",
    "thumbnailWidth": 640
};

// Run the Actor and wait for it to finish
const run = await client.actor("jetlaunch/media-toolkit-ffmpeg-exiftool").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 = {
    "operations": [
        "probe",
        "thumbnail",
    ],
    "mediaUrls": ["https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"],
    "thumbnailTime": "1",
    "thumbnailWidth": 640,
}

# Run the Actor and wait for it to finish
run = client.actor("jetlaunch/media-toolkit-ffmpeg-exiftool").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 '{
  "operations": [
    "probe",
    "thumbnail"
  ],
  "mediaUrls": [
    "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
  ],
  "thumbnailTime": "1",
  "thumbnailWidth": 640
}' |
apify call jetlaunch/media-toolkit-ffmpeg-exiftool --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jetlaunch/media-toolkit-ffmpeg-exiftool"
        }
    }
}
```

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/KwKRUSkubusgKgC5E/builds/Q02bUopXL5CYXDLu2/openapi.json
