Skip to content

Instantly share code, notes, and snippets.

@firedynasty
Created June 21, 2026 18:28
Show Gist options
  • Select an option

  • Save firedynasty/edb6bf30c7e930b3a9c9ffb711659ae0 to your computer and use it in GitHub Desktop.

Select an option

Save firedynasty/edb6bf30c7e930b3a9c9ffb711659ae0 to your computer and use it in GitHub Desktop.
Trim and Concatenate Videos

Trim Video - macOS Automator Action

Trims video files using ffmpeg with interactive start/end time prompts. Designed to be used as a macOS Automator "Run AppleScript" action that accepts files.

Usage

  1. Create a new Automator workflow (Quick Action or Application)
  2. Add a "Run AppleScript" action
  3. Paste the script below
  4. Select video files and run — you'll be prompted for start and end times

Script

on run {input, parameters}
    if (count of input) > 0 then
        set startTime to text returned of (display dialog "Start time (MM:SS):" default answer "0:00")
        set endTime to text returned of (display dialog "End time (MM:SS or blank for rest):" default answer "")
        
        set shellScript to ""
        
        repeat with selectedFile in input
            set filePath to POSIX path of selectedFile
            
            -- Generate output path with _1, _2, etc. suffix
            set outputPath to do shell script "f=" & quoted form of filePath & "; name=\"${f%.*}\"; ext=\"${f##*.}\"; n=1; while [ -e \"${name}_${n}.${ext}\" ]; do n=$((n+1)); done; echo \"${name}_${n}.${ext}\""
            
            if endTime is not "" then
                set shellScript to shellScript & "ffmpeg -ss " & startTime & " -to " & endTime & " -i " & quoted form of filePath & " -c copy " & quoted form of outputPath & ";" & linefeed
            else
                set shellScript to shellScript & "ffmpeg -ss " & startTime & " -i " & quoted form of filePath & " -c copy " & quoted form of outputPath & ";" & linefeed
            end if
        end repeat
        
        tell application "Terminal"
            activate
            do script shellScript in front window
        end tell
    else
        display dialog "No files were selected." buttons {"OK"} default button "OK"
    end if
end run

Concatenate Videos - macOS Automator Action

Concatenates multiple video files with 0.5s black gap transitions using ffmpeg with hardware acceleration. Designed to be used as a macOS Automator "Run AppleScript" action that accepts files.

Usage

  1. Create a new Automator workflow (Quick Action or Application)
  2. Add a "Run AppleScript" action
  3. Paste the script below
  4. Select 2+ video files and run — output is saved alongside the first file with a _concat_N suffix

Features

  • Hardware-accelerated encoding via VideoToolbox (h264_videotoolbox)
  • Automatic 0.5s black gap between clips
  • Handles mixed codecs (e.g. HEVC + H.264) by decoding to raw frames
  • Auto-detects resolution and frame rate from the first clip
  • Unique output naming to avoid overwrites

Script

