DV转HDR10.ps1

<#PSScriptInfo
 
.VERSION 1.0.0
 
.GUID 5e8883a1-e074-4138-8def-d8c0a4607e41
 
.AUTHOR 埃博拉酱
 
.COMPANYNAME 一致行动党
 
.COPYRIGHT (c) 2026 埃博拉酱. All rights reserved.
 
.TAGS ffmpeg DolbyVision HDR10 HEVC HEVCConvert VideoTranscode NvidiaEncode
 
.LICENSEURI https://opensource.org/licenses/MIT
 
.RELEASENOTES
    1.0.0 - 初始发布。支持 Dolby Vision Profile 5 → HDR10 转换,
            优先 hevc_nvenc GPU 编码,回退到 libx265 CPU 编码,
            缺少 ffmpeg 时通过 winget 自动安装。
 
#>


<#
.SYNOPSIS
    将 Dolby Vision Profile 5 HEVC 视频转换为标准 HDR10 HEVC。
 
.DESCRIPTION
    使用 ffmpeg 的 libplacebo 过滤器(需 Vulkan)正确处理 Dolby Vision
    Profile 5 的 ICtCp 色彩空间,输出兼容所有 HDR10 播放器的标准 HEVC 文件。
 
    编码器优先级:hevc_nvenc(NVIDIA GPU)→ libx265(CPU)。
    缺少支持 libplacebo 的 ffmpeg 时,通过 winget 自动安装 Gyan.FFmpeg。
 
.PARAMETER 输入文件
    源视频文件路径(MKV、MP4 等,须含 Dolby Vision Profile 5 视频轨)。
 
.PARAMETER 输出文件
    输出文件路径。省略时自动在源文件同目录生成,后缀为 _HDR10.mkv。
 
.PARAMETER 质量
    编码质量(1-51,默认 19)。hevc_nvenc 对应 CQ 值,libx265 对应 CRF 值。
    数值越小质量越高、文件越大。
 
.PARAMETER 测试秒数
    仅转换前 N 秒(默认 0 = 转换完整文件)。用于快速验证结果。
 
.EXAMPLE
    .\DV转HDR10.ps1 -输入文件 "movie.DV.mkv"
 
    转换完整文件,输出 movie.DV_HDR10.mkv。
 
.EXAMPLE
    .\DV转HDR10.ps1 -输入文件 "movie.DV.mkv" -测试秒数 10
 
    仅转换前 10 秒,输出 movie.DV_HDR10_test10s.mkv,用于验证颜色。
 
.EXAMPLE
    .\DV转HDR10.ps1 -输入文件 "movie.DV.mkv" -输出文件 "movie.HDR10.mkv" -质量 22
 
    自定义输出路径和质量。
 
.NOTES
    依赖:ffmpeg 8.1+(Gyan 完整版,含 libplacebo + Vulkan)。
    缺少时脚本会通过 winget 自动安装。
 
    Dolby Vision Profile 5 使用 ICtCp 色彩空间,普通播放器若不识别 DV 元数据
    则会将像素误判为 BT.2020nc,导致画面严重偏绿。本脚本通过 libplacebo 的
    apply_dolbyvision 参数在 GPU 上完成正确的颜色空间转换,输出标准 HDR10。
#>


# 将 Dolby Vision Profile 5 HEVC 转换为标准 HDR10 HEVC
# 优先使用 hevc_nvenc(GPU),不支持时回退到 libx265(CPU)
# 依赖:ffmpeg 8.1+(含 libplacebo + Vulkan)

param(
    [Parameter(Mandatory, HelpMessage = "输入 MKV/MP4 文件路径")]
    [string]$输入文件,

    [string]$输出文件 = "",

    [ValidateRange(1, 51)]
    [int]$质量 = 19,   # nvenc CQ 或 libx265 CRF,数值越小质量越高

    [int]$测试秒数 = 0  # 大于 0 时只转换前 N 秒(用于测试)
)

# ── 自动生成输出文件名 ─────────────────────────────
if (-not $输出文件) {
    $绝对路径 = (Resolve-Path $输入文件).Path
    $基础名   = [System.IO.Path]::GetFileNameWithoutExtension($绝对路径)
    $目录     = [System.IO.Path]::GetDirectoryName($绝对路径)
    $后缀     = if ($测试秒数 -gt 0) { "_HDR10_test${测试秒数}s" } else { "_HDR10" }
    $输出文件 = Join-Path $目录 "${基础名}${后缀}.mkv"
}

