# Video Scene Splitter: scenes, keyframes, probe report (`frameprobe/video-scene-splitter`) Actor

Turn any video into scenes and frames your model can read. Scene boundaries with timestamps, one keyframe per scene, and a full technical report. Deterministic ffmpeg, no API key.

- **URL**: https://apify.com/frameprobe/video-scene-splitter.md
- **Developed by:** [FrameProbe](https://apify.com/frameprobe) (community)
- **Categories:** Videos, Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Video Scene Splitter: scenes, keyframes, probe report

**Last verified: 2026-08-12**

Turn any video into scenes and frames your model can read. Give it a video, get back every scene
boundary with timestamps, one representative still per scene, and a technical report on the file.

Deterministic ffmpeg. No model, no API key, nothing to sign up for.

### What you get, per video

- **Scene boundaries.** Every cut, with start, end and duration. Contiguous, covering the whole
  video, no gaps.
- **One keyframe per scene**, saved to the key-value store. Taken from the middle of the scene, not
  the first frame, so it is not a dissolve or a transition. If the middle happens to be a black
  frame, it samples elsewhere in the scene instead of handing your model an empty picture. The key
  is stored **inside each scene object**, so you never join two lists by position.
- **An ffprobe report.** Duration, resolution, frame rate, video and audio codec, audio presence,
  bitrate, file size, container.

### Quickstart

The recommended path is chaining. Social platforms block video fetching from cloud servers, so run
a scraper first and hand this Actor its dataset.

1. Run any TikTok, Instagram or YouTube scraper.
2. Open its run, copy the **dataset ID** from the Storage tab.
3. Start this Actor with that ID in **Dataset from a scraper run**.
4. Read `scenes[]` on each output record; fetch the stills by `keyframeKey`.

Already have direct media URLs? Put them in **Direct video URLs** instead and skip steps 1 and 2.

```json
{
  "datasetId": "aBcD1234efGh5678",
  "maxVideos": 10,
  "sceneThreshold": 0.35,
  "minSceneSeconds": 0.6,
  "maxKeyframes": 40
}
```

### Example output

```json
{
  "url": "https://cdn.example.com/video/7234567890.mp4",
  "platform": "tiktok",
  "videoId": "9f2a1c7d4e6b8a03",
  "status": "ok",
  "durationSeconds": 52.209,
  "width": 1080,
  "height": 1920,
  "fps": 29.97,
  "videoCodec": "h264",
  "audioCodec": "aac",
  "hasAudio": true,
  "bitrateKbps": 2412.6,
  "sizeBytes": 15728640,
  "containerFormat": "mov,mp4,m4a,3gp,3g2,mj2",
  "sceneCount": 14,
  "cutCadenceSeconds": 2.4,
  "keyframeCount": 14,
  "keyframesTruncated": false,
  "scenes": [
    {"index": 0, "startSeconds": 0.0, "endSeconds": 3.44, "durationSeconds": 3.44,
     "keyframeKey": "scene-9f2a1c7d4e6b8a03-0000.jpg"},
    {"index": 1, "startSeconds": 3.44, "endSeconds": 5.02, "durationSeconds": 1.58,
     "keyframeKey": "scene-9f2a1c7d4e6b8a03-0001.jpg"}
  ],
  "processingSeconds": 8.31,
  "processedAt": "2026-08-12T22:49:05+00:00"
}
```

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `videoUrls` | array | demo clip | Direct media URLs (`.mp4` and similar) |
| `datasetId` | string | — | Read URLs from a scraper's dataset. **The recommended input** |
| `maxVideos` | integer | 10 (max 100) | Ceiling on videos attempted this run |
| `sceneThreshold` | number | 0.35 (0.10-0.90) | How different two frames must be to count as a cut. **Lower finds more scenes** |
| `minSceneSeconds` | number | 0.6 (0-10) | Cuts closer than this merge into the scene before them |
| `maxKeyframes` | integer | 40 (max 100) | Cap on saved stills. Every boundary is still returned |
| `keyframeLongEdge` | integer | 1024 (240-1920) | Longest edge of each still, aspect preserved |
| `includeKeyframes` | boolean | true | Off gives boundaries and the probe report only. Faster and cheaper |

### Cost

| Event | Price |
|---|---|
| Actor start | $0.01 per run |
| Minute of video processed | $0.005 per minute, rounded up, minimum one per video |

Price scales with the work because processing cost does. A 40-second clip is one minute-unit; a
3 minute 10 second video is four.

What that means in practice: 20 short clips cost $0.11. A single 10-minute talk costs $0.06.

**Not charged:** videos that fail to download, resolve or decode. They still appear in the dataset
with a specific reason. A video whose stills were cut short by the time budget **is** charged; its
scene list and probe report are complete and the record flags `keyframesTruncated`.

### Limits, stated plainly

- **Social page links do not work from here.** TikTok, Instagram and YouTube block video fetching
  from datacenter IPs. Paste one and you get a message naming the scraper to chain instead. This is
  a platform restriction, not a bug in this Actor.
- **Instagram and TikTok media URLs expire.** They are short-lived signed links, usually good for
  hours. Fetch them in the same session you scraped them; a stored URL will 403.
- **Maximum 600 seconds and 200 MB per video.** Longer or larger fails with a specific message.
- **Scene detection compares brightness.** Two shots with similar luma score low however different
  their colours are. If an obvious cut is missed, lower `sceneThreshold`. This is a property of the
  detector, not a defect.
- **Cut cadence is a measured proxy.** The detector fires on overlays and kinetic text as well as
  hard cuts, so read it as editing rhythm rather than an exact shot count.
- **A resumed run does not retry failures.** If the platform migrates your run between servers, it
  picks up where it left off and skips everything already in the dataset, including the videos
  that failed. That is deliberate: it is what stops a migration from charging you twice. To retry
  a failed video, start a new run with it.
- **Each video gets a 25 second processing budget.** Nearly always ample; the measured cost is
  about 2.5 seconds per minute of video. A very slow host can burn it on the download alone, in
  which case the video fails with a message saying so and is not charged.

### Changelog

- **0.1** First build. Scene boundaries, one keyframe per scene, full ffprobe report. Per-minute
  pricing. Chains from any scraper's dataset.

Built by FrameProbe. Also see **Reel Teardown**, which adds a vision read on top of the same
pipeline: hooks, structure, on-screen text and why a video worked.

# Actor input Schema

## `videoUrls` (type: `array`):

Direct media file URLs (ending in .mp4 and similar), processed straight away. Social page links (tiktok.com, instagram.com, youtube.com) will NOT work: those platforms block video fetching from cloud servers, so chain a scraper and use the dataset field below. If you paste a page link anyway, the Actor tells you which scraper to run.

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

The recommended input. Run a TikTok, Instagram or YouTube scraper first, then pick its dataset here: this Actor reads the media URLs the scraper already collected and splits each one. Recognised fields include videoUrl, videoUrlNoWaterMark, mediaUrl, webVideoUrl and url.

## `maxVideos` (type: `integer`):

Hard stop on how many videos this run processes, so a large input list cannot spend more than you expect. Cost is per minute of video, so this is a ceiling on how many videos are attempted, not on the total bill. Use the run's maximum charge setting for a hard spend cap.

## `sceneThreshold` (type: `number`):

How different two frames must be to count as a cut, from 0.10 to 0.90. LOWER finds more scenes, including soft transitions, camera moves and animated text. HIGHER finds only hard cuts. Start at the default; if a fast-cut video comes back with too few scenes, lower it by 0.05 at a time.

## `minSceneSeconds` (type: `number`):

Cuts closer together than this merge into the scene before them. Overlays, flashes and kinetic text all trip a scene detector, so without a floor a 3-minute video can come back with 300 'scenes'. RAISE it for cleaner, chunkier scenes; set it to 0 to keep every detection.

## `maxKeyframes` (type: `integer`):

Cap on how many stills are saved per video. Every scene boundary is always returned in the dataset; this only limits the images. When a video has more scenes than this, stills are sampled evenly across the whole video rather than stopping partway through.

## `keyframeLongEdge` (type: `integer`):

Longest edge of each saved still, in pixels. Aspect ratio is preserved and the image is never upscaled. 1024 is a good size for feeding a vision model; drop to 512 for cheaper storage and faster runs.

## `includeKeyframes` (type: `boolean`):

Turn this off to get scene boundaries and the probe report only, with no images. Faster, and it costs you less, because extracting stills is most of the processing time. Useful when you only need cut timings or a technical report.

## Actor input object example

```json
{
  "videoUrls": [
    "https://media.w3.org/2010/05/sintel/trailer.mp4"
  ],
  "maxVideos": 10,
  "sceneThreshold": 0.35,
  "minSceneSeconds": 0.6,
  "maxKeyframes": 40,
  "keyframeLongEdge": 1024,
  "includeKeyframes": true
}
```

# Actor output Schema

## `videos` (type: `string`):

One record per input video, including the ones that failed. Successful records carry every scene boundary with start, end and duration, the measured cut cadence, and a technical report: duration, resolution, frame rate, codecs, audio presence, bitrate and file size. Failed records carry a specific reason and what to do about it, and are never charged.

## `keyframes` (type: `string`):

The JPEG stills, one per scene, keyed scene-<videoId>-NNNN.jpg and referenced by the keyframeKey field on each scene object. Each still is taken from the middle of its scene rather than the first frame, so it is not a dissolve or a transition.

# 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 = {
    "videoUrls": [
        "https://media.w3.org/2010/05/sintel/trailer.mp4"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("frameprobe/video-scene-splitter").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 = { "videoUrls": ["https://media.w3.org/2010/05/sintel/trailer.mp4"] }

# Run the Actor and wait for it to finish
run = client.actor("frameprobe/video-scene-splitter").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 '{
  "videoUrls": [
    "https://media.w3.org/2010/05/sintel/trailer.mp4"
  ]
}' |
apify call frameprobe/video-scene-splitter --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,frameprobe/video-scene-splitter"
        }
    }
}

```

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/Nznx89MMswyc9gwg2/builds/Ggm6HurV5M4YOveGj/openapi.json