on run {input, parameters}
    if (count of input) > 1 then
        set ts to do shell script "date +%s"
        set scriptFile to "/tmp/ffconcat_" & ts & ".sh"
        
        -- Get video info from first clip
        set firstPath to POSIX path of (item 1 of input)
        set vidInfo to do shell script "/opt/homebrew/bin/ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate -of csv=p=0 " & quoted form of firstPath
        set vidWidth to do shell script "echo " & quoted form of vidInfo & " | cut -d',' -f1"
        set vidHeight to do shell script "echo " & quoted form of vidInfo & " | cut -d',' -f2"
        set vidFps to do shell script "echo " & quoted form of vidInfo & " | cut -d',' -f3"
        
        set fileCount to count of input
        
        -- Generate unique output path
        set outputPath to do shell script "f=" & quoted form of firstPath & "; name=\"${f%.*}\"; n=1; while [ -e \"${name}_concat_${n}.mp4\" ]; do n=$((n+1)); done; echo \"${name}_concat_${n}.mp4\""
        
        -- Build shell script content
        set NL to linefeed
        set scriptContent to "#!/bin/bash" & NL
        set scriptContent to scriptContent & "/opt/homebrew/bin/ffmpeg \\" & NL
        set scriptContent to scriptContent & "  -hwaccel videotoolbox \\" & NL
        
        -- Input files
        repeat with selectedFile in input
            set filePath to POSIX path of selectedFile
            set scriptContent to scriptContent & "  -i '" & filePath & "' \\" & NL
        end repeat
        
        -- Build filter_complex: gap sources + concat
        -- Using filter_complex avoids codec mismatch (HEVC source vs H.264 gap)
        -- because each input is decoded to raw frames before concatenation
        set fc to ""
        repeat with g from 0 to (fileCount - 2)
            set fc to fc & "color=c=black:s=" & vidWidth & "x" & vidHeight & ":r=" & vidFps & ":d=0.5,setpts=PTS-STARTPTS[gv" & g & "];"
            set fc to fc & "anullsrc=r=44100:cl=stereo,atrim=duration=0.5,asetpts=PTS-STARTPTS[ga" & g & "];"
        end repeat
        
        -- Interleave clips and gaps: [0:v][0:a][gv0][ga0][1:v][1:a]...
        repeat with i from 1 to fileCount
            set fc to fc & "[" & (i - 1) & ":v][" & (i - 1) & ":a]"
            if i < fileCount then
                set fc to fc & "[gv" & (i - 1) & "][ga" & (i - 1) & "]"
            end if
        end repeat
        
        set totalSegs to fileCount + fileCount - 1
        set fc to fc & "concat=n=" & totalSegs & ":v=1:a=1[v][a]"
        
        set scriptContent to scriptContent & "  -filter_complex '" & fc & "' \\" & NL
        set scriptContent to scriptContent & "  -map '[v]' -map '[a]' \\" & NL
        set scriptContent to scriptContent & "  -c:v h264_videotoolbox -b:v 8000k -c:a aac \\" & NL
        set scriptContent to scriptContent & "  '" & outputPath & "'" & NL
        set scriptContent to scriptContent & "rm -f '" & scriptFile & "'" & NL
        
        -- Write script via Python to safely handle special chars in paths
        do shell script "python3 -c \"import sys; open(sys.argv[1], 'w').write(sys.argv[2])\" " & quoted form of scriptFile & " " & quoted form of scriptContent
        do shell script "chmod +x " & quoted form of scriptFile
        
        tell application "Terminal"
            activate
            do script "bash " & quoted form of scriptFile in front window
        end tell
    else
        display dialog "Select 2 or more videos to concatenate." buttons {"OK"} default button "OK"
    end if
end run

Requirements

  • ffmpeg installed via Homebrew (/opt/homebrew/bin/ffmpeg)
  • macOS with Automator
  • Apple Silicon Mac (for VideoToolbox hardware acceleration)

Bulk Trim Video - macOS Automator Action

Bulk trims a video into multiple clips using a timestamps file. Each line in the text file defines a clip with start,end times in MM:SS format. Outputs are saved to a dated subfolder.

Usage

  1. Create a new Automator workflow (Quick Action or Application)
  2. Add a "Run AppleScript" action
  3. Paste the script below
  4. Select video file(s) and run — you'll be prompted to choose a timestamps .txt file

Timestamps File Format

Each line: start,end in MM:SS format. Leave end blank to trim to the end of the video.

0:30,1:15
2:00,2:45
3:10,

Script

