# Video Scene Detection & Shot List - Cut Timecodes, CSV, EDL (`kantolabs/video-scene-detection`) Actor

Detect every shot in a video file: cut, dissolve and fade timecodes, one keyframe per shot, a shot sheet image, pacing stats, and CSV, JSON and CMX3600 EDL files. Optional clip splitting without re-encoding. $0.018 per video minute.

- **URL**: https://apify.com/kantolabs/video-scene-detection.md
- **Developed by:** [Kanto Labs](https://apify.com/kantolabs) (community)
- **Categories:** Videos, Automation, Developer tools
- **Stats:** 3 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $14.40 / 1,000 video minutes

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

### What does Video Scene Detection & Shot List do?

**Video Scene Detection & Shot List** finds every shot in a video file and gives you a **shot list with cut timecodes**. Each shot has its start and end time, its frame numbers and an SMPTE timecode, and how it begins: a hard **cut**, a **dissolve** or a **fade** through black. You also get **one keyframe image per shot**, a single **shot sheet image** (a storyboard of the whole video) and pacing stats such as cuts per minute and average shot length. Each video's shot list is saved as a **CSV**, a **JSON** and a **CMX3600 EDL** file you can import into Premiere Pro, DaVinci Resolve or Avid. If you need the shots as separate files, it can also **split the video into one clip per shot** without re-encoding.

It runs on direct video file links (MP4, MOV, WEBM, MKV, AVI, M4V, MPEG-TS), and there is no AI model or API key to set up.

### Who is it for?

- **Automation builders (n8n, Make, Zapier, API, AI agents)**: every row has the same field names, a flat `shots` array and links to the files, so a workflow can loop over shots without any parsing.
- **Ad and UGC researchers**: tear down your own or your client's ads shot by shot. The shot sheet, shot lengths and cuts per minute show the pacing at a glance, and help you rebuild a winning video shot for shot.
- **Video editors**: import the EDL to get every cut on your timeline, or split an archive film into one clip per shot.
- **AI dataset builders**: cut long videos into clean single-shot clips, or keep one keyframe per shot for captioning.

### How to use it

1. Paste one or more **direct links to video files** into *Video file URLs*. Dropbox and Google Drive share links work when shared as "anyone with the link".
2. Keep the defaults, or tune *Cut threshold* (lower finds subtler cuts) and *Min shot length*.
3. Optional: turn on *Split into clips*.
4. Run it. Each video gives one dataset row plus its CSV, JSON, EDL, keyframes and shot sheet in the run's key-value store, all behind public signed links.

Running the actor with no input (`{}`) processes a 21-second public-domain NASA sample.

### Pricing

Pay per event, with no start fee and no subscription:

| Event | Price | Silver | Gold |
|---|---|---|---|
| Video minute (each started minute of video scanned) | $0.018 | $0.0162 | $0.0144 |
| Clip (only if *Split into clips* is on) | $0.002 | $0.0018 | $0.0016 |

Keyframes, the shot sheet, and the CSV, JSON and EDL files are **included** in the minute price.

**Videos up to 1080p** (in either orientation, vertical 1080×1920 included) count 1 video minute per minute. Bigger frames take far longer to decode, so they count one video minute per started 2.2 megapixels per frame: **1440p counts 2, and 4K counts 4** (that is, $0.072 per minute of 4K). Each row shows its `billingMultiplier`.

**Worked example:** a 10-minute video costs 10 × $0.018 = **$0.18**. If it has 60 shots and you also split it into clips, add 60 × $0.002 = $0.12, for **$0.30** in total. A batch of 100 ads of 30 seconds each costs 100 × $0.018 = **$1.80**.

Refused links, failed downloads and unreadable files are **free** rows that say what went wrong. If you set a *max cost per run*, the actor checks it **before** each video. It scans only as many minutes as the budget allows (`truncated: true`), and any further videos get free `skipped` rows.

### Output example

Here is a real result for the public-domain 1953 trailer *Phantom From Space* (98 s). It found 43 shots: 36 hard cuts and 6 dissolves.

```json
{
  "fileName": "PhantomFromSpace.mp4",
  "status": "ok",
  "durationSeconds": 98.06,
  "fps": 29.97,
  "shotCount": 43,
  "hardCutCount": 36,
  "dissolveCount": 6,
  "fadeCount": 0,
  "averageShotSeconds": 2.28,
  "medianShotSeconds": 2.102,
  "cutsPerMinute": 25.7,
  "csvUrl": "https://api.apify.com/v2/key-value-stores/.../records/27c536fa-PhantomFromSpace-shots.csv?signature=...",
  "edlUrl": "https://api.apify.com/v2/key-value-stores/.../records/27c536fa-PhantomFromSpace-shots.edl?signature=...",
  "shotSheetImageUrl": "https://api.apify.com/v2/key-value-stores/.../records/27c536fa-PhantomFromSpace-shot-sheet.jpg?signature=...",
  "billedMinutes": 2,
  "shots": [
    {
      "index": 2,
      "start": 4.304,
      "end": 7.207,
      "duration": 2.903,
      "startTimecode": "00:00:04.304",
      "endTimecode": "00:00:07.207",
      "startSmpte": "00:00:04:09",
      "endSmpte": "00:00:07:06",
      "startFrame": 129,
      "endFrame": 216,
      "transition": "cut",
      "cutScore": 0.5165,
      "keyframeTime": 6,
      "keyframeUrl": "https://api.apify.com/v2/key-value-stores/.../records/27c536fa-PhantomFromSpace-shot0002-000006000ms.jpg?signature=...",
      "clipStart": 4.304,
      "clipEnd": 7.207,
      "clipUrl": "https://api.apify.com/v2/key-value-stores/.../records/27c536fa-PhantomFromSpace-clip0002.mp4?signature=..."
    }
  ]
}
```

The EDL is frame-accurate. In the example above, frame 128 is the last frame of shot 1 and frame 129 is the first frame of shot 2.

### How it detects shots

The actor decodes the video **once** at a tiny analysis size. A cut has to be a sharp change that stands out from the motion around it, so camera pans and people moving are not cuts. Camera flashes, which change the picture and then return to it, are ignored.

Dissolves are found when the picture changes gradually and the new picture persists across most of the frame. A hand-held camera drifting, or someone gesturing, does not count. A fade is found when a run of black frames has content on both sides.

The boundary of a dissolve or fade is placed at the middle of the transition. In our tests, a single-take talking-head video gave exactly **1 shot**, and edited interviews and trailers gave one shot per camera change. In screen recordings, a jump to a new page counts as a cut, because the whole picture changes.

### Clips without re-encoding

*Split into clips* cuts the file with stream copy, so it is fast and keeps the original quality. Stream copy can only start a clip on a video keyframe. Each clip therefore starts at the first keyframe in the opening part of its shot, and its exact `clipStart` and `clipEnd` are reported.

If a shot has no keyframe near its start (common in old films and heavily compressed files), it gets no clip of its own. Its frames stay at the end of the previous clip, and it is **not charged**.

### FAQ

#### Why does a 4K video cost more per minute?

Decoding a 4K frame takes about four times the work of a 1080p frame, and every frame must be decoded to find frame-accurate cuts. To keep the price per minute low for the usual 720p and 1080p files, larger frames are billed by size. If you have a 1080p proxy of a 4K edit, run that instead: the timecodes are identical.

#### Is it free to try?

Yes, within Apify's free monthly credit. A 1-minute video costs $0.018.

#### Can I use YouTube, TikTok or Instagram links?

No. Their terms forbid downloading. Use direct links to video files you are allowed to process: your storage, S3/R2/GCS (public or pre-signed), a CDN, Dropbox or Google Drive.

#### Why did I get too many or too few shots?

Raise *Cut threshold* (for example to 0.2–0.3) for fewer shots, or lower it (0.06–0.1) for very dark or low-contrast footage. Raise *Min shot length* to merge rapid montages. Screen recordings report page changes as cuts.

#### Which editors can import the EDL?

Any editor that reads CMX3600: Premiere Pro, DaVinci Resolve, Avid Media Composer, Final Cut Pro (via a converter) and others. The reel is `AX`, and the file name is in the `FROM CLIP NAME` comment, so the editor can relink the media.

#### How long can a video be?

Up to 300 minutes (180 by default) and 4 GB. A 10-minute 1080p video takes about 4–5 minutes to process, so for videos over an hour raise the run timeout in Run options.

### Related Kanto Labs actors

- [Video Frame Extractor](https://apify.com/kantolabs/video-frame-extractor): frames every N seconds, thumbnails and scene keyframes.
- [Video to LLM Context](https://apify.com/kantolabs/video-to-llm-context): keyframes plus a transcript aligned to each scene, for AI agents.
- [Transcribe Audio & Video to Text](https://apify.com/kantolabs/audio-video-to-text): Whisper transcripts with SRT and VTT subtitles.

Found a problem? Open an issue on the Issues tab. We answer within a day.

# Actor input Schema

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

Direct links to video files (MP4, MOV, WEBM, MKV, AVI, M4V, MPEG-TS...), one per line. Your own storage, S3/R2/GCS (public or pre-signed URLs), CDNs, an Apify key-value store, Dropbox and Google Drive share links ('anyone with the link') all work. Pages of platforms such as YouTube, TikTok or Instagram are refused.

## `threshold` (type: `number`):

How big a picture change must be to count as a cut. Lower = more sensitive (finds cuts between similar-looking shots, dark footage); higher = only obvious cuts. A change must also stand out from the motion around it, so camera moves are not cuts. 0.12 suits most videos; try 0.2-0.3 if you get too many shots.

## `minShotSeconds` (type: `number`):

Cuts closer together than this are merged (the stronger one wins), so flashes and very fast montages do not produce slivers. 0.5 keeps the quick cuts of trailers and ads.

## `detectDissolves` (type: `boolean`):

Also find gradual transitions: cross-fades (dissolves) and fades through black. The boundary is placed at the middle of the transition. Turn off to report hard cuts only.

## `keyframes` (type: `boolean`):

Save one image from the middle of every shot (public link in each shot row). Included in the price.

## `shotSheetImage` (type: `boolean`):

Save ONE image with every shot's keyframe tiled in order and labelled (shot number + start time) - a storyboard of the video for ad teardowns and shot-by-shot remakes. Included in the price.

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

Keyframes are made for the first N shots (all shots are still listed with timecodes).

## `keyframeMaxWidth` (type: `integer`):

Keyframes wider than this are scaled down (aspect kept). 0 = original size.

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

Format of the keyframe images.

## `imageQuality` (type: `integer`):

30-100.

## `skipBlankFrames` (type: `boolean`):

When a shot's middle frame is black or one flat colour, step forward up to 2 seconds inside the shot to a frame with content; a shot that is black throughout gets no image.

## `splitClips` (type: `boolean`):

Also cut the video into one file per shot WITHOUT re-encoding (fast, original quality, $0.002 per clip). Stream copy can only start a clip on a video keyframe, so a clip may start a few frames after the cut; the exact clip start/end is reported per shot.

## `maxClips` (type: `integer`):

Clips are cut for the first N shots only.

## `maxDurationMinutes` (type: `integer`):

Longer videos are skipped (free). Up to 300. Videos over ~60 minutes may need a longer run timeout (Run options).

## `maxVideoSizeMb` (type: `integer`):

Larger files are skipped (free).

## `downloadTimeoutSecs` (type: `integer`):

Give up on a video that does not download within this time (free).

## Actor input object example

```json
{
  "videoUrls": [
    "https://api.apify.com/v2/key-value-stores/aXcXwfChlioPWAUJv/records/sample-vertical-talk.mp4?signature=105QVevs72w3z5gMQzgIJ"
  ],
  "threshold": 0.12,
  "minShotSeconds": 0.5,
  "detectDissolves": true,
  "keyframes": true,
  "shotSheetImage": true,
  "maxKeyframes": 500,
  "keyframeMaxWidth": 640,
  "imageFormat": "jpg",
  "imageQuality": 85,
  "skipBlankFrames": true,
  "splitClips": false,
  "maxClips": 200,
  "maxDurationMinutes": 180,
  "maxVideoSizeMb": 2000,
  "downloadTimeoutSecs": 600
}
```

# Actor output Schema

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

No description

## `files` (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 = {
    "videoUrls": [
        "https://api.apify.com/v2/key-value-stores/aXcXwfChlioPWAUJv/records/sample-vertical-talk.mp4?signature=105QVevs72w3z5gMQzgIJ"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kantolabs/video-scene-detection").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://api.apify.com/v2/key-value-stores/aXcXwfChlioPWAUJv/records/sample-vertical-talk.mp4?signature=105QVevs72w3z5gMQzgIJ"] }

# Run the Actor and wait for it to finish
run = client.actor("kantolabs/video-scene-detection").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://api.apify.com/v2/key-value-stores/aXcXwfChlioPWAUJv/records/sample-vertical-talk.mp4?signature=105QVevs72w3z5gMQzgIJ"
  ]
}' |
apify call kantolabs/video-scene-detection --silent --output-dataset

```

## MCP server setup

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

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/Ixnt4PmoE3W7aWVR3/builds/v1hfjFUljtWLbb2Zv/openapi.json
