首页
关于
壁纸
直播
留言
友链
统计
Search
1
《吞食天地1》金手指代码
8,475 阅读
2
白嫖Emby
7,183 阅读
3
Emby客户端IOS破解
7,167 阅读
4
《三国志英杰传》攻略
6,844 阅读
5
破解emby-server
4,629 阅读
moonjerx
game
age-of-empires
zx3
san-guo-zhi
尼尔:机械纪元
net
emby
learn-video
docker
torrent
photoshop
route
minio
git
ffmpeg
im
vue
gitlab
typecho
svn
alipay
nasm
srs
mail-server
tailscale
kkfileview
aria2
webdav
synology
redis
oray
chemical
mxsite
math
π
x-ui
digital-currency
server
nginx
baota
k8s
http
cloud
linux
shell
database
vpn
esxi
rancher
domain
k3s
ewomail
os
android
windows
ios
app-store
macos
develop
java
javascript
uniapp
nodejs
hbuildx
maven
android-studio
jetbrain
jenkins
css
mybatis
php
python
hardware
hard-disk
pc
RAM
software
pt
calibre
notion
office
language
literature
philosophy
travel
登录
Search
标签搜索
ubuntu
mysql
openwrt
zerotier
springboot
centos
openvpn
jdk
吞食天地2
synology
spring
idea
windows11
吞食天地1
transmission
google-play
Japanese
xcode
群晖
kiftd
MoonjerX
累计撰写
386
篇文章
累计收到
466
条评论
首页
栏目
moonjerx
game
age-of-empires
zx3
san-guo-zhi
尼尔:机械纪元
net
emby
learn-video
docker
torrent
photoshop
route
minio
git
ffmpeg
im
vue
gitlab
typecho
svn
alipay
nasm
srs
mail-server
tailscale
kkfileview
aria2
webdav
synology
redis
oray
chemical
mxsite
math
π
x-ui
digital-currency
server
nginx
baota
k8s
http
cloud
linux
shell
database
vpn
esxi
rancher
domain
k3s
ewomail
os
android
windows
ios
app-store
macos
develop
java
javascript
uniapp
nodejs
hbuildx
maven
android-studio
jetbrain
jenkins
css
mybatis
php
python
hardware
hard-disk
pc
RAM
software
pt
calibre
notion
office
language
literature
philosophy
travel
页面
关于
壁纸
直播
留言
友链
统计
搜索到
386
篇与
moonjerx
的结果
2026-06-20
自建 RustDesk 进阶篇:把信令和中继藏进 443,
这是上一篇《自建 RustDesk 服务端的三个容器,到底各干什么?》的续集。建议先看完上一篇,理解 hbbs / hbbr / api 各自的角色,这篇会直接用到。上一篇结尾留了个尾巴:在跨境(比如大陆访问香港节点)场景里,RustDesk 的默认信令端口 21116 容易被链路上的设备做 RST 干扰,导致服务器一切正常、端口看着也通,但主控端就是连不上。当时给的应急办法是换端口(21116 → 31116 之类)。它的问题在于:如果干扰是按端口号识别的,换个端口能躲一阵;但如果是按协议特征(DPI) 识别的,换端口只是续命,过几天新端口照样被追上。这篇讲根治方案:把 hbbs 的信令和 hbbr 的中继,全部塞进 443 端口的 TLS 加密流量(WSS) 里。一、为什么 443 + WSS 能根治核心就一句话:让你的远控流量,在链路上看起来和访问一个普通 HTTPS 网站一模一样。躲开端口特征:对外只暴露 443,没有 21116、21117 这些一眼就知道是远控的"特征端口"。躲开协议特征(DPI):流量被 TLS 加密,中间设备拆不开,看不到里面的 RustDesk 握手,自然没法按协议指纹下手。顺带一个安全红利:全部走 443 之后,21114-21119 这些端口可以对公网全部关掉,攻击面大幅缩小。WSS = WebSocket over TLS。RustDesk 服务端本来就内置了 WebSocket 端口,我们要做的就是用 nginx 在 443 上做 TLS 终止,再把 WebSocket 转发给它们。二、用到哪些 WebSocket 端口RustDesk 服务端的两个核心服务,各自有一个专门的 WebSocket 端口:服务WebSocket 端口干什么hbbs(红娘)21118信令的 ws 通道hbbr(搬运工)21119中继的 ws 通道rustdesk-api(前台)21114网页后台 / API(普通 HTTP)这三个端口只需要 nginx 在本机访问,不用对公网开放。客户端永远只连 443。注意:如果你之前用了"换端口"的招(比如 hbbs 改成了 31116),上 WSS 之后可以撤回默认 21116。因为公网根本碰不到 21116 了,只有本机 nginx 去连它,换不换端口都无所谓。回归默认反而更省心。三、服务端:nginx 在 443 做 TLS 反代假设你的域名是 rustdesk.example.com,证书已经签好(下一节讲证书)。nginx 站点配置:server { listen 443 ssl http2; server_name rustdesk.example.com; ssl_certificate /etc/nginx/cert/rustdesk.example.com.pem; ssl_certificate_key /etc/nginx/cert/rustdesk.example.com.key; # 网页后台 + API(rustdesk-api 容器,本机 21114) location / { proxy_pass http://127.0.0.1:21114; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # ① hbbs 信令的 WebSocket(本机 21118) location /ws/id { proxy_pass http://127.0.0.1:21118; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 120s; } # ② hbbr 中继的 WebSocket(本机 21119) location /ws/relay { proxy_pass http://127.0.0.1:21119; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 120s; } }三个关键点:/ws/id → 21118、/ws/relay → 21119 是 RustDesk 约定好的路径,不能改名。WebSocket 必须带 Upgrade / Connection "upgrade" 这两个头,并且 proxy_http_version 1.1,否则握手升级不了。proxy_read_timeout 给长一点(120s 起),远控是长连接,超时太短会被掐断。如果你用宝塔面板,可以在站点的"反向代理"里分别加这几条;但 /ws/id、/ws/relay 的 WebSocket 头宝塔默认可能不带,建议直接编辑站点的 nginx 配置文件手动加,最稳。改完 nginx -t 测试无误后 nginx -s reload。四、证书域名解析到服务器后,用 Let's Encrypt 签一张免费证书即可。宝塔面板里"网站 → SSL → Let's Encrypt"一键申请最省事。命令行用 acme.sh 或 certbot 也行。证书路径填到上面 nginx 配置的 ssl_certificate / ssl_certificate_key 两行。WSS 必须有合法证书,自签证书客户端会拒连。这是和"裸 TCP"最大的不同——但也正是这张证书,让你的流量长得像正经网站。五、客户端:改成走 443 的 WSS这一步是 WSS 方案的前提,也是要特别说明的地方:客户端必须是支持 WebSocket 的版本/构建。 RustDesk 较新版本的服务端和客户端已原生支持 WebSocket 连接;如果你是自己编译的客户端(比如做了品牌定制),需要确认构建里开启了 WebSocket 能力。配置上,客户端的 ID 服务器 / 中继服务器 / API 服务器都指向你的 443 域名(rustdesk.example.com),Key 不变。WebSocket 启用后,客户端会自动走 wss://你的域名/ws/id 和 wss://你的域名/ws/relay,全程 443。各版本开启 WebSocket 的具体开关可能不同(有的是网络设置里的选项,有的需要构建参数)。落地前请以你所用版本/构建的实际配置为准,先用一台客户端验证能连通,再全量下发。六、收尾大杀器:只留 443,关掉所有远控端口确认 WSS 能正常远控之后,就可以做这件最爽的事了:把 21114-21119 对公网全部关闭,防火墙只留 443(和 SSH)。按 RustDesk 官方说法,当客户端全部走 WebSocket 时,服务端可以关掉 21114-21119,只保留 443 一个对外端口。此时:对外扫描你的服务器,只看到一个 443,和千千万万个网站没区别;链路上的设备抓不到远控特征,也就没法按端口或协议来封;攻击面从一排端口缩成一个,安全性也上来了。防火墙(云厂商安全组 + 服务器本地,两层都要)最终只需要:协议端口说明TCP443全部远控流量(WSS)+ 网页后台TCP你的 SSH 端口远程管理21114-21119 那一排,可以从两层防火墙里全部删掉。七、和"换端口"方案怎么选 换端口443 WSS落地难度极低(改配置 + 放行端口)中(要证书 + nginx + 客户端支持 ws)抗封能力续命,可能被重新识别根治,看起来就是普通 HTTPS对外端口仍暴露一排远控端口只留 443适合场景临时救急、先恢复使用长期稳定运营实战建议:出问题先用换端口快速恢复业务,腾出手再上 WSS 做长期方案。 两步走,既不耽误用,又能根治。小结WSS 方案的本质,是把"一眼就能被认出来的远控流量",伪装成"满大街都是的 HTTPS 流量"。服务端:nginx 在 443 做 TLS,/ws/id 转 21118、/ws/relay 转 21119;客户端:用支持 WebSocket 的构建,指向 443 域名;收尾:关掉 21114-21119,只留 443。
2026年06月20日
26 阅读
0 评论
0 点赞
2026-06-20
自建 RustDesk 服务端的三个容器,到底各干什么?一篇大白话讲清楚
自己搭过 RustDesk 服务端的人,大概都被那一长串 docker run 劝退过:hbbs、hbbr、还有一个第三方的 rustdesk-api,三个容器一堆端口,看着头大。这篇就用大白话把它们讲明白。先打个比方建立整体印象:RustDesk 干的事,本质就是 让两台电脑隔着互联网"牵手"。三个容器分别扮演:红娘、搬运工、前台管理员。记住这三个角色,后面就全通了。一、rustdesk-hbbs —— 红娘(信令服务器)hbbs -r 你的域名:21117它负责 牵线搭桥。所有客户端开机后,都会先到红娘这里 报到登记:"我是 ID 12345,我现在在线,我的网络地址是 xxx"。当你想远程某台电脑时,你先找红娘说一句"我要连 67890",红娘就把 双方的网络地址互相告知,撮合两台电脑尝试 直接牵手(P2P 打洞)。它用到三个端口——而且你只需要指定主端口,另外两个是 hbbs 自动派生出来的:端口协议用途怎么来的21116TCP+UDP主信令:登记报到、发起连接请求你指定的主端口21115TCPNAT 类型测试(判断你的网络好不好打洞)主端口 减 121118TCPWebSocket,给网页客户端用主端口 加 2小记:hbbs 可以理解成 Handbook bridge server——牵线的那个。二、rustdesk-hbbr —— 搬运工(中继服务器)hbbr红娘撮合之后,如果两台电脑能 直接牵手(P2P 成功),数据就走直连,搬运工不参与,速度最快。但现实里,双方常常都躲在路由器后面(NAT),握不上手。 这时候就轮到搬运工出场:两台电脑各自把数据交给它,由它在中间 转发画面、键鼠、文件。走中继时,所有流量都从它身上过,所以 它是最吃带宽的一个。如果你的主控和被控分布在不同地区、还经常打洞失败,那这台搬运工的出口带宽就是体验好坏的关键。端口协议用途21117TCP中继转发数据(屏幕 / 键鼠 / 文件都走这)21119TCPWebSocket 中继,网页客户端用小记:hbbr = Handbook bridge relay——搬东西的那个。红娘那条 -r 你的域名:21117 参数,就是让红娘在撮合时顺便告诉客户端:"打洞失败的话,去这个地址找搬运工。"三、rustdesk-api —— 前台管理员(Web 管理后台)lejianwen/rustdesk-api:...注意:这个容器不是 RustDesk 官方的,是社区做的增强后台。前面两个(红娘、搬运工)才是干活的核心,光有它俩,RustDesk 就已经能正常用了。这个 api 属于 锦上添花,它给你提供:网页管理后台(/_admin/):查看哪些设备在线、管理用户、地址簿、权限网页客户端(/webclient/):不装软件,浏览器里直接发起远程API 接口:给客户端下发配置、登录鉴权等端口用途21114api 服务的 HTTP 端口它靠几个环境变量,知道红娘和搬运工分别在哪:ID_SERVER = 127.0.0.1:21116 # 红娘在本机 21116 RELAY_SERVER = 127.0.0.1:21117 # 搬运工在本机 21117 SIGNAL_SERVER = 127.0.0.1:21115 # NAT 测试端口如果你改了 hbbs 的主端口,这几个值要 跟着一起改,否则后台和真正干活的容器就对不上号了。把它们串起来看你(主控) 对方(被控) │ │ │ ①「我要连对方」 ① 早就登记过 │ └──────────► 红娘 hbbs (21116) ◄─────────────┘ │ ② 撮合,互告地址 │ ┌─────────────┴─────────────┐ ▼ ▼ 能直连?──是──► P2P 直连,搬运工不参与(最快) │ 否 ▼ 搬运工 hbbr (21117) 在中间转发画面 / 键鼠 / 文件一句话总结容器角色干什么核心端口缺了会怎样rustdesk-hbbs红娘登记、撮合、打洞21116不可缺,没它谁都连不上rustdesk-hbbr搬运工打洞失败时中继转发21117不可缺,没它打洞失败就彻底连不上rustdesk-api前台管理员网页后台 / 网页客户端 / API21114可缺,只是没有后台,裸用照样行hbbs 牵线、hbbr 搬货、api 管后台。 前两个是骨架,第三个是装修。附录:一键部署脚本(可直接套用)下面这套脚本把三个容器一次性拉起来。用之前只需要改一处——把 RD_DOMAIN 换成你自己的域名:#!/usr/bin/env bash # RustDesk 服务端一键部署脚本(hbbs + hbbr + api) # 用法:改好 RD_DOMAIN,保存为 deploy.sh,然后 bash deploy.sh RD_DOMAIN="rustdesk.example.com" # ← 改成你自己的域名 RD_DIR="/home/$USER/dockerfile/rustdesk" # 数据存放目录,可自定义 # 清理旧容器(首次部署没有也不会报错) docker rm -f rustdesk-hbbs rustdesk-hbbr rustdesk-api 2>/dev/null # 准备数据目录 mkdir -p "$RD_DIR"/{data,api} # ① 红娘:hbbs 信令服务器 docker run -d \ --name rustdesk-hbbs \ --restart always \ --network host \ -v "$RD_DIR/data":/root \ rustdesk/rustdesk-server:1.1.15 \ hbbs -r ${RD_DOMAIN}:21117 # ② 搬运工:hbbr 中继服务器 docker run -d \ --name rustdesk-hbbr \ --restart always \ --network host \ -v "$RD_DIR/data":/root \ rustdesk/rustdesk-server:1.1.15 \ hbbr # ③ 前台管理员:rustdesk-api 网页后台(第三方,可选) docker run -d \ --name rustdesk-api \ --restart always \ --network host \ -e TZ=Asia/Shanghai \ -e RUSTDESK_API_LANG=zh-CN \ -e RUSTDESK_API_APP_WEB_CLIENT=1 \ -e RUSTDESK_API_RUSTDESK_ID_SERVER=127.0.0.1:21116 \ -e RUSTDESK_API_RUSTDESK_RELAY_SERVER=127.0.0.1:21117 \ -e RUSTDESK_API_RUSTDESK_API_SERVER=https://${RD_DOMAIN} \ -e RUSTDESK_API_RUSTDESK_SIGNAL_SERVER=127.0.0.1:21115 \ -e RUSTDESK_API_APP_REGISTER=false \ -v "$RD_DIR/api":/app/data \ -v "$RD_DIR/data":/app/conf/data \ lejianwen/rustdesk-api:v2.7 # 等待启动,打印关键信息 sleep 5 echo "=== 部署完成 ===" docker ps | grep rustdesk echo "" echo "管理后台: https://${RD_DOMAIN}/_admin/" echo "网页客户端: https://${RD_DOMAIN}/webclient" echo "ID 服务器: ${RD_DOMAIN}:21116" echo "中继服务器: ${RD_DOMAIN}:21117" echo "公钥(Key): $(cat "$RD_DIR/data/id_ed25519.pub" 2>/dev/null || echo '稍等几秒再查看')" echo "管理员密码: $(docker logs rustdesk-api 2>/dev/null | grep -i 'Admin Password Is:' | tail -1 | sed 's/.*Admin Password Is: //')"部署完别忘了 放行端口(云厂商安全组 + 服务器本地防火墙,两层都要开):协议端口说明TCP21114-21119api / hbbs / hbbr 全套UDP21116hbbs 信令(必须,容易漏)客户端配置时填三样:ID 服务器 = 你的域名:21116、中继服务器 = 你的域名:21117、Key = 上面打印出来的公钥内容。脚本里用的是默认端口 21116。如果你的网络环境需要换端口(见下一节),把 hbbs 那行加上 -p 新端口,同时改 api 的 ID_SERVER / SIGNAL_SERVER,并放行新端口对应的三个口。一点实战补充:为什么有人把 21116 改成别的端口21116 是 RustDesk 的默认信令端口,也正因为太出名,成了被重点识别的"特征端口"。在一些跨境(比如大陆访问香港节点)的场景里,会遇到一个很迷惑的现象:服务器、防火墙、Key、配置全都正确telnet 端口看着也通但只要真正发起远程,主控端就连不上这种情况往往不是服务器的锅,而是 链路上对"特定 IP + 21116 端口"做了 RST 干扰。一个常见的应急办法,就是把 hbbs 的主端口从 21116 挪到一个冷门高位端口(比如 31116),配置同步下发,很多时候就能立刻恢复。记得改端口要三处一起改:hbbs 的 -p 参数、api 的环境变量、防火墙放行新端口(派生的三个端口都要放)——而且云厂商安全组和服务器本地防火墙是两层,都得开。不过换端口属于"续命"而非"根治",更稳的长期方案是把信令和中继套进 443 的 TLS/WSS 流量里,让链路层抓不到特征。这就是另一篇的故事了。
2026年06月20日
26 阅读
0 评论
0 点赞
2026-05-18
禁用 Windows 10 自动更新 — 完整踩坑记录
操作环境:Windows 10,PowerShell(管理员权限) 目标:彻底、永久禁止系统自动更新一、背景Windows 10 的自动更新机制非常顽固,微软在多个层面都做了保护:服务层:wuauserv(Windows Update)+ UsoSvc(Update Orchestrator)策略层:注册表 AU 策略计划任务层:\Microsoft\Windows\UpdateOrchestrator\* 下十余个任务权限保护:部分计划任务文件归属 TrustedInstaller,普通管理员无法直接修改仅禁用服务是不够的,Win10 会通过计划任务自动把服务重新拉起。二、踩坑过程第一次尝试(只禁服务 + 注册表)Stop-Service wuauserv -Force Set-Service wuauserv -StartupType Disabled Set-Service UsoSvc -StartupType Disabled Stop-Service UsoSvc -Force reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoUpdate /t REG_DWORD /d 1 /f reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v AUOptions /t REG_DWORD /d 1 /f问题:未处理计划任务,系统会通过 UpdateOrchestrator 定时任务重新启用服务并触发更新。第二次尝试(追加禁用计划任务)Get-ScheduledTask -TaskPath "\Microsoft\Windows\UpdateOrchestrator\*" | Disable-ScheduledTask报错:Disable-ScheduledTask : 拒绝访问。 HRESULT 0x80070005原因:这些计划任务文件(位于 C:\Windows\System32\Tasks\Microsoft\Windows\UpdateOrchestrator\)的所有者是 TrustedInstaller,即使管理员也无写入权限。第三次尝试(夺权后再禁用)✅ 成功思路:对每个任务文件先 takeown 夺取所有权,再 icacls 授权,最后执行 Disable-ScheduledTask。Get-ScheduledTask -TaskPath "\Microsoft\Windows\UpdateOrchestrator\*" | ForEach-Object { $path = "C:\Windows\System32\Tasks\Microsoft\Windows\UpdateOrchestrator\$($_.TaskName)" takeown /f $path /a icacls $path /grant Administrators:F Disable-ScheduledTask -TaskName $_.TaskName -TaskPath $_.TaskPath }结果:全部 13 个任务均成功禁用(State: Disabled)。三、最终完整一行命令⚠️ 需以管理员身份运行 PowerShellStop-Service wuauserv -Force; Set-Service wuauserv -StartupType Disabled; Set-Service UsoSvc -StartupType Disabled; Stop-Service UsoSvc -Force; reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoUpdate /t REG_DWORD /d 1 /f; reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v AUOptions /t REG_DWORD /d 1 /f; Get-ScheduledTask -TaskPath "\Microsoft\Windows\UpdateOrchestrator\*" | ForEach-Object { $path = "C:\Windows\System32\Tasks\Microsoft\Windows\UpdateOrchestrator\$($_.TaskName)"; takeown /f $path /a; icacls $path /grant Administrators:F; Disable-ScheduledTask -TaskName $_.TaskName -TaskPath $_.TaskPath }四、三层封锁说明层级操作说明服务层禁用 wuauserv + UsoSvc停止并设为 Disabled,阻止开机自启策略层写注册表 AU 策略NoAutoUpdate=1,AUOptions=1,策略级别覆盖用户设置计划任务层夺权并禁用全部 UpdateOrchestrator 任务防止任务重新拉起服务五、被禁用的计划任务清单任务名作用Reboot_AC插电时重启安装更新Reboot_Battery电池模式重启安装更新Report policies上报策略信息Schedule Maintenance Work计划维护工作Schedule Scan计划扫描更新Schedule Scan Static Task静态扫描任务Schedule Wake To Work唤醒执行更新Schedule Work计划更新工作Start Oobe Expedite WorkOOBE 加急更新StartOobeAppsScanAfterUpdate更新后扫描应用StartOobeAppsScan_LicenseAccepted许可接受后扫描UpdateModelTask更新模型任务USO_UxBroker更新界面代理六、恢复方法(如需重新开启更新)Set-Service wuauserv -StartupType Manual Start-Service wuauserv Set-Service UsoSvc -StartupType Manual Start-Service UsoSvc Get-ScheduledTask -TaskPath "\Microsoft\Windows\UpdateOrchestrator\*" | Enable-ScheduledTask reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f记录时间:2026-05-18
2026年05月18日
73 阅读
0 评论
0 点赞
2026-04-29
用 FFmpeg + BAT 批处理打造视频工具箱:踩坑全记录
记录一次从零开始编写 Windows 批处理视频工具箱的完整过程,重点整理调试途中踩到的经典 CMD 坑。工具最终实现了视频掐头去尾、多种合并模式、旋转、分辨率预处理、参数对比统一等功能。最终脚本@echo off chcp 936 >nul setlocal enabledelayedexpansion :: 配置项: 是否显示ffmpeg编码输出 (1=显示, 0=隐藏) set "SHOW_LOG=1" title 视频工具箱 :MENU cls echo ========================================== echo 视频工具箱 (FFmpeg) echo ========================================== echo. echo 1. 移除视频开头 (掐头) echo 2. 移除视频结尾 (去尾) echo 3. 合并多个视频 (直接合并) echo 4. 合并多个视频 (自动统一分辨率) echo 5. 旋转视频 - 快速 (仅修改标记) echo 6. 旋转视频 - 真实 (重新编码) echo 7. 预处理视频分辨率 (填充黑边) echo 8. 预处理视频参数 (对比并统一) echo 9. 退出 echo. echo ========================================== set /p choice="请输入选项 (1-9): " if "%choice%"=="1" goto MODE_CUT_START if "%choice%"=="2" goto MODE_CUT_END if "%choice%"=="3" goto MODE_MERGE if "%choice%"=="4" goto MODE_MERGE_PAD if "%choice%"=="5" goto MODE_ROTATE_FAST if "%choice%"=="6" goto MODE_ROTATE_REAL if "%choice%"=="7" goto MODE_PAD if "%choice%"=="8" goto MODE_PREPROCESS if "%choice%"=="9" exit /b goto MENU :: ========================================== :: 模式 1: 移除开头 :: ========================================== :MODE_CUT_START cls echo [模式: 移除开头] echo. set /a file_count=0 set /a index=0 echo 序号 文件名 echo ---------------------------------------- for /f "delims=" %%i in ('dir /b *.mp4 *.mov *.m4v 2^>nul') do ( set "tn=%%i" echo !tn! | findstr /i "_out _rotated _padded _handle" >nul if errorlevel 1 ( set /a index+=1 set "file_!index!=%%i" set /a file_count+=1 echo !index!. %%i ) ) if !file_count! equ 0 (echo 未找到视频文件 & pause & goto MENU) echo. set /p sel="输入序号(逗号分隔): " if "%sel%"=="" goto MENU set /p sec="移除开头秒数: " if "%sec%"=="" goto MENU set "sel=%sel: =%" set "sel=%sel:,= %" for %%n in (%sel%) do ( call set "vf=%%file_%%n%%" echo 处理: !vf! if "%SHOW_LOG%"=="1" ( for /f "delims=" %%f in ("!vf!") do ffmpeg -ss %sec% -i "!vf!" -map 0 -c:v copy -c:a copy -map_metadata 0 "%%~nf_out%%~xf" -y ) else ( for /f "delims=" %%f in ("!vf!") do ffmpeg -ss %sec% -i "!vf!" -map 0 -c:v copy -c:a copy -map_metadata 0 "%%~nf_out%%~xf" -y -loglevel error ) if !errorlevel! equ 0 (echo 成功) else (echo 失败) ) pause & goto MENU :: ========================================== :: 模式 2: 移除结尾 :: ========================================== :MODE_CUT_END cls echo [模式: 移除结尾] echo. set /a file_count=0 set /a index=0 echo 序号 文件名 echo ---------------------------------------- for /f "delims=" %%i in ('dir /b *.mp4 *.mov *.m4v 2^>nul') do ( set "tn=%%i" echo !tn! | findstr /i "_out _rotated _padded _handle" >nul if errorlevel 1 ( set /a index+=1 set "file_!index!=%%i" set /a file_count+=1 echo !index!. %%i ) ) if !file_count! equ 0 (echo 未找到视频文件 & pause & goto MENU) echo. set /p sel="输入序号(逗号分隔): " if "%sel%"=="" goto MENU set /p rmsec="移除结尾秒数: " if "%rmsec%"=="" goto MENU set "sel=%sel: =%" set "sel=%sel:,= %" for %%n in (%sel%) do ( call set "vf=%%file_%%n%%" echo 处理: !vf! ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "!vf!" > temp_dur.txt 2>nul set /p dur=<temp_dur.txt del temp_dur.txt 2>nul for /f %%b in ('powershell -command "[math]::Round([double]'!dur!' - %rmsec%, 3)"') do set keep=%%b echo 保留: !keep! 秒 if "%SHOW_LOG%"=="1" ( for /f "delims=" %%f in ("!vf!") do ffmpeg -i "!vf!" -t !keep! -map 0 -c:v copy -c:a copy -map_metadata 0 "%%~nf_out%%~xf" -y ) else ( for /f "delims=" %%f in ("!vf!") do ffmpeg -i "!vf!" -t !keep! -map 0 -c:v copy -c:a copy -map_metadata 0 "%%~nf_out%%~xf" -y -loglevel error ) if !errorlevel! equ 0 (echo 成功) else (echo 失败) ) pause & goto MENU :: ========================================== :: 模式 3: 直接合并 :: ========================================== :MODE_MERGE cls echo [模式: 合并视频 (直接合并)] echo. set /a file_count=0 set /a index=0 echo 序号 文件名 echo ---------------------------------------- for /f "delims=" %%i in ('dir /b *.mp4 *.mov *.m4v 2^>nul') do ( set /a index+=1 set "file_!index!=%%i" set /a file_count+=1 echo !index!. %%i ) if !file_count! equ 0 (echo 未找到视频文件 & pause & goto MENU) echo. set /p sel="输入序号(逗号分隔): " if "%sel%"=="" goto MENU del temp_list.txt 2>nul set "sel=%sel: =%" set "sel=%sel:,= %" for %%n in (%sel%) do ( call set "tmpf=%%file_%%n%%" echo file '!tmpf!' >> temp_list.txt ) echo 合并中... if "%SHOW_LOG%"=="1" ( ffmpeg -f concat -safe 0 -i temp_list.txt -c copy -y merged_out.mp4 ) else ( ffmpeg -f concat -safe 0 -i temp_list.txt -c copy -y merged_out.mp4 -loglevel error -stats ) if exist merged_out.mp4 ( echo 成功: merged_out.mp4 del temp_list.txt 2>nul ) else ( echo 失败 ) pause & goto MENU :: ========================================== :: 模式 4: 合并视频 (全自动 - 统一所有参数后合并) :: ========================================== :MODE_MERGE_PAD cls echo [模式: 合并视频 - 全自动统一参数并合并] echo. set /a file_count=0 set /a index=0 echo 序号 文件名 echo ---------------------------------------- for /f "delims=" %%i in ('dir /b *.mp4 *.mov *.m4v 2^>nul') do ( set /a index+=1 set "file_!index!=%%i" set /a file_count+=1 echo !index!. %%i ) if !file_count! equ 0 (echo 未找到视频文件 & pause & goto MENU) echo. set /p sel="输入序号(逗号分隔): " if "%sel%"=="" goto MENU del temp_sel.txt temp_list.txt temp_merge_*.mp4 2>nul set "sel=%sel: =%" set "sel=%sel:,= %" for %%n in (%sel%) do ( call set "tmpf=%%file_%%n%%" echo !tmpf! >> temp_sel.txt ) :: 读取所有文件信息 set /a maxw=0 set /a maxh=0 set /a sel_count=0 for /f "delims=" %%f in (temp_sel.txt) do ( set /a sel_count+=1 echo %%f > temp_cur.txt set /p cur_file=<temp_cur.txt del temp_cur.txt 2>nul set "sel_file_!sel_count!=!cur_file!" ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=s=x:p=0 "!cur_file!" > temp_w.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 "!cur_file!" > temp_h.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of csv=s=x:p=0 "!cur_file!" > temp_br.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=time_base -of csv=s=x:p=0 "!cur_file!" > temp_tb.txt 2>nul set /p tw=<temp_w.txt set /p th=<temp_h.txt set /p tbr=<temp_br.txt set /p ttb=<temp_tb.txt del temp_w.txt temp_h.txt temp_br.txt temp_tb.txt 2>nul set /a tw=!tw!+0 set /a th=!th!+0 set "sel_w_!sel_count!=!tw!" set "sel_h_!sel_count!=!th!" set "sel_br_!sel_count!=!tbr!" set "sel_tb_!sel_count!=!ttb!" if !tw! gtr !maxw! set /a maxw=!tw! if !th! gtr !maxh! set /a maxh=!th! ) del temp_sel.txt 2>nul :: 展示对比信息 echo. echo ========================================== for /L %%i in (1,1,%sel_count%) do ( echo [%%i] !sel_file_%%i! echo 分辨率: !sel_w_%%i! x !sel_h_%%i! time_base: !sel_tb_%%i! ) echo. echo 目标分辨率: !maxw! x !maxh! 目标time_base: 1/90000 echo ========================================== echo. :: 逐个处理并生成临时文件 for /L %%i in (1,1,%sel_count%) do ( echo %%i > temp_idx.txt echo !sel_file_%%i! > temp_cf.txt echo !sel_w_%%i! > temp_vw.txt echo !sel_h_%%i! > temp_vh.txt echo !sel_tb_%%i! > temp_vtb.txt echo !sel_br_%%i! > temp_vbr.txt set /p cur_file=<temp_cf.txt set /p vw=<temp_vw.txt set /p vh=<temp_vh.txt set /p vtb=<temp_vtb.txt set /p vbr=<temp_vbr.txt del temp_cf.txt temp_vw.txt temp_vh.txt temp_vtb.txt temp_vbr.txt temp_idx.txt 2>nul set /a vw=!vw!+0 set /a vh=!vh!+0 set /a vbr=!vbr!+0 echo [%%i] !cur_file! set "need_pad=1" if "!vw!"=="!maxw!" if "!vh!"=="!maxh!" set "need_pad=0" set "need_tb=1" echo !vtb! > temp_check.txt findstr /x "1/90000" temp_check.txt >nul 2>nul if not errorlevel 1 set "need_tb=0" del temp_check.txt 2>nul if "!need_pad!"=="1" ( echo 填充分辨率 + 统一time_base set /a px=maxw - vw set /a px=px / 2 set /a py=maxh - vh set /a py=py / 2 ffmpeg -noautorotate -i "!cur_file!" -vf "pad=!maxw!:!maxh!:!px!:!py!:black" -c:v libx264 -preset fast -b:v !vbr! -video_track_timescale 90000 -c:a copy "temp_merge_%%i.mp4" -y -loglevel error if exist "temp_merge_%%i.mp4" ( echo 完成 echo file 'temp_merge_%%i.mp4' >> temp_list.txt ) else ( echo 失败 ) ) if "!need_pad!"=="0" if "!need_tb!"=="1" ( echo 统一time_base ffmpeg -i "!cur_file!" -c copy -video_track_timescale 90000 "temp_merge_%%i.mp4" -y -loglevel error if exist "temp_merge_%%i.mp4" ( echo 完成 echo file 'temp_merge_%%i.mp4' >> temp_list.txt ) else ( echo 失败 ) ) if "!need_pad!"=="0" if "!need_tb!"=="0" ( echo 参数一致,直接使用 echo file '!cur_file!' >> temp_list.txt ) echo. ) echo 合并中... ffmpeg -f concat -safe 0 -i temp_list.txt -c copy -y merged_out.mp4 -loglevel error -stats if exist merged_out.mp4 ( echo. echo 成功: merged_out.mp4 del temp_list.txt temp_merge_*.mp4 2>nul ) else ( echo 失败 ) pause & goto MENU :: ========================================== :: 模式 5: 旋转视频 - 快速 :: ========================================== :MODE_ROTATE_FAST cls echo [模式: 旋转视频 - 快速] echo. set /a file_count=0 set /a index=0 echo 序号 文件名 echo ---------------------------------------- for /f "delims=" %%i in ('dir /b *.mp4 *.mov *.m4v 2^>nul') do ( set "tn=%%i" echo !tn! | findstr /i "_rotated" >nul if errorlevel 1 ( set /a index+=1 set "file_!index!=%%i" set /a file_count+=1 echo !index!. %%i ) ) if !file_count! equ 0 (echo 未找到视频文件 & pause & goto MENU) echo. set /p sel="输入序号(逗号分隔): " if "%sel%"=="" goto MENU echo. echo 1. 左转90度 echo 2. 右转90度 echo 3. 旋转180度 set /p rc="请选择: " if "%rc%"=="1" set "rv=270" if "%rc%"=="2" set "rv=90" if "%rc%"=="3" set "rv=180" if not defined rv goto MODE_ROTATE_FAST set "sel=%sel: =%" set "sel=%sel:,= %" for %%n in (%sel%) do ( call set "vf=%%file_%%n%%" echo 处理: !vf! for /f "delims=" %%f in ("!vf!") do ffmpeg -i "!vf!" -map 0 -c copy -metadata:s:v:0 rotate=%rv% "%%~nf_rotated%%~xf" -y -loglevel error if !errorlevel! equ 0 (echo 成功) else (echo 失败) ) pause & goto MENU :: ========================================== :: 模式 6: 旋转视频 - 真实 :: ========================================== :MODE_ROTATE_REAL cls echo [模式: 旋转视频 - 真实重编码] echo. set /a file_count=0 set /a index=0 echo 序号 文件名 echo ---------------------------------------- for /f "delims=" %%i in ('dir /b *.mp4 *.mov *.m4v 2^>nul') do ( set "tn=%%i" echo !tn! | findstr /i "_rotated" >nul if errorlevel 1 ( set /a index+=1 set "file_!index!=%%i" set /a file_count+=1 echo !index!. %%i ) ) if !file_count! equ 0 (echo 未找到视频文件 & pause & goto MENU) echo. set /p sel="输入序号(逗号分隔): " if "%sel%"=="" goto MENU echo. echo 1. 左转90度 echo 2. 右转90度 echo 3. 旋转180度 set /p rc="请选择: " if "%rc%"=="1" set "tv=2" if "%rc%"=="2" set "tv=1" if "%rc%"=="3" set "tv=2,transpose=2" if not defined tv goto MODE_ROTATE_REAL set "sel=%sel: =%" set "sel=%sel:,= %" for %%n in (%sel%) do ( call set "vf=%%file_%%n%%" echo 处理: !vf! if "%SHOW_LOG%"=="1" ( for /f "delims=" %%f in ("!vf!") do ffmpeg -i "!vf!" -vf "transpose=%tv%" -c:v libx264 -preset fast -crf 18 -c:a copy "%%~nf_rotated%%~xf" -y ) else ( for /f "delims=" %%f in ("!vf!") do ffmpeg -i "!vf!" -vf "transpose=%tv%" -c:v libx264 -preset fast -crf 18 -c:a copy "%%~nf_rotated%%~xf" -y -loglevel error -stats ) if !errorlevel! equ 0 (echo 成功) else (echo 失败) ) pause & goto MENU :: ========================================== :: 模式 7: 预处理视频分辨率 :: ========================================== :MODE_PAD cls echo [模式: 预处理视频分辨率] echo. set /a file_count=0 set /a index=0 echo 序号 分辨率(宽x高) 文件名 echo ---------------------------------------- for /f "delims=" %%i in ('dir /b *.mp4 *.mov *.m4v 2^>nul') do ( set /a index+=1 set "file_!index!=%%i" set /a file_count+=1 ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=s=x:p=0 "%%i" > temp_w.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 "%%i" > temp_h.txt 2>nul set /p fw=<temp_w.txt set /p fh=<temp_h.txt del temp_w.txt temp_h.txt 2>nul echo !index!. !fw! x !fh! %%i ) if !file_count! equ 0 (echo 未找到视频文件 & pause & goto MENU) echo. set /p sel="输入序号: " if "%sel%"=="" goto MENU call set "vf=%%file_%sel%%%" echo. echo 选中: !vf! echo. echo 请输入目标分辨率,格式: 宽,高 echo 例如当前视频是 720x400,想填充到 720x404,输入: 720,404 echo. set /p res="目标分辨率(宽,高): " if "%res%"=="" goto MENU for /f "tokens=1,2 delims=," %%a in ("%res%") do ( set /a target_w=%%a set /a target_h=%%b ) echo 目标分辨率: !target_w!(宽) x !target_h!(高) ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=s=x:p=0 "!vf!" > temp_w.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 "!vf!" > temp_h.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of csv=s=x:p=0 "!vf!" > temp_br.txt 2>nul set /p vw=<temp_w.txt set /p vh=<temp_h.txt set /p vbr=<temp_br.txt del temp_w.txt temp_h.txt temp_br.txt 2>nul set /a vw=!vw!+0 set /a vh=!vh!+0 set /a vbr=!vbr!+0 if "!vw!"=="!target_w!" if "!vh!"=="!target_h!" ( echo 当前分辨率已与目标一致,无需处理 pause & goto MENU ) set /a px=target_w - vw set /a px=px / 2 set /a py=target_h - vh set /a py=py / 2 echo 当前: !vw!(宽) x !vh!(高) 填充: 左右各!px!px 上下各!py!px echo 处理中... for /f "delims=" %%f in ("!vf!") do ( if "%SHOW_LOG%"=="1" ( ffmpeg -noautorotate -i "!vf!" -vf "pad=!target_w!:!target_h!:!px!:!py!:black" -c:v libx264 -preset fast -b:v !vbr! -video_track_timescale 90000 -c:a copy "%%~nf_padded%%~xf" -y ) else ( ffmpeg -noautorotate -i "!vf!" -vf "pad=!target_w!:!target_h!:!px!:!py!:black" -c:v libx264 -preset fast -b:v !vbr! -video_track_timescale 90000 -c:a copy "%%~nf_padded%%~xf" -y -loglevel error ) ) if !errorlevel! equ 0 ( echo 成功 ) else ( echo 失败 ) pause & goto MENU :: ========================================== :: 模式 8: 预处理视频参数 (对比并统一) :: ========================================== :MODE_PREPROCESS cls echo [模式: 预处理视频参数 - 对比并统一] echo. set /a file_count=0 set /a index=0 echo 序号 文件名 echo ---------------------------------------- for /f "delims=" %%i in ('dir /b *.mp4 *.mov *.m4v 2^>nul') do ( set "tn=%%i" echo !tn! | findstr /i "_handle" >nul if errorlevel 1 ( set /a index+=1 set "file_!index!=%%i" set /a file_count+=1 echo !index!. %%i ) ) if !file_count! equ 0 (echo 未找到视频文件 & pause & goto MENU) echo. set /p sel="输入要对比的文件序号(逗号分隔,至少2个): " if "%sel%"=="" goto MENU del temp_sel.txt 2>nul set "sel=%sel: =%" set "sel=%sel:,= %" for %%n in (%sel%) do ( call set "tmpf=%%file_%%n%%" echo !tmpf! >> temp_sel.txt ) :: 读取文件信息并存入变量 set /a maxw=0 set /a maxh=0 set /a sel_count=0 for /f "delims=" %%f in (temp_sel.txt) do ( set /a sel_count+=1 echo %%f > temp_cur.txt set /p cur_file=<temp_cur.txt del temp_cur.txt 2>nul set "sel_file_!sel_count!=!cur_file!" ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=s=x:p=0 "!cur_file!" > temp_w.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 "!cur_file!" > temp_h.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of csv=s=x:p=0 "!cur_file!" > temp_br.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=time_base -of csv=s=x:p=0 "!cur_file!" > temp_tb.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of csv=s=x:p=0 "!cur_file!" > temp_fps.txt 2>nul ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of csv=s=x:p=0 "!cur_file!" > temp_codec.txt 2>nul ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "!cur_file!" > temp_dur.txt 2>nul set /p fw=<temp_w.txt set /p fh=<temp_h.txt set /p fbr=<temp_br.txt set /p ftb=<temp_tb.txt set /p ffps=<temp_fps.txt set /p fcodec=<temp_codec.txt set /p fdur=<temp_dur.txt del temp_w.txt temp_h.txt temp_br.txt temp_tb.txt temp_fps.txt temp_codec.txt temp_dur.txt 2>nul set /a fw=!fw!+0 set /a fh=!fh!+0 set "sel_w_!sel_count!=!fw!" set "sel_h_!sel_count!=!fh!" set "sel_br_!sel_count!=!fbr!" set "sel_tb_!sel_count!=!ftb!" set "sel_fps_!sel_count!=!ffps!" set "sel_codec_!sel_count!=!fcodec!" set "sel_dur_!sel_count!=!fdur!" if !fw! gtr !maxw! set /a maxw=!fw! if !fh! gtr !maxh! set /a maxh=!fh! ) del temp_sel.txt 2>nul :: 展示对比结果 echo. echo ========================================== echo 对比结果 echo ========================================== for /L %%i in (1,1,%sel_count%) do ( echo. echo [%%i] !sel_file_%%i! echo 分辨率 : !sel_w_%%i! x !sel_h_%%i! echo time_base: !sel_tb_%%i! echo 帧率 : !sel_fps_%%i! echo 编码 : !sel_codec_%%i! echo 码率 : !sel_br_%%i! bps echo 时长 : !sel_dur_%%i! 秒 ) echo. echo ========================================== echo 统一参数 echo ========================================== echo 目标分辨率 : !maxw! x !maxh! echo 目标time_base: 1/90000 echo. echo 将对每个文件生成 _handle 文件 echo ========================================== echo. set /p confirm="确认处理? (y/n): " if /i not "%confirm%"=="y" goto MENU echo. for /L %%i in (1,1,%sel_count%) do ( echo !sel_file_%%i! > temp_cf.txt echo !sel_w_%%i! > temp_vw.txt echo !sel_h_%%i! > temp_vh.txt echo !sel_tb_%%i! > temp_vtb.txt echo !sel_br_%%i! > temp_vbr.txt set /p cur_file=<temp_cf.txt set /p vw=<temp_vw.txt set /p vh=<temp_vh.txt set /p vtb=<temp_vtb.txt set /p vbr=<temp_vbr.txt del temp_cf.txt temp_vw.txt temp_vh.txt temp_vtb.txt temp_vbr.txt 2>nul set /a vw=!vw!+0 set /a vh=!vh!+0 set /a vbr=!vbr!+0 echo [%%i] 处理: !cur_file! set "need_pad=1" if "!vw!"=="!maxw!" if "!vh!"=="!maxh!" set "need_pad=0" set "need_tb=1" echo !vtb! > temp_check.txt findstr /x "1/90000" temp_check.txt >nul 2>nul if not errorlevel 1 set "need_tb=0" del temp_check.txt 2>nul if "!need_pad!"=="1" ( echo 填充分辨率 + 统一time_base set /a px=maxw - vw set /a px=px / 2 set /a py=maxh - vh set /a py=py / 2 for /f "delims=" %%f in ("!cur_file!") do ( ffmpeg -noautorotate -i "!cur_file!" -vf "pad=!maxw!:!maxh!:!px!:!py!:black" -c:v libx264 -preset fast -b:v !vbr! -video_track_timescale 90000 -c:a copy "%%~nf_handle%%~xf" -y -loglevel error ) ) if "!need_pad!"=="0" if "!need_tb!"=="1" ( echo 统一time_base for /f "delims=" %%f in ("!cur_file!") do ( ffmpeg -i "!cur_file!" -c copy -video_track_timescale 90000 "%%~nf_handle%%~xf" -y -loglevel error ) ) if "!need_pad!"=="0" if "!need_tb!"=="0" ( echo 无需处理,参数已一致 for /f "delims=" %%f in ("!cur_file!") do ( copy "!cur_file!" "%%~nf_handle%%~xf" >nul ) ) for /f "delims=" %%f in ("!cur_file!") do ( if exist "%%~nf_handle%%~xf" ( echo 成功: %%~nf_handle%%~xf ) else ( echo 失败 ) ) echo. ) echo ========================================== echo 全部处理完成 echo 请使用模式3直接合并所有 _handle 文件 echo ========================================== pause & goto MENU背景手头有一批视频文件需要批量处理:掐头去尾、合并分集、统一分辨率。想做一个放在视频目录下就能双击运行的 .bat 工具箱,依赖 FFmpeg 和 ffprobe,无需安装额外环境。最终实现的功能菜单:1. 移除视频开头 (掐头) 2. 移除视频结尾 (去尾) 3. 合并多个视频 (直接合并) 4. 合并多个视频 (全自动统一参数后合并) 5. 旋转视频 - 快速 (仅修改标记) 6. 旋转视频 - 真实 (重新编码) 7. 预处理视频分辨率 (填充黑边) 8. 预处理视频参数 (对比并统一) 9. 退出经典错误一览❌ 错误1:-metadata:s:v rotate=-90 修改旋转无效现象: 执行后视频没有任何变化。原因: -metadata:s:v rotate=-90 只是修改元数据标记,很多播放器不识别,且参数值应为 270 而非 -90。正确做法::: 快速旋转(仅改标记,秒级完成) ffmpeg -i input.mp4 -map 0 -c copy -metadata:s:v:0 rotate=270 output.mp4 :: 真实旋转(重新编码,100%兼容) ffmpeg -i input.mp4 -vf "transpose=2" -c:v libx264 output.mp4transpose 参数对照:1 = 顺时针90°2 = 逆时针90°2,transpose=2 = 旋转180°❌ 错误2:-ss 参数位置错误导致截取不准现象: 指定去掉开头8秒,实际从第6秒开始截取;输出文件比原文件还大。原因: -ss 放在 -i 之后是输出侧裁剪,需要解码所有帧,精度低且文件结构复杂。:: ❌ 错误写法(输出侧裁剪) ffmpeg -i input.mp4 -ss 8 -c copy output.mp4 :: ✅ 正确写法(输入侧裁剪,快速且精确) ffmpeg -ss 8 -i input.mp4 -c copy output.mp4❌ 错误3:ffprobe 获取分辨率时 %%h 变量失效现象: 脚本显示 宽=500 高=%h,高度变成了字面字符串。原因: 在 for 循环中使用 tokens=1,2 delims=x 时,%%h 作为第二个 token 变量,在循环外或特定上下文中会失效。修复: 将宽和高分两次独立获取,并通过临时文件中转:ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=s=x:p=0 "input.mp4" > temp_w.txt ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 "input.mp4" > temp_h.txt set /p vw=<temp_w.txt set /p vh=<temp_h.txt del temp_w.txt temp_h.txt❌ 错误4:文件名含 [ ] 导致 for /f 解析失败现象: 文件名如 [001]-video.mp4,在 for /f ... in ('dir /b') 循环中调用 ffprobe 时,分辨率始终为空。原因: CMD 的 for 循环会将 [ ] 当作通配符处理。修复: 先将文件名写入临时文件,再用 set /p 读取:echo %%f > temp_cur.txt set /p cur_file=<temp_cur.txt del temp_cur.txt ffprobe ... "!cur_file!" > temp_w.txt❌ 错误5:set /a 浮点数截断导致1KB空文件现象: 去除结尾操作后,输出文件只有1KB。原因: 视频时长是浮点数(如 125.47 秒),set /a 只能处理整数,直接赋值会截断为 125,导致 -t 参数计算错误。修复: 使用 PowerShell 计算浮点数:ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "input.mp4" > temp_dur.txt set /p dur=<temp_dur.txt for /f %%b in ('powershell -command "[math]::Round([double]'!dur!' - %rmsec%, 3)"') do set keep=%%b ffmpeg -i "input.mp4" -t !keep! -c copy output.mp4❌ 错误6:if !var!==!var! 触发 CMD 解析崩溃现象: 整个 for 循环块进不去,连第一行 pause 也无法执行,直接闪退。原因: if !vw!==!maxw! 展开后形如 if 608==608,CMD 解析时将右侧紧贴 == 的 ! 误识别为三个等号,导致整个代码块预解析失败。:: ❌ 危险写法 if !vw!==!maxw! ... :: ✅ 正确写法(变量两边加引号) if "!vw!"=="!maxw!" ...CMD 铁律:if 比较变量时必须加引号。❌ 错误7:for /L 循环内 if...else 嵌套导致预解析崩溃现象: 同样是进不去循环,直接闪退。原因: CMD 对整个括号块进行预解析,循环内嵌套 if "%SHOW_LOG%"=="1" (...) else (...) 会导致括号层次解析失败。:: ❌ 危险写法 for /L %%i in (1,1,%count%) do ( if "%SHOW_LOG%"=="1" ( ffmpeg ... ) else ( ffmpeg ... -loglevel error ) ) :: ✅ 修复写法(去掉 else,拆成两个独立 if) for /L %%i in (1,1,%count%) do ( if "%SHOW_LOG%"=="1" ffmpeg ... if "%SHOW_LOG%"=="0" ffmpeg ... -loglevel error )❌ 错误8:set /a px=(!maxw! - !vw!) / 2 导致 for 块崩溃(最隐蔽)现象: 经历了错误6、7的修复后,循环依然闪退,连循环体第一行都进不去。根本原因(由 GPT 最终定位):for (...) do ( ... ) 本身是代码块,内部再出现 set /a px=(...) 的算术括号,叠加延迟变量 !var!,CMD 预解析阶段会将括号结构解析错,导致整个 for 块语法崩溃。这是 CMD 解析期(parse-time)崩溃,而非运行期错误,因此任何运行期的修复都无效。:: ❌ 致命写法(for块内的算术括号) set /a px=(!maxw! - !vw!) / 2 set /a py=(!maxh! - !vh!) / 2 :: ✅ 正确写法(去掉括号,set /a 内可直接用变量名) set /a px=maxw - vw set /a px=px / 2 set /a py=maxh - vh set /a py=py / 2set /a 中可以直接使用变量名,不需要 !变量!,且不能在 for 块内使用括号表达式。❌ 错误9:合并视频后 time_base 不一致导致时间轴异常现象: 两个视频合并后,播放到衔接点卡住;用 ffprobe 检查合并文件时长,发现远超两段视频之和(如应为7000秒,实际显示41000秒)。诊断:ffprobe -v error -show_entries stream=codec_name,start_time,time_base -of default=noprint_wrappers=1 "video1.mp4" ffprobe -v error -show_entries stream=codec_name,start_time,time_base -of default=noprint_wrappers=1 "video2.mp4"输出对比:video1: time_base=1/15360 video2: time_base=1/90000原因: time_base 不一致,流复制拼接时时间戳计算完全错乱。修复: 合并前统一 time_base 为标准值 1/90000(无需重新编码,仅修改容器信息):ffmpeg -i "video1.mp4" -c copy -video_track_timescale 90000 "video1_fixed.mp4"❌ 错误10:填充黑边后 time_base 被重置现象: 用 -c:v libx264 重编码填充黑边后,生成文件的 time_base 变为非标准值,再与其他文件合并时又出现时间轴问题。修复: 填充黑边时同步指定 time_base:ffmpeg -noautorotate -i "input.mp4" ^ -vf "pad=1920:1080:8:0:black" ^ -c:v libx264 -preset fast -b:v 1200000 ^ -video_track_timescale 90000 ^ -c:a copy "output.mp4"CMD 批处理避坑总结问题错误写法正确写法变量比较if !a!==!b!if "!a!"=="!b!"for块内if分支if (...) else (...)拆成两个独立 iffor块内算术set /a x=(!a! - !b!) / 2set /a x=a-b / set /a x=x/2含特殊字符文件名直接在for循环中使用先写临时文件再 set /p 读取浮点数计算set /a 直接赋值通过 PowerShell 计算编码问题chcp 65001 (UTF-8)chcp 936 (ANSI/GBK)CMD 的 for 代码块会整体预解析,任何括号结构错误都会导致整块失效,表现为直接闪退。这是 parse-time 崩溃,与运行期错误性质不同,运行期的修复对其无效。FFmpeg 常用参数备忘:: 查看视频信息 ffprobe -v error -show_entries stream=codec_name,width,height,r_frame_rate,bit_rate,time_base -of default=noprint_wrappers=1 "input.mp4" :: 流复制截取(快速,输入侧 -ss) ffmpeg -ss 10 -i "input.mp4" -c copy "output.mp4" :: 填充黑边 ffmpeg -i "input.mp4" -vf "pad=宽:高:x偏移:y偏移:black" -c:v libx264 "output.mp4" :: 统一 time_base(不重编码) ffmpeg -i "input.mp4" -c copy -video_track_timescale 90000 "output.mp4" :: concat 合并 ffmpeg -f concat -safe 0 -i filelist.txt -c copy "output.mp4"工具依赖:FFmpeg(含 ffprobe),需加入系统 PATH。脚本放于视频目录下双击运行。
2026年04月29日
55 阅读
0 评论
1 点赞
2026-04-27
此内容被密码保护
加密文章,请前往内页查看详情
2026年04月27日
49 阅读
0 评论
0 点赞
1
2
...
78
您的IP: