烧录字幕.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 失败,退出码 $退出码"
}