Skip to content

Instantly share code, notes, and snippets.

@miku1958
Last active March 7, 2026 02:36
Show Gist options
  • Select an option

  • Save miku1958/67e59d05d1be70b206ea86c97405dcef to your computer and use it in GitHub Desktop.

Select an option

Save miku1958/67e59d05d1be70b206ea86c97405dcef to your computer and use it in GitHub Desktop.
ffmpeg.bat
@echo off
chcp 65001 > nul
title Video Transcode Tool
cd /D "%~dp0"
for %%F in (%*) do call :main "%%~F"
pause
goto :eof
:main
setlocal EnableDelayedExpansion
rem ───── probe video info ─────────────────────
for /f "delims=" %%H in ('ffprobe -v error -select_streams v:0 -show_entries stream^=height -of default^=nokey^=1:noprint_wrappers^=1 "%~1"') do set "SRC_HEIGHT=%%H"
for /f "delims=" %%R in ('ffprobe -v error -select_streams v:0 -show_entries stream^=r_frame_rate -of default^=nokey^=1:noprint_wrappers^=1 "%~1"') do set "SRC_FPS_RAW=%%R"
set "FPS_NUM=!SRC_FPS_RAW!"
set "FPS_DEN=1"
for /f "tokens=1,2 delims=/" %%A in ("!SRC_FPS_RAW!") do (
set "FPS_NUM=%%A"
if not "%%B"=="" set "FPS_DEN=%%B"
)
set /a "FPS_HALF=!FPS_DEN! / 2"
set /a "SRC_FPS=(!FPS_NUM! + !FPS_HALF!) / !FPS_DEN!"
for /f "delims=" %%D in ('ffprobe -v error -show_entries format^=duration -of default^=nokey^=1:noprint_wrappers^=1 "%~1"') do set "SRC_DUR_RAW=%%D"
for /f "tokens=1 delims=." %%S in ("!SRC_DUR_RAW!") do set "SRC_DUR_SEC=%%S"
set "SRC_AUDIO_KBPS=128"
for /f "delims=" %%B in ('ffprobe -v error -select_streams a:0 -show_entries stream^=bit_rate -of default^=nokey^=1:noprint_wrappers^=1 "%~1"') do (
if not "%%B"=="N/A" set /a "SRC_AUDIO_KBPS=%%B / 1000"
)
echo Probed: !SRC_HEIGHT!p / !SRC_FPS!fps / !SRC_DUR_SEC!s
rem ───── write PowerShell GUI script ──────────
set "PS_SCRIPT=%TEMP%\ffmpeg_gui_%RANDOM%.ps1"
set "PS_RESULT=%TEMP%\ffmpeg_gui_%RANDOM%.txt"
> "!PS_SCRIPT!" (
echo Add-Type -AssemblyName System.Windows.Forms
echo Add-Type -AssemblyName System.Drawing
echo [System.Windows.Forms.Application]::EnableVisualStyles^(^)
echo(
echo $f = New-Object System.Windows.Forms.Form
echo $f.Text = 'FFmpeg Transcode'
echo $f.Size = New-Object System.Drawing.Size^(440,550^)
echo $f.StartPosition = 'CenterScreen'
echo $f.FormBorderStyle = 'FixedDialog'
echo $f.MaximizeBox = $false
echo $f.Font = New-Object System.Drawing.Font^('Microsoft YaHei UI',9^)
echo(
echo $y = 12
echo $lblFile = New-Object System.Windows.Forms.Label
echo $lblFile.Location = New-Object System.Drawing.Point^(15,$y^)
echo $lblFile.Size = New-Object System.Drawing.Size^(400,22^)
echo $lblFile.Text = 'File: %~nx1'
echo $f.Controls.Add^($lblFile^)
echo $y += 24
echo $lblInfo = New-Object System.Windows.Forms.Label
echo $lblInfo.Location = New-Object System.Drawing.Point^(15,$y^)
echo $lblInfo.Size = New-Object System.Drawing.Size^(400,22^)
echo $lblInfo.Text = 'Source: !SRC_HEIGHT!p / !SRC_FPS!fps / !SRC_DUR_SEC!s'
echo $f.Controls.Add^($lblInfo^)
echo $y += 32
echo(
echo function New-ComboRow^($label, [string[]]$items, $default^) {
echo $l = New-Object System.Windows.Forms.Label
echo $l.Location = New-Object System.Drawing.Point^(15,$script:y^)
echo $l.Size = New-Object System.Drawing.Size^(130,26^)
echo $l.Text = $label
echo $l.TextAlign = 'MiddleLeft'
echo $f.Controls.Add^($l^)
echo $c = New-Object System.Windows.Forms.ComboBox
echo $c.DropDownStyle = 'DropDownList'
echo $c.Location = New-Object System.Drawing.Point^(150,$script:y^)
echo $c.Size = New-Object System.Drawing.Size^(250,26^)
echo foreach^($i in $items^){[void]$c.Items.Add^($i^)}
echo $idx = [Array]::IndexOf^($items, $default^)
echo if^($idx -ge 0^){$c.SelectedIndex=$idx}else{$c.SelectedIndex=$c.Items.Count-1}
echo $f.Controls.Add^($c^)
echo $script:y += 36
echo return $c
echo }
echo(
echo function New-TextRow^($label, $default^) {
echo $l = New-Object System.Windows.Forms.Label
echo $l.Location = New-Object System.Drawing.Point^(15,$script:y^)
echo $l.Size = New-Object System.Drawing.Size^(130,26^)
echo $l.Text = $label
echo $l.TextAlign = 'MiddleLeft'
echo $f.Controls.Add^($l^)
echo $t = New-Object System.Windows.Forms.TextBox
echo $t.Location = New-Object System.Drawing.Point^(150,$script:y^)
echo $t.Size = New-Object System.Drawing.Size^(250,26^)
echo $t.Text = $default
echo $f.Controls.Add^($t^)
echo $script:y += 36
echo return $t
echo }
echo(
echo $srcH = !SRC_HEIGHT!
echo $srcF = !SRC_FPS!
echo(
echo $resItems = @^(^)
echo $resDefault = 'keep'
echo foreach^($r in @^(2160,1440,1080,720^)^){if^($r -le $srcH^){$resItems += "$($r)p"}; if^($r -eq $srcH^){$resDefault = "$($r)p"}}
echo if^($resDefault -eq 'keep'^){$resItems += 'keep'}
echo $cbRes = New-ComboRow 'Resolution' $resItems $resDefault
echo(
echo $fpsItems = @^(^)
echo $fpsDefault = 'keep'
echo foreach^($fp in @^(240,120,60,30,24^)^){if^($fp -le $srcF^){$fpsItems += "$($fp)fps"}; if^($fp -eq $srcF^){$fpsDefault = "$($fp)fps"}}
echo if^($fpsDefault -eq 'keep'^){$fpsItems += 'keep'}
echo $cbFps = New-ComboRow 'FPS' $fpsItems $fpsDefault
echo(
echo $cbCodec = New-ComboRow 'Codec' @^('H.264 ^(libx264^)','H.265 ^(libx265^)'^) 'H.264 ^(libx264^)'
echo $tbCrf = New-TextRow 'CRF' '23'
echo $cbDedup = New-ComboRow 'Dedup' @^('None','Dedup ^(CFR, shorter^)','Dedup ^(VFR^)'^) 'None'
echo $cbAudio = New-ComboRow 'Audio' @^('Keep audio','Remove audio'^) 'Keep audio'
echo $tbTarget = New-TextRow 'Target MB' ''
echo(
echo $lSpd = New-Object System.Windows.Forms.Label
echo $lSpd.Location = New-Object System.Drawing.Point^(15,$script:y^)
echo $lSpd.Size = New-Object System.Drawing.Size^(130,26^)
echo $lSpd.Text = 'Speed'
echo $lSpd.TextAlign = 'MiddleLeft'
echo $f.Controls.Add^($lSpd^)
echo $tbSpd = New-Object System.Windows.Forms.TextBox
echo $tbSpd.Location = New-Object System.Drawing.Point^(150,$script:y^)
echo $tbSpd.Size = New-Object System.Drawing.Size^(60,26^)
echo $tbSpd.Text = '1.0'
echo $f.Controls.Add^($tbSpd^)
echo $slSpd = New-Object System.Windows.Forms.TrackBar
echo $slSpd.Location = New-Object System.Drawing.Point^(220,$script:y^)
echo $slSpd.Size = New-Object System.Drawing.Size^(185,45^)
echo $slSpd.Minimum = 1
echo $slSpd.Maximum = 100
echo $slSpd.Value = 10
echo $slSpd.TickFrequency = 10
echo $slSpd.SmallChange = 1
echo $slSpd.LargeChange = 10
echo $f.Controls.Add^($slSpd^)
echo $script:y += 50
echo $slSpd.Add_ValueChanged^({
echo $tbSpd.Text = ^($slSpd.Value / 10^).ToString^('0.0'^)
echo }^)
echo $tbSpd.Add_Leave^({
echo $v = 0.0
echo if^([double]::TryParse^($tbSpd.Text, [ref]$v^)^){
echo if^($v -lt 0.1^){$v = 0.1}
echo if^($v -gt 10.0^){$v = 10.0}
echo $v = [Math]::Round^($v, 1^)
echo $slSpd.Value = [int]^($v * 10^)
echo $tbSpd.Text = $v.ToString^('0.0'^)
echo }
echo }^)
echo(
echo $script:y += 8
echo $btnOk = New-Object System.Windows.Forms.Button
echo $btnOk.Text = 'Start'
echo $btnOk.Size = New-Object System.Drawing.Size^(130,38^)
echo $btnOk.Location = New-Object System.Drawing.Point^(80,$script:y^)
echo $btnOk.DialogResult = 'OK'
echo $f.Controls.Add^($btnOk^)
echo $f.AcceptButton = $btnOk
echo(
echo $btnNo = New-Object System.Windows.Forms.Button
echo $btnNo.Text = 'Cancel'
echo $btnNo.Size = New-Object System.Drawing.Size^(130,38^)
echo $btnNo.Location = New-Object System.Drawing.Point^(230,$script:y^)
echo $btnNo.DialogResult = 'Cancel'
echo $f.Controls.Add^($btnNo^)
echo $f.CancelButton = $btnNo
echo(
echo $dlgResult = $f.ShowDialog^(^)
echo if^($dlgResult -ne 'OK'^){ 'CANCEL' ^| Out-File -Encoding ascii '!PS_RESULT!'; exit }
echo(
echo $res = $cbRes.SelectedItem; if^($res -eq 'keep'^){$res='keep'}else{$res=$res-replace'p',''}
echo $fps = $cbFps.SelectedItem; if^($fps -eq 'keep'^){$fps='keep'}else{$fps=$fps-replace'fps',''}
echo $codec = if^($cbCodec.SelectedIndex -eq 1^){'libx265'}else{'libx264'}
echo $codec = $codec ^| Select-Object -First 1
echo $crf = $tbCrf.Text; if^(-not $crf^){$crf='23'}
echo $dedup = $cbDedup.SelectedIndex
echo $audio = $cbAudio.SelectedIndex
echo $tgt = $tbTarget.Text; if^(-not $tgt^){$tgt='0'}
echo $spd = $tbSpd.Text; if^(-not $spd^){$spd='1.0'}
echo(
echo "$res^|$fps^|$codec^|$crf^|$dedup^|$audio^|$tgt^|$spd" ^| Out-File -Encoding ascii '!PS_RESULT!'
)
rem ───── run PowerShell GUI ───────────────────
powershell -NoProfile -ExecutionPolicy Bypass -File "!PS_SCRIPT!"
set "GUI_RESULT="
if exist "!PS_RESULT!" (
for /f "usebackq delims=" %%G in ("!PS_RESULT!") do set "GUI_RESULT=%%G"
del "!PS_RESULT!" >nul 2>&1
)
del "!PS_SCRIPT!" >nul 2>&1
if "!GUI_RESULT!"=="CANCEL" (
echo Cancelled.
endlocal
goto :eof
)
if "!GUI_RESULT!"=="" (
echo Error: no GUI result.
endlocal
goto :eof
)
rem ───── parse GUI result ─────────────────────
for /f "tokens=1,2,3,4,5,6,7,8 delims=|" %%A in ("!GUI_RESULT!") do (
set "CHOSEN_RES=%%A"
set "CHOSEN_FPS=%%B"
set "CHOSEN_CODEC=%%C"
set "CHOSEN_CRF=%%D"
set "DEDUP_IDX=%%E"
set "AUDIO_IDX=%%F"
set "TARGET_MB=%%G"
set "CHOSEN_SPEED=%%H"
)
if "!CHOSEN_SPEED!"=="" set "CHOSEN_SPEED=1.0"
set "CODEC_TAG="
if "!CHOSEN_CODEC!"=="libx265" set "CODEC_TAG=-tag:v hvc1"
set "DEDUP_MODE=none"
if "!DEDUP_IDX!"=="1" set "DEDUP_MODE=cfr"
if "!DEDUP_IDX!"=="2" set "DEDUP_MODE=vfr"
rem ───── build -vf filter chain ───────────────
set "VF_CHAIN="
if "!DEDUP_MODE!"=="cfr" set "VF_CHAIN=mpdecimate=hi=300:lo=300:frac=1:max=0,setpts=N/FRAME_RATE/TB"
if "!DEDUP_MODE!"=="vfr" set "VF_CHAIN=mpdecimate=hi=300:lo=300:frac=1:max=0"
if not "!CHOSEN_RES!"=="keep" (
if "!VF_CHAIN!"=="" (
set "VF_CHAIN=scale=-2:!CHOSEN_RES!"
) else (
set "VF_CHAIN=!VF_CHAIN!,scale=-2:!CHOSEN_RES!"
)
)
set "AUDIO_PARAM=-c:a copy"
if "!AUDIO_IDX!"=="1" set "AUDIO_PARAM=-an"
rem ───── apply speed ──────────────────────────
set "AF_PARAM="
set "SPEED_X10=10"
for /f "tokens=1,2 delims=." %%I in ("!CHOSEN_SPEED!") do (
if not "%%J"=="" (set /a "SPEED_X10=%%I * 10 + %%J") else (set /a "SPEED_X10=%%I * 10")
)
if not "!SPEED_X10!"=="10" (
set /a "SPD_INT=!SPEED_X10! / 10"
set /a "SPD_DEC=!SPEED_X10! %% 10"
set "SPEED_VAL=!SPD_INT!.!SPD_DEC!"
if "!VF_CHAIN!"=="" (
set "VF_CHAIN=setpts=PTS/!SPEED_VAL!"
) else (
set "VF_CHAIN=!VF_CHAIN!,setpts=PTS/!SPEED_VAL!"
)
if not "!AUDIO_IDX!"=="1" (
set "ATEMPO_X10=!SPEED_X10!"
set "AF_CHAIN="
call :build_atempo
set "AF_PARAM=-af !AF_CHAIN!"
set "AUDIO_PARAM=-c:a aac -b:a !SRC_AUDIO_KBPS!k"
)
)
set "VF_PARAM="
if not "!VF_CHAIN!"=="" set "VF_PARAM=-vf !VF_CHAIN!"
set "FPS_PARAM="
if not "!CHOSEN_FPS!"=="keep" set "FPS_PARAM=-r !CHOSEN_FPS!"
set "VFR_PARAM="
if "!DEDUP_MODE!"=="vfr" set "VFR_PARAM=-vsync vfr"
rem ───── compute maxrate cap ──────────────────
set "RATE_PARAM="
if not "!TARGET_MB!"=="0" (
set /a "AUDIO_COST=!SRC_AUDIO_KBPS!"
if "!AUDIO_IDX!"=="1" set "AUDIO_COST=0"
set /a "TOTAL_KBPS=!TARGET_MB! * 8192 / !SRC_DUR_SEC!"
set /a "VIDEO_KBPS=!TOTAL_KBPS! - !AUDIO_COST!"
if !VIDEO_KBPS! LEQ 0 set /a "VIDEO_KBPS=100"
set /a "MAXRATE=!VIDEO_KBPS! * 3 / 2"
set /a "BUFSIZE=!MAXRATE! * 2"
set "RATE_PARAM=-maxrate !MAXRATE!k -bufsize !BUFSIZE!k"
)
rem ───── build output filename suffix ─────────
set "SUFFIX="
if not "!CHOSEN_RES!"=="keep" set "SUFFIX=!SUFFIX!_!CHOSEN_RES!p"
if not "!CHOSEN_FPS!"=="keep" set "SUFFIX=!SUFFIX!_!CHOSEN_FPS!fps"
if "!CHOSEN_CODEC!"=="libx265" set "SUFFIX=!SUFFIX!_h265"
if not "!CHOSEN_CRF!"=="23" set "SUFFIX=!SUFFIX!_crf!CHOSEN_CRF!"
if not "!DEDUP_MODE!"=="none" set "SUFFIX=!SUFFIX!_dedup"
if "!DEDUP_MODE!"=="vfr" set "SUFFIX=!SUFFIX!-vfr"
if "!AUDIO_IDX!"=="1" set "SUFFIX=!SUFFIX!_noaudio"
if not "!TARGET_MB!"=="0" set "SUFFIX=!SUFFIX!_!TARGET_MB!mb"
if not "!SPEED_X10!"=="10" (
set /a "SPD_S_INT=!SPEED_X10! / 10"
set /a "SPD_S_DEC=!SPEED_X10! %% 10"
if "!SPD_S_DEC!"=="0" (
set "SUFFIX=!SUFFIX!_!SPD_S_INT!x"
) else (
set "SUFFIX=!SUFFIX!_!SPD_S_INT!.!SPD_S_DEC!x"
)
)
if "!SUFFIX!"=="" set "SUFFIX=_transcoded"
echo.
echo ================================================
echo Res: !CHOSEN_RES!
echo FPS: !CHOSEN_FPS!
echo Codec: !CHOSEN_CODEC!
echo CRF: !CHOSEN_CRF!
echo Dedup: !DEDUP_MODE!
echo Audio: !AUDIO_PARAM!
echo Speed: !CHOSEN_SPEED!x
echo Rate: !RATE_PARAM!
echo Output: %~n1!SUFFIX!.mp4
echo ================================================
echo.
ffmpeg -i "%~1" -c:v !CHOSEN_CODEC! !CODEC_TAG! -crf !CHOSEN_CRF! !RATE_PARAM! !VF_PARAM! !FPS_PARAM! !VFR_PARAM! !AUDIO_PARAM! !AF_PARAM! "%~dpn1!SUFFIX!.mp4"
echo.
echo [done] %~nx1
echo.
endlocal
goto :eof
:build_atempo
if !ATEMPO_X10! LSS 5 (
set "AF_CHAIN=!AF_CHAIN!atempo=0.5,"
set /a "ATEMPO_X10=!ATEMPO_X10! * 2"
goto :build_atempo
)
set /a "AT_INT=!ATEMPO_X10! / 10"
set /a "AT_DEC=!ATEMPO_X10! %% 10"
set "AF_CHAIN=!AF_CHAIN!atempo=!AT_INT!.!AT_DEC!"
goto :eof
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment