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.
- Create a new Automator workflow (Quick Action or Application)
- Add a "Run AppleScript" action
- Paste the script below
- Select video files and run — you'll be prompted for start and end times
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 runConcatenates 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.
- Create a new Automator workflow (Quick Action or Application)
- Add a "Run AppleScript" action
- Paste the script below
- Select 2+ video files and run — output is saved alongside the first file with a
_concat_Nsuffix
- 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
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- ffmpeg installed via Homebrew (
/opt/homebrew/bin/ffmpeg) - macOS with Automator
- Apple Silicon Mac (for VideoToolbox hardware acceleration)
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.
- Create a new Automator workflow (Quick Action or Application)
- Add a "Run AppleScript" action
- Paste the script below
- Select video file(s) and run — you'll be prompted to choose a timestamps
.txtfile
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,
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- ffmpeg installed and available in PATH
- macOS with Automator
- Apple Silicon Mac (for HEVC VideoToolbox hardware encoding)
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.
- Open trimmed video in HandBrake
- Dimensions tab → set width to
1920(1080p) or1280(720p), keep aspect ratio - Video tab → Frame Rate:
30, selectConstant Framerate - Encoder:
H.265 (VideoToolbox)on Mac orH.265 (NVEnc)on Windows - Quality: RF 22–24 for a good size/quality balance
# 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.
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.
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.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"# 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.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"