on run {input, parameters}
    if (count of input) > 0 then
        set txtFile to POSIX path of (choose file with prompt "Select timestamps file:" of type {"txt"})
        
        set shellScript to ""
        
        repeat with selectedFile in input
            set filePath to POSIX path of selectedFile
            
            set shellScript to shellScript & "f=" & quoted form of filePath & "; " & ¬
                "ext=\"${f##*.}\"; " & ¬
                "basename=\"${f##*/}\"; basename=\"${basename%.*}\"; " & ¬
                "dir=\"${f%/*}\"; " & ¬
                "dateStr=$(date +%m%d); " & ¬
                "outDir=\"${dir}/${dateStr}-${basename}\"; " & ¬
                "mkdir -p \"$outDir\"; " & ¬
                "n=1; " & ¬
                "while IFS=, read -r startTime endTime || [ -n \"$startTime\" ]; do " & ¬
                "while [ -e \"${outDir}/clip_${n}.${ext}\" ]; do n=$((n+1)); done; " & ¬
                "startTime=$(echo \"$startTime\" | tr -d '[:space:]'); " & ¬
                "endTime=$(echo \"$endTime\" | tr -d '[:space:]'); " & ¬
                "sSecs=$(echo \"$startTime\" | awk -F: '{print $1*60+$2}'); " & ¬
                "if [ -n \"$endTime\" ]; then " & ¬
                "eSecs=$(echo \"$endTime\" | awk -F: '{print $1*60+$2}'); " & ¬
                "dur=$((eSecs - sSecs)); " & ¬
                "ffmpeg -nostdin -ss \"$sSecs\" -i " & quoted form of filePath & " -t \"$dur\" -c:v hevc_videotoolbox -c:a copy \"${outDir}/clip_${n}.${ext}\"; " & ¬
                "else " & ¬
                "ffmpeg -nostdin -ss \"$sSecs\" -i " & quoted form of filePath & " -c:v hevc_videotoolbox -c:a copy \"${outDir}/clip_${n}.${ext}\"; " & ¬
                "fi; " & ¬
                "n=$((n+1)); " & ¬
                "done < " & quoted form of txtFile & linefeed
        end repeat
        
        tell application "Terminal"
            activate
            do script shellScript in front window
        end tell
    else
        display dialog "No files were selected." buttons {"OK"} default button "OK"
    end if
end run

Requirements

  • ffmpeg installed and available in PATH
  • macOS with Automator
  • Apple Silicon Mac (for HEVC VideoToolbox hardware encoding)

Post-Processing: Downscale with HandBrake

After trimming/concatenating, use HandBrake to downscale to a standard resolution:

  • 1080p30 — Resolution: 1920x1080, Frame Rate: 30 fps (constant)
  • 720p30 — Resolution: 1280x720, Frame Rate: 30 fps (constant)

HandBrake is free, open source, and works on macOS, Windows, and Linux.

Recommended Settings

  1. Open trimmed video in HandBrake
  2. Dimensions tab → set width to 1920 (1080p) or 1280 (720p), keep aspect ratio
  3. Video tab → Frame Rate: 30, select Constant Framerate
  4. Encoder: H.265 (VideoToolbox) on Mac or H.265 (NVEnc) on Windows
  5. Quality: RF 22–24 for a good size/quality balance

HandBrake CLI (batch processing)

# macOS/Linux — downscale all mp4s in a folder to 1080p30
for f in *.mp4; do
  HandBrakeCLI -i "$f" -o "${f%.*}_1080p.mp4" \
    --width 1920 --rate 30 --cfr \
    --encoder vt_h265 --quality 22
done
# Windows — downscale all mp4s in a folder to 1080p30
Get-ChildItem *.mp4 | ForEach-Object {
    $out = $_.BaseName + "_1080p.mp4"
    HandBrakeCLI -i $_.FullName -o $out `
        --width 1920 --rate 30 --cfr `
        --encoder nvenc_h265 --quality 22
}

Replace --width 1920 with --width 1280 for 720p. Replace --quality 22 with a higher number (e.g., 26) for smaller file size.


Windows Alternative: LosslessCut

The scripts above are macOS-only (Automator + AppleScript). For Windows users, LosslessCut is a free, open-source GUI app that covers all three workflows:

  • Trim — set start/end points and export segments (lossless, no re-encode)
  • Bulk trim — mark multiple segments in one video and batch export them all
  • Concatenate — merge multiple files with drag-and-drop ordering

Uses ffmpeg under the hood. Also works on Mac and Linux.


Windows Alternative: PowerShell Scripts

If you prefer a script-based approach on Windows, install ffmpeg for Windows and use these PowerShell equivalents. Save as .ps1 files and run them, or add them to the right-click "Send To" folder.

For hardware encoding, replace videotoolbox with your GPU's encoder:

  • NVIDIA: h264_nvenc / hevc_nvenc
  • Intel: h264_qsv / hevc_qsv
  • AMD: h264_amf / hevc_amf

Trim Video

# trim_video.ps1 — Drag and drop video files onto this script
param([Parameter(ValueFromRemainingArguments)]$files)

$startTime = Read-Host "Start time (MM:SS)"
$endTime = Read-Host "End time (MM:SS, blank for rest)"