# ── 查找支持 libplacebo 的 ffmpeg ───────────────────
function 查找ffmpeg {
    # 优先查 winget 安装的 Gyan.FFmpeg
    $winget包路径 = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages\Gyan.FFmpeg*" `
        -Recurse -Filter "ffmpeg.exe" -ErrorAction SilentlyContinue |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 1 -ExpandProperty FullName

    $候选列表 = @($winget包路径, (Get-Command ffmpeg -ErrorAction SilentlyContinue).Source) |
        Where-Object { $_ -and (Test-Path $_) }

    foreach ($路径 in $候选列表) {
        $临时文件 = "$env:TEMP\ff_filters_$PID.txt"
        Start-Process -FilePath $路径 -ArgumentList "-hide_banner", "-filters" `
            -Wait -NoNewWindow -RedirectStandardOutput $临时文件 | Out-Null
        if (Select-String -Path $临时文件 -Pattern "libplacebo" -Quiet) {
            Remove-Item $临时文件 -ErrorAction SilentlyContinue
            return $路径
        }
        Remove-Item $临时文件 -ErrorAction SilentlyContinue
    }
    return $null
}

$ffmpeg = 查找ffmpeg
if (-not $ffmpeg) {
    Write-Host "未找到支持 libplacebo 的 ffmpeg,尝试通过 winget 自动安装 Gyan.FFmpeg ..."

    if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
        Write-Error "winget 不可用,请手动安装 ffmpeg 8.1+(Gyan 完整版)后重试。"
        exit 1
    }

    $安装结果 = winget install Gyan.FFmpeg --silent 2>&1
    if ($LASTEXITCODE -ne 0) {
        Write-Error "winget 安装失败:$安装结果"
        exit 1
    }

    # 刷新当前进程的 PATH
    $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" +
                [System.Environment]::GetEnvironmentVariable("PATH", "User")

    $ffmpeg = 查找ffmpeg
    if (-not $ffmpeg) {
        Write-Error "安装后仍未找到支持 libplacebo 的 ffmpeg,请重新打开终端后再试。"
        exit 1
    }

    Write-Host "安装成功。"
}

$版本信息 = (& $ffmpeg -version 2>&1 | Select-Object -First 1) -replace "ffmpeg version ", ""
Write-Host "ffmpeg : $版本信息"
Write-Host "路径 : $ffmpeg"

# ── 检测 hevc_nvenc ────────────────────────────────
$编码器临时 = "$env:TEMP\ff_encoders_$PID.txt"
Start-Process -FilePath $ffmpeg -ArgumentList "-hide_banner", "-encoders" `
    -Wait -NoNewWindow -RedirectStandardOutput $编码器临时 | Out-Null
$支持nvenc = Select-String -Path $编码器临时 -Pattern "hevc_nvenc" -Quiet
Remove-Item $编码器临时 -ErrorAction SilentlyContinue

# ── 构建过滤器和编码参数 ──────────────────────────
$视频过滤器基础 = "libplacebo=colorspace=bt2020nc:color_trc=smpte2084:color_primaries=bt2020:range=tv:apply_dolbyvision=true"

if ($支持nvenc) {
    Write-Host "编码器 : hevc_nvenc(GPU)"
    $视频过滤器 = "$视频过滤器基础,format=p010le"
    $视频编码参数 = @(
        "-c:v", "hevc_nvenc",
        "-preset", "p4",
        "-rc", "vbr",
        "-cq", "$质量"
    )
} else {
    Write-Host "编码器 : libx265(CPU,hvenc 不可用)"
    $x265参数 = "colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:hdr10=1:crf=$质量"
    $视频过滤器 = "$视频过滤器基础,format=yuv420p10le"
    $视频编码参数 = @(
        "-c:v", "libx265",
        "-preset", "medium",
        "-x265-params", $x265参数
    )
}

$色彩元数据 = @(
    "-color_primaries", "bt2020",
    "-color_trc",       "smpte2084",
    "-colorspace",      "bt2020nc",
    "-color_range",     "tv"
)

# ── 组装完整参数列表 ─────────────────────────────
$时长参数 = if ($测试秒数 -gt 0) { @("-t", "$测试秒数") } else { @() }
$参数列表 = @("-y") + $时长参数 + @("-i", $输入文件, "-vf", $视频过滤器) +
            $视频编码参数 +
            $色彩元数据 +
            @("-map", "0:v:0", "-map", "0:a", "-c:a", "copy", $输出文件)

# ── 执行转换 ──────────────────────────────────────
Write-Host ""
Write-Host "输入 : $输入文件"
Write-Host "输出 : $输出文件"
Write-Host "质量 : $质量"
if ($测试秒数 -gt 0) { Write-Host "测试模式: 仅转换前 ${测试秒数} 秒" }
Write-Host ""
Write-Host "── 开始转换 ──────────────────────────────────"

& $ffmpeg @参数列表

if ($LASTEXITCODE -eq 0) {
    $大小MB = [math]::Round((Get-Item $输出文件).Length / 1MB, 1)
    Write-Host ""
    Write-Host "── 转换完成 ──────────────────────────────────"
    Write-Host "输出文件:$输出文件(${大小MB} MB)"
} else {
    Write-Error "转换失败(退出码:$LASTEXITCODE)"
    exit $LASTEXITCODE
}