烧录字幕.ps1
|
<#PSScriptInfo
.VERSION 1.0.0 .GUID 8a3f5c2e-7b1d-4e9a-b6c8-2d4f0e1a3b5c .AUTHOR 埃博拉酱-机器人 .COMPANYNAME 一致行动党 .COPYRIGHT (c) 2026 埃博拉酱-机器人 MIT License. .TAGS ffmpeg subtitle hardsub hevc nvenc libx265 burn-in video mkv 字幕 烧录 .LICENSEURI https://opensource.org/licenses/MIT .RELEASENOTES 1.0.0 - 初始版本。支持图形字幕和文本字幕烧录,NVENC 硬件加速编码,自动回退 libx265。 #> <# .SYNOPSIS 将视频中第一个字幕轨烧录(硬编码)到第一个视频轨,使用 HEVC 编码。 .DESCRIPTION 调用 ffmpeg 将视频文件中的第一个字幕轨渲染到第一个视频轨上,生成硬字幕视频。 支持图形字幕(DVD/PGS/DVB/XSUB)和文本字幕(ASS/SRT 等)。 优先使用 hevc_nvenc (NVIDIA GPU) 编码,不可用时自动回退到 libx265 (CPU)。 烧录后移除原视频轨和原字幕轨,其余轨道(音频等)原样保留。 .PARAMETER 输入文件 输入视频文件路径(支持 MKV/MP4 等 ffmpeg 支持的格式)。 .PARAMETER 输出文件 输出文件路径。默认在同目录生成 _烧录字幕 后缀的同格式文件。 .EXAMPLE .\烧录字幕.ps1 "D:\电影.mkv" 将 电影.mkv 中的字幕烧录到视频,输出 电影_烧录字幕.mkv。 .EXAMPLE .\烧录字幕.ps1 "D:\电影.mkv" "D:\输出.mkv" 指定输出文件路径。 .NOTES 前置依赖:ffmpeg、ffprobe 需在 PATH 中可用。 .LINK https://ffmpeg.org/ #> param( [Parameter(Mandatory, Position = 0)] [string]$输入文件, [Parameter(Position = 1)] [string]$输出文件 ) $ErrorActionPreference = 'Stop' if (-not (Test-Path -LiteralPath $输入文件)) { throw "文件不存在: $输入文件" } if (-not $输出文件) { $目录 = [IO.Path]::GetDirectoryName($输入文件) $文件名 = [IO.Path]::GetFileNameWithoutExtension($输入文件) $扩展名 = [IO.Path]::GetExtension($输入文件) $输出文件 = [IO.Path]::Combine($目录, "${文件名}_烧录字幕${扩展名}") } # ── 探测流信息 ────────────────────────────────────────── $探测原始 = & ffprobe -v quiet -print_format json -show_streams $输入文件 2>&1 $探测结果 = ($探测原始 | Out-String).Trim() | ConvertFrom-Json $全部轨道 = $探测结果.streams $视频轨列表 = @($全部轨道 | Where-Object codec_type -eq 'video') $字幕轨列表 = @($全部轨道 | Where-Object codec_type -eq 'subtitle') if ($视频轨列表.Count -eq 0) { throw "输入文件没有视频轨" } if ($字幕轨列表.Count -eq 0) { throw "输入文件没有字幕轨" } $视频轨 = $视频轨列表[0] $字幕轨 = $字幕轨列表[0] $视频序号 = [int]$视频轨.index $字幕序号 = [int]$字幕轨.index $字幕编码 = $字幕轨.codec_name $视频宽 = [int]$视频轨.width $视频高 = [int]$视频轨.height Write-Host "视频轨 #$视频序号 : $($视频轨.codec_name) ${视频宽}x${视频高}" -ForegroundColor Cyan Write-Host "字幕轨 #$字幕序号 : $字幕编码" -ForegroundColor Cyan Write-Host "输出 : $输出文件" -ForegroundColor Green # ── 检测 NVENC 可用性 ────────────────────────────────── function Test-NVENC { $prevEA = $ErrorActionPreference $ErrorActionPreference = 'SilentlyContinue' & ffmpeg -hide_banner -f lavfi -i nullsrc=s=256x256:d=0.1 -c:v hevc_nvenc -f null NUL 2>&1 | Out-Null $可用 = $LASTEXITCODE -eq 0 $ErrorActionPreference = $prevEA return $可用 } if (Test-NVENC) { $编码器 = 'hevc_nvenc' $编码参数 = @('-c:v', 'hevc_nvenc', '-cq', '23', '-b:v', '0') Write-Host "编码器: hevc_nvenc (GPU)" -ForegroundColor Cyan } else { $编码器 = 'libx265' $编码参数 = @('-c:v', 'libx265', '-crf', '23', '-preset', 'medium') Write-Host "编码器: libx265 (CPU 回退,NVENC 不可用)" -ForegroundColor Yellow } # ── 构建滤镜 ──────────────────────────────────────────── $图形字幕编码 = 'dvd_subtitle', 'hdmv_pgs_subtitle', 'dvb_subtitle', 'xsub' if ($字幕编码 -in $图形字幕编码) { # 图形字幕:缩放到视频分辨率后叠加 $滤镜 = "[0:$字幕序号]scale=${视频宽}:${视频高}[sub];[0:$视频序号][sub]overlay=eof_action=pass[vout]" } else { # 文本字幕:用 subtitles 滤镜(需对路径做 ffmpeg 转义) $转义路径 = ($输入文件 -replace '\\', '/') -replace "([\[\]:;,'])", '\$1' $滤镜 = "[0:$视频序号]subtitles='${转义路径}':si=0[vout]" } # ── -map:烧录视频 + 除原视频轨和原字幕轨外的所有轨 ── $映射参数 = [Collections.Generic.List[string]]::new() $映射参数.Add('-map'); $映射参数.Add('[vout]') foreach ($轨 in $全部轨道) { $序号 = [int]$轨.index if ($序号 -eq $视频序号 -or $序号 -eq $字幕序号) { continue } $映射参数.Add('-map'); $映射参数.Add("0:$序号") } # ── 执行 ffmpeg ───────────────────────────────────────── $ff参数 = @('-i', $输入文件, '-filter_complex', $滤镜) + $映射参数.ToArray() + $编码参数 + @('-c:a', 'copy', '-c:s', 'copy', '-y', $输出文件) Write-Host "`n> ffmpeg $($ff参数 -join ' ')`n" -ForegroundColor DarkGray # ffmpeg 向 stderr 输出进度,需临时关闭 ErrorAction 以免误报 $prevEA = $ErrorActionPreference $ErrorActionPreference = 'SilentlyContinue' & ffmpeg @ff参数 $退出码 = $LASTEXITCODE $ErrorActionPreference = $prevEA if ($退出码 -eq 0) { Write-Host "`n完成: $输出文件" -ForegroundColor Green } else { throw "ffmpeg 失败,退出码 $退出码" } |