foreach ($file in $files) {
    $dir = Split-Path $file
    $name = [System.IO.Path]::GetFileNameWithoutExtension($file)
    $ext = [System.IO.Path]::GetExtension($file)
    
    $n = 1
    do { $out = Join-Path $dir "${name}_${n}${ext}"; $n++ } while (Test-Path $out)
    
    if ($endTime) {
        ffmpeg -ss $startTime -to $endTime -i $file -c copy $out
    } else {
        ffmpeg -ss $startTime -i $file -c copy $out
    }
}
Read-Host "Press Enter to exit"

Concatenate Videos

# concat_videos.ps1 — Drag and drop 2+ video files onto this script
param([Parameter(ValueFromRemainingArguments)]$files)

if ($files.Count -lt 2) { Write-Host "Select 2 or more videos."; Read-Host; exit }

$first = $files[0]
$dir = Split-Path $first
$name = [System.IO.Path]::GetFileNameWithoutExtension($first)

$n = 1
do { $out = Join-Path $dir "${name}_concat_${n}.mp4"; $n++ } while (Test-Path $out)

# Probe first video for resolution and fps
$info = ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate -of csv=p=0 $first
$parts = $info -split ","
$w = $parts[0]; $h = $parts[1]; $fps = $parts[2]

$inputs = ($files | ForEach-Object { "-i `"$_`"" }) -join " "
$fileCount = $files.Count

# Build filter_complex: black gaps between clips
$fc = ""
for ($g = 0; $g -lt ($fileCount - 1); $g++) {
    $fc += "color=c=black:s=${w}x${h}:r=${fps}:d=0.5,setpts=PTS-STARTPTS[gv${g}];"
    $fc += "anullsrc=r=44100:cl=stereo,atrim=duration=0.5,asetpts=PTS-STARTPTS[ga${g}];"
}
for ($i = 0; $i -lt $fileCount; $i++) {
    $fc += "[${i}:v][${i}:a]"
    if ($i -lt ($fileCount - 1)) { $fc += "[gv${i}][ga${i}]" }
}
$totalSegs = $fileCount + $fileCount - 1
$fc += "concat=n=${totalSegs}:v=1:a=1[v][a]"

$cmd = "ffmpeg $inputs -filter_complex `"$fc`" -map `"[v]`" -map `"[a]`" -c:v h264_nvenc -b:v 8000k -c:a aac `"$out`""
Invoke-Expression $cmd
Read-Host "Press Enter to exit"

Bulk Trim Video

# bulk_trim.ps1 — Drag and drop video files onto this script
param([Parameter(ValueFromRemainingArguments)]$files)

Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.Filter = "Text files (*.txt)|*.txt"
$dialog.Title = "Select timestamps file"
if ($dialog.ShowDialog() -ne "OK") { exit }
$txtFile = $dialog.FileName

foreach ($file in $files) {
    $dir = Split-Path $file
    $name = [System.IO.Path]::GetFileNameWithoutExtension($file)
    $ext = [System.IO.Path]::GetExtension($file)
    $dateStr = Get-Date -Format "MMdd"
    $outDir = Join-Path $dir "${dateStr}-${name}"
    New-Item -ItemType Directory -Force -Path $outDir | Out-Null
    
    $n = 1
    foreach ($line in Get-Content $txtFile) {
        $parts = $line -split ","
        $startTime = $parts[0].Trim()
        $endTime = if ($parts.Count -gt 1) { $parts[1].Trim() } else { "" }
        
        do { $out = Join-Path $outDir "clip_${n}${ext}"; $n++ } while (Test-Path $out)
        
        # Convert MM:SS to seconds
        $sParts = $startTime -split ":"
        $sSecs = [int]$sParts[0] * 60 + [int]$sParts[1]
        
        if ($endTime) {
            $eParts = $endTime -split ":"
            $eSecs = [int]$eParts[0] * 60 + [int]$eParts[1]
            $dur = $eSecs - $sSecs
            ffmpeg -nostdin -ss $sSecs -i $file -t $dur -c:v hevc_nvenc -c:a copy $out
        } else {
            ffmpeg -nostdin -ss $sSecs -i $file -c:v hevc_nvenc -c:a copy $out
        }
    }
}
Read-Host "Press Enter to exit"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment