# CCTV Motion Clipper — Keep Only the Moments with Motion (`adorable_partial/cctv-motion-clipper`) Actor

Turn hours of security-camera footage into minutes: finds motion in CCTV/NVR videos, returns a timeline with thumbnails and a summary video of only the moving parts, with source time burned in. Ignores clock overlays and noise.

- **URL**: https://apify.com/adorable_partial/cctv-motion-clipper.md
- **Developed by:** [Leandro Zanatta](https://apify.com/adorable_partial) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 video minute scanneds

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

## CCTV Motion Clipper — Keep Only the Moments with Motion

Turn **hours of security-camera footage into minutes**. Send CCTV / NVR / DVR video exports and get back a **timeline of every motion event** (start, end, peak, thumbnail), a **summary video** with only the moving parts back to back, and optional **clips per event**, all with the **original time burned in** so every frame can be traced back to the source.

![Motion curve of a 51-second camera clip: two motion events exported, idle periods with a clock overlay and sensor noise ignored](https://api.apify.com/v2/key-value-stores/RL7JMSU2StgBzcpmV/records/demo.png?signature=1bTRe30HqiXcp16jDjrMU)

*Real output of this Actor on a test clip with idle periods, sensor noise and an on-screen clock.*

### Why use it

- ⏱️ **Review footage in a fraction of the time**: a night of a quiet camera can shrink from hours to a few minutes of events.
- 🕒 **Ignores what is not motion**: the camera's on-screen clock, sensor noise and small flicker do not trigger events; whole-picture changes (lights switching, IR mode) are ignored too.
- 🎯 **Tunable**: sensitivity presets, minimum object size, ignore regions (trees, flags, a busy road), minimum event length and merge gap.
- 🧾 **Evidence-friendly**: each exported frame shows its **source time**, and the JSON timeline lists exact start and end seconds of every event.
- 📦 **Bulk and automated**: many files per run; schedule it every morning on the night's exports or trigger it from your storage via webhook.
- 🧩 **No software to install**: no VMS licence, no FFmpeg or OpenCV setup. Video URLs in, timeline and videos out.

### Who uses it

| Sector | Typical use |
|---|---|
| **Security companies & monitoring centres** | Review client cameras after an incident, deliver only the relevant footage |
| **Condominiums, schools & retail** | Find when someone entered a room, garage or stockroom without watching the whole night |
| **Insurance & loss prevention** | Extract the moments that matter from claim footage |
| **Investigations & legal** | Build a timeline from long recordings, with traceable timestamps |
| **Wildlife & research cameras** | Keep only the clips where animals appear |
| **Construction & facilities** | Summarize site cameras for daily reports |
| **AI teams** | Pre-filter footage before running detection or tracking models (less GPU time) |

### How it works

1. The video is decoded at **5 frames per second** in low resolution (fast and cheap, enough for motion).
2. Each frame is compared with the previous one; small noise is removed and the **largest moving object** is measured as a % of the picture.
3. Samples above the **sensitivity** threshold become motion; whole-picture changes (> 60% of the frame) are treated as lighting changes and ignored.
4. Nearby motions are **merged**, very short ones are dropped, and **pre/post roll** seconds are added.
5. Events are exported from the **original video** (thumbnail, clip and summary), downscaled to 720p by default, with the source time written in the corner.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| **Video URLs** (`videoUrls`) | array | — | CCTV/NVR exports (MP4, MKV, AVI, MOV, TS…), up to 4 GB each. Google Drive and Dropbox links work |
| **Sensitivity** (`sensitivity`) | string | `medium` | `high` (small or distant objects), `medium` (people and vehicles), `low` (large, close movements) |
| **Custom minimum object size** (`minMotionPercent`) | string | — | Overrides sensitivity, in % of the frame (high = 0.08, medium = 0.2, low = 0.6) |
| **Ignore regions** (`ignoreRegions`) | array | — | Rectangles in % of the picture, e.g. `[{"x":0,"y":0,"width":40,"height":8}]` |
| **Minimum event length** (`minEventSeconds`) | string | `0.6` | Drop shorter motions |
| **Merge events closer than** (`mergeGapSeconds`) | string | `3` | Join motions separated by less than this |
| **Seconds before / after** (`preRollSeconds`, `postRollSeconds`) | string | `2` / `2` | Context kept around each event |
| **Create summary video** (`createSummaryVideo`) | boolean | `true` | One MP4 with every event back to back |
| **Create one clip per event** (`createClips`) | boolean | `false` | Separate MP4 per event |
| **Create thumbnails** (`createThumbnails`) | boolean | `true` | JPEG at the peak of each event |
| **Show source time on video** (`burnTimestamp`) | boolean | `true` | Original position written on exported frames |
| **Export resolution** (`maxHeight`) | integer | `720` | Max height of exported videos |
| **Max minutes per video** (`maxDurationMinutes`) | integer | `720` | Scan and bill only the first N minutes |
| **Max events per video** (`maxEventsPerVideo`) | integer | `300` | Safety limit for very busy scenes |

Example: a garage camera where a road in the top third of the picture should be ignored:

```json
{
  "videoUrls": ["https://example.com/nvr/garage-2026-10-02.mp4"],
  "sensitivity": "medium",
  "ignoreRegions": [{ "x": 0, "y": 0, "width": 100, "height": 33 }],
  "createClips": true
}
```

### Output

One dataset row per video:

```json
{
  "sourceUrl": "https://example.com/nvr/garage-2026-10-02.mp4",
  "durationSeconds": 51.0,
  "scannedSeconds": 51.0,
  "eventsFound": 2,
  "motionSeconds": 16.8,
  "timeSavedPercent": 67.1,
  "summaryVideoUrl": "https://api.apify.com/v2/key-value-stores/.../records/video001-motion-summary.mp4",
  "events": [
    {
      "event": 1, "startSeconds": 17.8, "endSeconds": 27.6, "durationSeconds": 9.8,
      "startTime": "00:00:17.80", "endTime": "00:00:27.60",
      "peakMotionPercent": 4.02, "peakAtSeconds": 20.0,
      "thumbnailUrl": "https://api.apify.com/v2/key-value-stores/.../records/video001-event0001.jpg",
      "clipUrl": "https://api.apify.com/v2/key-value-stores/.../records/video001-clip0001.mp4"
    }
  ],
  "truncated": false,
  "billedMinutes": 1
}
```

`peakMotionPercent` is the size of the largest moving object at the peak, in % of the picture: use it to tune **Custom minimum object size**. Failed videos return an `error` field and are free.

### Pricing

Pay per **started minute of video scanned** (exports included), plus a tiny per-run start fee. No subscription. Failed videos are free. Apify plan discounts apply automatically (see the **Pricing** tab).

| Example | Minutes billed |
|---|---|
| 51-second test clip | 1 |
| 1 hour of a parking-lot camera | 60 |
| 12-hour night recording | 720 |

**Speed:** on the default 4 GB of memory a 20-minute 720p recording was scanned and exported in about 4 minutes. Give the run more memory (more CPU) for long recordings, and raise the run **timeout** (Run options) for files of many hours.

### Use it from code or an AI agent

**Python: get the events of last night's export**

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("adorable_partial/cctv-motion-clipper").call(
    run_input={"videoUrls": ["https://example.com/nvr/cam01-night.mp4"], "sensitivity": "high"},
    timeout_secs=4 * 3600,
)
for video in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(video["eventsFound"], "events, summary:", video["summaryVideoUrl"])
    for e in video["events"]:
        print(e["startTime"], "→", e["endTime"], e["thumbnailUrl"])
```

**JavaScript**

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('adorable_partial/cctv-motion-clipper').call({
    videoUrls: ['https://example.com/nvr/entrance.mp4'],
    createClips: true,
});
const [video] = (await client.dataset(run.defaultDatasetId).listItems()).items;
console.log(video.timeSavedPercent, video.events.length);
```

**No-code and agents:** schedule a daily run, send the summary link to Slack or e-mail with **Make**, **Zapier** or **n8n**, or let an AI agent call it through the **Apify MCP server**.

### FAQ

**My camera has a clock or logo on screen. Will it trigger events?** Usually not: the clock changes only a tiny area. If it is large, add it to **Ignore regions**.

**Trees, rain or a busy road create too many events.** Add those areas to **Ignore regions**, use **Low** sensitivity, or raise **Minimum event length**.

**It missed a person far away.** Use **High** sensitivity or set **Custom minimum object size** to about half the `peakMotionPercent` you see for similar events.

**Does it detect *what* moved (person, car, animal)?** No, it detects motion. Run the thumbnails or summary through a detector if you need classes, or anonymize faces and plates before sharing with the related Actor below.

**Is the footage kept?** Only in your own Apify storage for the run, following your account's data-retention settings. Nothing is sent to third parties.

**Which formats?** Any common container and codec (H.264, H.265/HEVC, MJPEG, MPEG-4…). Proprietary NVR formats must be exported to MP4/AVI first. Outputs are H.264 MP4 at 15 fps.

### Related Actors

- [Image & Video Anonymizer](https://apify.com/adorable_partial/image-anonymizer) — blur faces and license plates before sharing footage (GDPR / LGPD)
- [Video Optimizer](https://apify.com/adorable_partial/video-optimizer) — shrink CCTV archives (5 fps, 720p, 90%+ smaller)
- [Video Frame Extractor for AI](https://apify.com/adorable_partial/video-frame-extractor-ai) — frames for datasets and inspection
- [Face Compare](https://apify.com/adorable_partial/face-compare) — face verification and embeddings

### Support

A camera that triggers too much or too little? Open an issue in the **Issues** tab with the settings you used.

### Need a different tool? Request it

Missing a feature, an input or output format, or a whole different tool for your workflow? **Open an issue in the Issues tab** and describe what you need:

- what goes in (files, URLs, data) and what should come out,
- roughly how much you process (per day or month),
- one example, if you can share it.

Requests with a real use case get built, either as a new option in this Actor or as a new Actor in [this collection](https://apify.com/adorable_partial), and you get notified in the issue when it's live. Every request is read.

# Actor input Schema

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

Security-camera / NVR exports (MP4, MKV, AVI, MOV, TS...), up to 4 GB each. Public Google Drive and Dropbox links work.

## `sensitivity` (type: `string`):

How big a moving object must be to count as motion.

## `minMotionPercent` (type: `string`):

Overrides Sensitivity: smallest moving area that counts, as % of the picture (e.g. 0.1). Every event's peak value is in the output to help you tune it.

## `ignoreRegions` (type: `array`):

Areas to ignore, in % of the picture: \[{"x":0,"y":0,"width":40,"height":8}] for a clock overlay at the top-left, or trees, flags and busy streets. The on-screen clock is usually ignored automatically because it is small.

## `minEventSeconds` (type: `string`):

Ignore motion shorter than this (filters single-frame flicker).

## `mergeGapSeconds` (type: `string`):

Two motions separated by less than this become one event.

## `preRollSeconds` (type: `string`):

Extra video kept before motion starts.

## `postRollSeconds` (type: `string`):

Extra video kept after motion ends.

## `createSummaryVideo` (type: `boolean`):

One MP4 with all motion events back to back.

## `createClips` (type: `boolean`):

Also store each event as its own MP4.

## `createThumbnails` (type: `boolean`):

A JPEG of the moment with most motion in each event.

## `burnTimestamp` (type: `boolean`):

Write the original position (hh:mm:ss) in the corner of exported videos, so every frame can be traced back.

## `maxHeight` (type: `integer`):

Exported clips are downscaled to this height (smaller files). Analysis is not affected.

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

Only the first N minutes of each video are scanned and billed.

## `maxEventsPerVideo` (type: `integer`):

Stop exporting after this many events (very busy scenes).

## Actor input object example

```json
{
  "videoUrls": [
    "https://api.apify.com/v2/key-value-stores/RL7JMSU2StgBzcpmV/records/sample-cctv.mp4?signature=1bvR1Dt2o6BOlw597bND6"
  ],
  "sensitivity": "medium",
  "minEventSeconds": "0.6",
  "mergeGapSeconds": "3",
  "preRollSeconds": "2",
  "postRollSeconds": "2",
  "createSummaryVideo": true,
  "createClips": false,
  "createThumbnails": true,
  "burnTimestamp": true,
  "maxHeight": 720,
  "maxDurationMinutes": 720,
  "maxEventsPerVideo": 300
}
```

# Actor output Schema

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

One row per video: events with start/end times, thumbnails, clip links and the summary video.

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

Summary videos, clips and thumbnails.

# 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/RL7JMSU2StgBzcpmV/records/sample-cctv.mp4?signature=1bvR1Dt2o6BOlw597bND6"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("adorable_partial/cctv-motion-clipper").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/RL7JMSU2StgBzcpmV/records/sample-cctv.mp4?signature=1bvR1Dt2o6BOlw597bND6"] }

# Run the Actor and wait for it to finish
run = client.actor("adorable_partial/cctv-motion-clipper").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/RL7JMSU2StgBzcpmV/records/sample-cctv.mp4?signature=1bvR1Dt2o6BOlw597bND6"
  ]
}' |
apify call adorable_partial/cctv-motion-clipper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adorable_partial/cctv-motion-clipper"
        }
    }
}
```

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/FHr2Ccmg0wTNdO94r/builds/c1daRy8f22J64rvh4/openapi.json
