public/超级合并.ps1

function 超级合并 {
    <#
    .SYNOPSIS
        按列表或剪贴板顺序合并多个视频片段,兼容时尽量无损 copy。
 
    .DESCRIPTION
        读取一组输入文件,按给定顺序生成一个连续输出文件。命令会先探测每段的第一个视频轨、音频轨和字幕轨,再决定走“同构快速路径”或“标准化路径”。
 
        如果没有显式传入 -列表文件,命令会从剪贴板读取文件列表;显式传入 -列表文件 时,会按 UTF-8 文本读取。列表支持一行一个路径、带引号路径,以及ffmpeg concat 形式的 file '路径';空行和以 # 开头的注释会被忽略。
 
        同构快速路径用于流数量相同且每个流索引位置的 codec_type/codec_name 都相同的输入。该路径会先用 mkvmerge 按真实视频包 PTS 精剪每段,再用 ffmpeg 修复 H.264 SPS/PPS 容器元数据并通过 concat demuxer 无损拼接,尽量保留所有轨道。此路径的判定依据是“轨道布局和编码名”,不会进一步比较分辨率、像素格式、帧率等参数;这些参数中途变化时能否稳定播放取决于解码器和播放器。
 
        标准化路径用于轨道布局不同或编码布局不一致的输入。视频、音频、字幕会分别判断:兼容则 copy,不兼容或缺失时只标准化对应部分。视频会比较编码、宽度、高度、像素格式和帧率;如果只有分辨率不同,且当前视频编码允许同一视频轨中途改变分辨率(当前按 H.264/HEVC/AV1/VP9 判断),会保留各段原分辨率并继续 copy。若视频仍需因为编码、像素格式、帧率等其它原因重编码,则按 -不兼容策略 选择统一宽高/帧率,必要时缩放并补边,编码器优先 GPU(NVENC/QSV/AMF),否则回退 CPU。音频会比较编码、采样率和声道数;不兼容或缺失时补静音并重编码,通常输出 AAC,Max 模式遇到无损音频时会优先保住无损目标。字幕会处理 ASS/SRT 互转、缺失字幕占位,以及位图字幕的限制。
 
        输出容器会根据用户指定扩展名、输入容器和字幕/音频兼容性选择;必要时会切换到 MKV。若用户明确指定了不匹配的扩展名,命令可能先写内部 MKV,再移动成用户指定文件名,并提示实际容器。合并完成后会尽量写入码率、时长、帧率等流标签,并修正 Matroska 的 FrameRate/DefaultDuration 元素。
 
    .PARAMETER 输出文件
        目标输出文件路径。默认值为 .\merged.mkv。若显式指定路径但不带扩展名,会按实际输出容器补扩展名;若显式指定了扩展名,最终文件名会尽量保持该扩展名,即使内部实际容器因兼容性改为 MKV。
 
    .PARAMETER 列表文件
        包含输入文件路径的 UTF-8 文本文件。显式传入时必须存在。未显式传入时,不使用参数默认值读取文件,而是从剪贴板读取路径列表。
 
    .PARAMETER 不兼容策略
        当无法避免重编码且宽度、高度、帧率、采样率或声道数不一致时,选择目标参数的策略。Min 选择较小值,Max 选择较大值。默认 Min。
        视频编码不一致时统一为 HEVC;像素格式不一致时统一为 yuv420p。音频不兼容时通常统一为 AAC;Max 模式且存在无损音频时,会优先选择 FLAC/ALAC 这类无损目标以避免无损源被转为有损。
 
    .PARAMETER 详细模式
        显示外部命令的详细输出,并保留临时工作目录,便于排查分段标准化、concat 列表和中间文件。默认会清理临时目录。
 
    .EXAMPLE
        超级合并 -输出文件 'D:\合集.mkv' -列表文件 'D:\要合并的文件列表.txt'
 
        按列表文件中的顺序合并所有片段。
 
    .EXAMPLE
        超级合并 'D:\合集.mkv'
 
        从剪贴板读取路径列表,并输出到 D:\合集.mkv。
 
    .EXAMPLE
        超级合并 -输出文件 'D:\合集.mkv' -列表文件 'D:\列表.txt' -不兼容策略 Max -详细模式
 
        在参数不一致时选择较大的目标规格,显示外部工具输出,并保留临时目录用于检查。
 
    .NOTES
        依赖 ffmpeg 和 ffprobe;同构快速路径还依赖 mkvmerge。命令会尝试自动检测或安装缺失依赖。输出成功不等同于所有播放器都能完美处理非常规码流,尤其是同一视频轨中途变更分辨率或参数集的文件。
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$输出文件 = '.\merged.mkv',

        [Parameter(Mandatory = $false, Position = 1)]
        [string]$列表文件 = '.\要合并的文件列表.txt',

        [Parameter(Mandatory = $false, Position = 2)]
        [ValidateSet('Min', 'Max')]
        [string]$不兼容策略 = 'Min',

        [switch]$详细模式
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $script:输出外部进程信息 = [bool]$详细模式
    $工作目录 = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ("视频工坊_ffmpeg_concat_work_{0}" -f ([Guid]::NewGuid().ToString('N')))

    确保_ffmpeg_ffprobe_可用

    $列表文件是否显式指定 = $PSBoundParameters.ContainsKey('列表文件')

    if (-not $列表文件是否显式指定) {
        if (-not (Get-Command Get-Clipboard -ErrorAction SilentlyContinue)) {
            throw "未显式指定 -列表文件,且当前环境不支持 Get-Clipboard。请显式指定 -列表文件,或升级/启用剪贴板 cmdlet。"
        }

        $clip = Get-Clipboard -Raw
        if ([string]::IsNullOrWhiteSpace($clip)) {
            throw "未显式指定 -列表文件,但剪贴板为空(或无法读取)。请先在剪贴板中放入要合并的文件路径列表(每行一个),或显式指定 -列表文件。"
        }

        $输入文件列表 = 解析_输入文件列表_来自文本 -Text $clip
        $输入文件列表 = @($输入文件列表 | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
        if ($输入文件列表.Count -lt 1) { throw "从剪贴板解析到的文件列表为空。请确保剪贴板中每行一个路径,或为 ffmpeg concat 的 'file \'...\'' 格式。" }
    } else {
        if (-not (Test-Path -LiteralPath $列表文件)) {
            throw "未找到列表文件:$列表文件"
        }

        $列表文本 = Get-Content -LiteralPath $列表文件 -Encoding UTF8 -Raw
        $输入文件列表 = 解析_输入文件列表_来自文本 -Text $列表文本
        $输入文件列表 = @($输入文件列表 | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
        if ($输入文件列表.Count -lt 1) { throw "从列表文件解析到的输入文件为空:$列表文件" }
    }

    foreach ($f in $输入文件列表) { if (-not (Test-Path -LiteralPath $f)) { throw "输入文件不存在:$f" } }

    # 记录:用户是否显式传入了 -输出文件(含位置参数)。
    $用户明确指定输出文件 = $PSBoundParameters.ContainsKey('输出文件')
    $默认输出文件 = $输出文件
    $用户输出文件 = if ($用户明确指定输出文件) { $输出文件 } else { $null }

    Write-Host "探测媒体参数..." -ForegroundColor Cyan
    $媒体信息列表 = @($输入文件列表 | ForEach-Object { 获取_媒体信息 -Path $_ })

    if ((@($媒体信息列表 | Where-Object { $null -eq $_.Video })).Count -gt 0) {
        $bad = @($媒体信息列表 | Where-Object { $null -eq $_.Video } | Select-Object -ExpandProperty Path)
        throw "以下文件缺少视频轨,无法合并:`n$($bad -join "`n")"
    }

    # -------- 同构快速路径 --------
    # 如果所有输入的轨道布局完全一致(流数量相同、且每个索引位的 "codec_type:codec_name" 都相同),
    # 即看起来就是从同一原片裁出的多段,就直接 -map 0 -c copy 无损合并所有轨道,不做任何筛检/补齐/转码。
    $同构 = $false
    if ($媒体信息列表.Count -ge 1) {
        $首签名 = [string]$媒体信息列表[0].StreamSignature
        $同构 = $true
        for ($i = 1; $i -lt $媒体信息列表.Count; $i++) {
            if ([string]$媒体信息列表[$i].StreamSignature -ne $首签名) { $同构 = $false; break }
        }
    }

    if ($同构) {
        Write-Host ("检测到所有输入轨道布局相同({0} 路流),启用同构快速路径:精剪后无损合并全部轨道。" -f $媒体信息列表[0].StreamCount) -ForegroundColor Green

        # mkvmerge 是专为 Matroska 多轨 append 设计的工具,对外部裁切工具 -ss copy 产生的 pts 异常段比 ffmpeg concat 更健壮。
        确保_mkvmerge_可用

        New-Item -ItemType Directory -Force -Path $工作目录 | Out-Null

        # mkvmerge 只能输出 MKV;若用户扩展名不是 .mkv 则先写临时 mkv,随后改名。
        $用户指定输出 = $输出文件
        $用户扩展名 = [System.IO.Path]::GetExtension($用户指定输出)
        $内部输出 = if ($用户扩展名 -and $用户扩展名.ToLowerInvariant() -eq '.mkv') {
            $用户指定输出
        } else {
            [System.IO.Path]::ChangeExtension($用户指定输出, '.mkv')
        }
        $实际输出扩展名 = '.mkv'

        # 构造 mkvmerge 参数:先用 --split parts:vStart-vEnd 对每段精确裁切出干净的临时 mkv,
        # 再用 "+" 追加合并。
        #
        # 为什么需要先裁切:外部裁切工具 -ss copy 生成的段常见问题是
        # (a) 视频包 pts 不归零(例如起点 447s),容器 start_time/duration 与真实包范围不一致;
        # (b) 音频 / 字幕轨可能包含超出视频范围的尾巴包;
        # (c) 不同段之间时间戳冲突(重叠或跳跃),直接 mkvmerge --append 会触发丢包或错位。
        # mkvmerge 的 --split parts 会基于源 pts 做精确裁切,输出一个时间轴归零、容器元数据干净、
        # 各轨道对齐的新文件,然后再 append 就稳。所有信息都来自真实的视频包数据,不依赖文件名或
        # 容器元数据。
        #
        # 注意:修复容器 CodecPrivate(SPS/PPS)的步骤必须放在精剪之后执行。
        # 原因:外部裁切工具 -ss copy 产生的段里视频 pts 起点常常不为零且 B 帧 pts 乱序,
        # 若直接对原始段做 `ffmpeg -c copy -bsf:v extract_extradata` 远端复用器会因 DTS/PTS
        # 异常丢包;而经过 `mkvmerge --split parts` 精剪得到的 clip 时间轴已归零且单调,
        # 再跑 extract_extradata 就稳。
        Write-Host "探测每段视频首末包 pts(真实内容范围)..." -ForegroundColor Cyan
        $段范围 = New-Object System.Collections.Generic.List[object]
        for ($i = 0; $i -lt $媒体信息列表.Count; $i++) {
            $info = $媒体信息列表[$i]
            $pr = 调用_外部命令 -Exe 'ffprobe' -ArgumentList @(
                '-v','error','-select_streams','v:0',
                '-show_entries','packet=pts_time',
                '-of','csv=p=0', $info.Path
            ) -CaptureOutput
            $rows = @(($pr.StdOut -split "`r?`n") | Where-Object { $_ -match '^[\d.]+' })
            if ($rows.Count -lt 1) {
                throw "第 $($i+1) 段未能读取视频包 pts:$($info.Path)"
            }
            $vStart = [double]$rows[0]
            $vEnd   = [double]$rows[-1]
            $段范围.Add([pscustomobject]@{
                Path   = $info.Path
                VStart = $vStart
                VEnd   = $vEnd
            })
            Write-Host (" [{0}/{1}] v_pts {2:0.###}s → {3:0.###}s (内容 {4:0.###}s)" -f `
                ($i+1), $媒体信息列表.Count, $vStart, $vEnd, ($vEnd - $vStart))
        }

        # 用 mkvmerge --split parts 对每段精剪到 [vStart, vEnd+一帧],输出到工作目录。
        # mkvmerge 的时间戳要求 HH:MM:SS.nnn 格式,纯秒数不被接受。
        $格式化时间戳 = {
            param([double]$秒)
            if ($秒 -lt 0) { $秒 = 0 }
            $h = [int][math]::Floor($秒 / 3600)
            $m = [int][math]::Floor(($秒 - $h*3600) / 60)
            $s = $秒 - $h*3600 - $m*60
            return ('{0:D2}:{1:D2}:{2:00.000}' -f $h, $m, $s)
        }
        $临时段列表 = New-Object System.Collections.Generic.List[string]
        for ($i = 0; $i -lt $段范围.Count; $i++) {
            $r = $段范围[$i]
            $tmpOut = Join-Path $工作目录 ("clip_{0:D3}.mkv" -f ($i+1))
            # 末尾加一小段余量以确保末帧被包含(mkvmerge --split parts 的 end 是开区间式截断)
            $endWithMargin = [double]$r.VEnd + 0.5
            $startStr = & $格式化时间戳 ([double]$r.VStart)
            $endStr   = & $格式化时间戳 $endWithMargin
            Write-Host ("精剪第 {0}/{1} 段: parts:{2}-{3}" -f ($i+1), $段范围.Count, $startStr, $endStr) -ForegroundColor Cyan
            # mkvmerge 约定:0=成功,1=成功但含警告(例如字幕编码不规范),2=失败。仅 2 才应视为错误。
            调用_外部命令 -Exe 'mkvmerge' -ArgumentList @(
                '-o', $tmpOut,
                '--split', ("parts:{0}-{1}" -f $startStr, $endStr),
                $r.Path
            ) -AllowedExitCodes @(1) | Out-Null
            # mkvmerge --split 以 parts 模式通常只产出一个输出(因为只有一段 parts),文件名就是我们指定的 $tmpOut。
            # 但为了稳健,若它产出了 "clip_XXX-001.mkv" 这种变体,也要纳入。
            if (-not (Test-Path -LiteralPath $tmpOut)) {
                $candidate = Get-ChildItem -LiteralPath $工作目录 -Filter ("{0}*" -f [System.IO.Path]::GetFileNameWithoutExtension($tmpOut)) -File -ErrorAction SilentlyContinue | Select-Object -First 1
                if (-not $candidate) { throw "mkvmerge 精剪第 $($i+1) 段后未生成输出文件:$tmpOut" }
                $tmpOut = $candidate.FullName
            }
            $临时段列表.Add($tmpOut)
        }

        # 修复每个 clip 的 SPS/PPS 容器元数据(独立处理、纯 copy、不重编码)。
        # 外部裁切工具 -ss copy 产生的源段常见问题:容器 CodecPrivate(H.264 extradata)里没有有效 SPS/PPS
        # (ffprobe 解析不出 profile/pix_fmt 就是这种情形)。mkvmerge --split parts 会把源的
        # CodecPrivate 原样带到 clip 里,且 H.264 以 AVCC 格式(length-prefix)存储。
        # BSF 链:h264_mp4toannexb → 把 AVCC 转为 Annex-B(添加 0x000001 start code,使后续 BSF 可
        # 找到 SPS/PPS NALU);extract_extradata → 从 Annex-B 码流中提取 SPS/PPS 写回容器 extradata。
        # matroska muxer 收到 Annex-B 数据时会自动转回 AVCC 写入文件。
        # 不主动归零时间戳:clip 的源 pts 可能非零(如 447s 起),但后续 concat demuxer 会基于每段
        # start_time/duration 重建连续时间戳,源 pts 偏移不影响结果。
        # 非致命:若某段 BSF 修复后仍无视频包(极少数情形),降级为直接使用原始 clip,
        # profile 可能仍为 unknown,但合并本身不中断。
        Write-Host "修复每段 clip 的 SPS/PPS 容器元数据(独立处理、纯 copy)..." -ForegroundColor Cyan
        $修复后列表 = New-Object System.Collections.Generic.List[string]
        for ($i = 0; $i -lt $临时段列表.Count; $i++) {
            $clip = $临时段列表[$i]
            $fixed = Join-Path $工作目录 ("fixed_{0:D3}.mkv" -f ($i+1))
            调用_外部命令 -Exe 'ffmpeg' -ArgumentList @(
                '-nostdin','-v','error','-y',
                '-i', $clip,
                '-map','0','-c','copy',
                '-bsf:v','h264_mp4toannexb,extract_extradata',
                $fixed
            ) | Out-Null
            # 校验:修复后的 clip 必须能读出视频包。
            $gotVideo = $false
            if (Test-Path -LiteralPath $fixed) {
                $vc = 调用_外部命令 -Exe 'ffprobe' -ArgumentList @(
                    '-v','error','-select_streams','v:0',
                    '-show_entries','packet=pts_time',
                    '-of','csv=p=0', $fixed
                ) -CaptureOutput
                $vcRows = @(($vc.StdOut -split "`r?`n") | Where-Object { $_ -match '^[\d.]+' })
                $gotVideo = ($vcRows.Count -ge 1)
            }
            if (-not $gotVideo) {
                # BSF 修复失败:降级为直接使用原始 clip(不修改 extradata)。
                Write-Warning ("第 {0} 段 extradata 修复失败,降级使用原始 clip(profile 可能为 unknown)" -f ($i+1))
                Remove-Item -LiteralPath $fixed -Force -ErrorAction SilentlyContinue
                $fixed = $clip
            }
            $修复后列表.Add($fixed)
        }

        # 最终拼接:用 ffmpeg concat demuxer 把所有已修复 extradata 的 clip 首尾相接。
        # 为什么不用 mkvmerge --append:实测 mkvmerge 在合并源 pts 非零归起的 clip 时,会残留源时间戳偏移导致输出里有空洞,播放器在空洞处卡住。改用 ffmpeg concat demuxer 把每个 clip 当独立段重建连续 pts,纯 copy。
        $concatList = Join-Path $工作目录 'concat_fixed.txt'
        $concatLines = New-Object System.Collections.Generic.List[string]
        $总视频时长 = [double]0
        for ($idx = 0; $idx -lt $修复后列表.Count; $idx++) {
            $clipPath = $修复后列表[$idx]
            $esc = ($clipPath -replace '\\','/') -replace "'","'\\''"
            $concatLines.Add("file '$esc'")
            $clipDur = $null
            try { $clipVideoRange = 获取_视频包范围 -Path $clipPath; if ($clipVideoRange) { $clipDur = [double]$clipVideoRange.DurationSeconds } } catch { $clipDur = $null; $global:LASTEXITCODE = 0 }
            if (-not $clipDur -or $clipDur -le 0) { $fallbackSpan = [double]$段范围[$idx].VEnd - [double]$段范围[$idx].VStart; $fallbackFrame = if ($媒体信息列表[$idx].Video -and $媒体信息列表[$idx].Video.Fps -and $媒体信息列表[$idx].Video.Fps -gt 0) { 1.0 / [double]$媒体信息列表[$idx].Video.Fps } else { 0.041709 }; $clipDur = [math]::Max(0.0, ($fallbackSpan + $fallbackFrame)) }
            if ($clipDur -and $clipDur -gt 0) { $durStr = $clipDur.ToString('0.000000', [System.Globalization.CultureInfo]::InvariantCulture); $concatLines.Add("duration $durStr"); $总视频时长 += $clipDur }
        }
        $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
        [System.IO.File]::WriteAllLines($concatList, [string[]]$concatLines.ToArray(), $utf8NoBom)
        Write-Host ("开始 ffmpeg concat 合并 {0} 段精剪文件..." -f $修复后列表.Count) -ForegroundColor Cyan
        $finalConcatArgs = @('-nostdin','-v','error','-y','-f','concat','-safe','0','-i',$concatList,'-map','0','-c','copy','-map_metadata','-1','-map_metadata:s:v','-1','-map_metadata:s:a','-1','-map_metadata:s:s','-1','-map_chapters','-1','-avoid_negative_ts','make_zero')
        if ($总视频时长 -gt 0) { $总视频时长文本 = $总视频时长.ToString('0.000000', [System.Globalization.CultureInfo]::InvariantCulture); Write-Host ("按视频包累计时长裁齐全部轨道:{0}s" -f $总视频时长文本) -ForegroundColor Cyan; $finalConcatArgs += @('-t', $总视频时长文本) }
        $finalConcatArgs += @($内部输出)
        调用_外部命令 -Exe 'ffmpeg' -ArgumentList $finalConcatArgs | Out-Null
        Write-Host "完成:$内部输出" -ForegroundColor Green

        if ($内部输出 -ne $用户指定输出) {
            try {
                Move-Item -LiteralPath $内部输出 -Destination $用户指定输出 -Force
                Write-Host ("已将结果文件命名为:{0}(注意:容器实际为 {1})" -f $用户指定输出, $实际输出扩展名) -ForegroundColor Yellow
            } catch {
                Write-Warning "移动结果文件失败:$($_.Exception.Message)"
            }
        }

        if (-not $详细模式) {
            try { Remove-Item -LiteralPath $工作目录 -Recurse -Force -ErrorAction SilentlyContinue } catch { }
        } else {
            Write-Host "已保留临时目录:$工作目录" -ForegroundColor Yellow
        }

        $global:LASTEXITCODE = 0
        return
    }

    $存在字幕 = ((@($媒体信息列表 | Where-Object { $null -ne $_.Subtitle })).Count -gt 0)
    $全部有音轨 = ((@($媒体信息列表 | Where-Object { $null -eq $_.Audio })).Count -eq 0)

    # -------- 字幕目标格式选择(按优先级) --------
    # 位图字幕(PGS/DVD 等)无法与文本字幕互转,也无法用空字幕占位,只能要求所有段字幕一致并走 copy。
    # 1) 含位图字幕:要求所有段都有字幕且 codec 完全一致,否则抛错;目标=该位图 codec
    # 2) 任一输入包含 ass → 输出 ass
    # 3) 所有输入都是 subrip/srt → 输出 subrip
    # 4) 其它文本字幕混合 → 输出 ass
    # 5) 所有输入都不含字幕轨 → 输出也不含字幕轨
    $位图字幕编码集合 = @('hdmv_pgs_subtitle','pgssub','dvd_subtitle','dvdsub','dvb_subtitle','dvbsub','xsub')
    $目标字幕编码 = $null
    $目标字幕为位图 = $false
    if ($存在字幕) {
        $subCodecs = @($媒体信息列表 | Where-Object { $null -ne $_.Subtitle } | ForEach-Object { $_.Subtitle.Codec })
        $uniqSub = 取_非空字符串去重 -Values $subCodecs
        $含位图字幕 = @($uniqSub | Where-Object { $_ -in $位图字幕编码集合 }).Count -gt 0
        if ($含位图字幕) {
            $allHaveSub0 = ((@($媒体信息列表 | Where-Object { $null -eq $_.Subtitle })).Count -eq 0)
            if (-not $allHaveSub0) {
                throw "存在位图字幕($($uniqSub -join ','))但有片段缺少字幕轨,无法统一(位图字幕无法用空字幕占位)。请先为所有段准备同一种字幕,或先移除字幕轨。"
            }
            if ($uniqSub.Count -ne 1) {
                throw "字幕编码不一致:$($uniqSub -join ',')。位图字幕无法与其它编码互转,请先统一字幕格式。"
            }
            $目标字幕编码 = $uniqSub[0]
            $目标字幕为位图 = $true
        } elseif ($uniqSub -contains 'ass') {
            $目标字幕编码 = 'ass'
        } elseif ($uniqSub.Count -eq 1 -and $uniqSub[0] -in @('subrip','srt')) {
            # ffprobe 通常显示为 subrip
            $目标字幕编码 = 'subrip'
        } else {
            $目标字幕编码 = 'ass'
        }
    }

    # -------- 输出容器选择(按优先级) --------
    $用户输出扩展名 = $null
    if ($用户明确指定输出文件 -and $用户输出文件) {
        $tmpExt = [System.IO.Path]::GetExtension($用户输出文件)
        if (-not [string]::IsNullOrWhiteSpace($tmpExt)) { $用户输出扩展名 = $tmpExt.ToLowerInvariant() }
    }

    $实际输出扩展名 = 选择_输出容器扩展名 -输入文件列表 $输入文件列表 -存在字幕 $存在字幕 -目标字幕编码 $目标字幕编码 -用户明确指定输出文件 $用户明确指定输出文件 -用户输出扩展名 $用户输出扩展名

    # -------- 输出文件名规则 --------
    # 1) 用户明确指定输出文件路径:最终使用用户指定的扩展名(即便容器不同)
    # 2) 未明确指定输出路径:最终扩展名与实际输出容器一致

    $最终输出文件 = $null
    $内部输出文件 = $null

    if ($用户明确指定输出文件) {
        $finalDir = Split-Path -Parent $用户输出文件
        if ([string]::IsNullOrWhiteSpace($finalDir)) { $finalDir = (Get-Location).Path }
        $finalBase = [System.IO.Path]::GetFileNameWithoutExtension($用户输出文件)
        $finalExt = [System.IO.Path]::GetExtension($用户输出文件)
        if ([string]::IsNullOrWhiteSpace($finalExt)) {
            # 用户给了路径但没给扩展名:按实际容器补齐
            $最终输出文件 = Join-Path $finalDir ($finalBase + $实际输出扩展名)
        } else {
            $最终输出文件 = $用户输出文件
        }

        $内部输出文件 = Join-Path $finalDir ($finalBase + $实际输出扩展名)
    } else {
        $d = Split-Path -Parent $默认输出文件
        if ([string]::IsNullOrWhiteSpace($d)) { $d = (Get-Location).Path }
        $b = [System.IO.Path]::GetFileNameWithoutExtension($默认输出文件)
        $最终输出文件 = Join-Path $d ($b + $实际输出扩展名)
        $内部输出文件 = $最终输出文件
    }

    if ($内部输出文件 -ne $最终输出文件) {
        $reason = if ($存在字幕 -and -not (测试_容器是否支持_字幕编码 -容器扩展名 $用户输出扩展名 -字幕编码 $目标字幕编码)) {
            '字幕格式与用户指定容器不兼容'
        } elseif ($用户明确指定输出文件) {
            '用户扩展名推断容器(若无字幕不兼容)'
        } else {
            '输入容器不一致,优先兼容'
        }
        Write-Host ("输出容器将使用 {0}(原因:{1})。内部输出:{2};最终文件名:{3}" -f $实际输出扩展名, $reason, $内部输出文件, $最终输出文件) -ForegroundColor Yellow
    } else {
        Write-Host ("输出容器将使用 {0}(最终文件扩展名与容器一致)" -f $实际输出扩展名) -ForegroundColor DarkCyan
    }

    # 后续流程统一使用内部输出文件路径
    $输出文件 = $内部输出文件

    $视频编码集合 = @($媒体信息列表 | ForEach-Object { $_.Video.Codec } | Select-Object -Unique)
    $音频编码集合 = @($媒体信息列表 | ForEach-Object { if ($_.Audio) { $_.Audio.Codec } else { $null } } | Select-Object -Unique)

    # ---- 逐参数兼容性判定(兼容就保留;不兼容才选“最小值”作为输出标准化参数) ----

    $视频宽度值集合  = @($媒体信息列表 | ForEach-Object { $_.Video.Width })
    $视频高度值集合 = @($媒体信息列表 | ForEach-Object { $_.Video.Height })
    $视频分辨率值集合 = @($媒体信息列表 | ForEach-Object { if ($_.Video -and $null -ne $_.Video.Width -and $null -ne $_.Video.Height) { ("{0}x{1}" -f $_.Video.Width, $_.Video.Height) } else { 'N/A' } })
    $视频像素格式值集合 = @($媒体信息列表 | ForEach-Object { $_.Video.PixFmt })
    $视频帧率原始值集合 = @($媒体信息列表 | ForEach-Object { $_.Video.FpsRaw })
    $视频帧率数值集合 = @($媒体信息列表 | ForEach-Object { $_.Video.Fps })

    $音频采样率值集合 = @($媒体信息列表 | ForEach-Object { if ($_.Audio) { $_.Audio.SampleRate } else { $null } })
    $音频声道数值集合   = @($媒体信息列表 | ForEach-Object { if ($_.Audio) { $_.Audio.Channels } else { $null } })

    $唯一视频编码 = @($视频编码集合 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
    $唯一视频宽度   = @($视频宽度值集合  | Where-Object { $null -ne $_ } | Select-Object -Unique)
    $唯一视频高度  = @($视频高度值集合 | Where-Object { $null -ne $_ } | Select-Object -Unique)
    $唯一视频像素格式  = 取_非空字符串去重 -Values $视频像素格式值集合
    $唯一视频帧率原始  = 取_非空字符串去重 -Values $视频帧率原始值集合

    $视频编码兼容  = ($唯一视频编码.Count -eq 1)
    $视频宽度兼容  = ($唯一视频宽度.Count -eq 1)
    $视频高度兼容 = ($唯一视频高度.Count -eq 1)
    $视频像素格式兼容 = ($唯一视频像素格式.Count -eq 1)
    $视频帧率兼容    = ($唯一视频帧率原始.Count -eq 1)

    $视频分辨率兼容 = ($视频宽度兼容 -and $视频高度兼容)
    $视频分辨率可动态变更 = $false
    if ((-not $视频分辨率兼容) -and $视频编码兼容 -and $唯一视频编码.Count -eq 1) {
        $视频分辨率可动态变更 = 测试_视频编码是否支持动态分辨率 -Codec $唯一视频编码[0]
    }

    $视频可直接拷贝 = ($视频编码兼容 -and ($视频分辨率兼容 -or $视频分辨率可动态变更) -and $视频像素格式兼容 -and $视频帧率兼容)

    $audioCodecCompatible = $false
    $audioSrCompatible = $false
    $audioChCompatible = $false
    $canCopyAudio = $false

    if ($全部有音轨) {
        $uniqACodec = @($音频编码集合 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
        $uniqASr = @($音频采样率值集合 | Where-Object { $null -ne $_ } | Select-Object -Unique)
        $uniqACh = @($音频声道数值集合   | Where-Object { $null -ne $_ } | Select-Object -Unique)

        $audioCodecCompatible = ($uniqACodec.Count -eq 1)
        $audioSrCompatible    = ($uniqASr.Count -eq 1)
        $audioChCompatible    = ($uniqACh.Count -eq 1)
        $canCopyAudio         = ($audioCodecCompatible -and $audioSrCompatible -and $audioChCompatible)
    }

    # 目标输出参数:兼容→保留(取首个);不兼容→统一为 hevc(无论 Min/Max)
    $目标视频编码 = if ($视频编码兼容) { $唯一视频编码[0] } else { 'hevc' }
    $目标视频宽度 = if ($视频宽度兼容) { [int64]$视频宽度值集合[0] } else { [int64](取_极值或空 -Values $视频宽度值集合 -Mode $不兼容策略) }
    $目标视频高度 = if ($视频高度兼容) { [int64]$视频高度值集合[0] } else { [int64](取_极值或空 -Values $视频高度值集合 -Mode $不兼容策略) }

    $目标视频帧率数值 = if ($视频帧率兼容) {
        [double]($视频帧率数值集合 | Where-Object { $null -ne $_ } | Select-Object -First 1)
    } else {
        [double](取_极值或空 -Values $视频帧率数值集合 -Mode $不兼容策略)
    }
    if (-not $目标视频帧率数值 -or $目标视频帧率数值 -le 0) { $目标视频帧率数值 = 24 }

    $目标视频像素格式 = if ($视频像素格式兼容) { $唯一视频像素格式[0] } else { 'yuv420p' }

    $目标音频采样率 = if ($audioSrCompatible) {
        [int64]($音频采样率值集合 | Where-Object { $null -ne $_ } | Select-Object -First 1)
    } else {
        [int64](取_极值或空 -Values $音频采样率值集合 -Mode $不兼容策略)
    }
    if (-not $目标音频采样率 -or $目标音频采样率 -le 0) { $目标音频采样率 = 48000 }

    $目标音频声道数 = if ($audioChCompatible) {
        [int64]($音频声道数值集合 | Where-Object { $null -ne $_ } | Select-Object -First 1)
    } else {
        [int64](取_极值或空 -Values $音频声道数值集合 -Mode $不兼容策略)
    }
    if (-not $目标音频声道数 -or $目标音频声道数 -le 0) { $目标音频声道数 = 2 }

    # 目标音频编码:若全体音频参数兼容则保留原编码;否则统一到 aac
    $存在无损输入音频 = $false
    if ($全部有音轨) {
        $allAudioCodecs = @($媒体信息列表 | ForEach-Object { if ($_.Audio) { $_.Audio.Codec } else { $null } })
        $allAudioNonEmpty = @($allAudioCodecs | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
        if ($allAudioNonEmpty.Count -eq $媒体信息列表.Count) {
            $lossless = @($allAudioNonEmpty | Where-Object { 测试_音频编码是否无损 -Codec $_ })
            $存在无损输入音频 = ($lossless.Count -gt 0)
        }
    }

    $目标音频编码 = if ($全部有音轨 -and $audioCodecCompatible -and $audioSrCompatible -and $audioChCompatible) {
        $uniqACodec[0]
    } else {
        if ($不兼容策略 -eq 'Max' -and $存在无损输入音频) {
            # Max 模式:只要存在无损输入且需要重编码(参数不兼容),则确保无损输入片段不会被转为有损。
            # 由于 concat 要求所有分段音频编码一致,这里选择无损目标编码(会导致有损输入也被转为无损封装,但不会进一步损失)。
            # 若全体 codec 已一致且为 flac/alac,则沿用;否则优先 flac(通用、压缩比好)。
            $uniqLosslessCodec = @(
                @($媒体信息列表 | ForEach-Object { if ($_.Audio) { $_.Audio.Codec } else { $null } }) |
                    Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                    ForEach-Object { $_.ToLowerInvariant() } |
                    Select-Object -Unique
            )
            if ($uniqLosslessCodec.Count -eq 1 -and $uniqLosslessCodec[0] -in @('flac','alac')) {
                $uniqLosslessCodec[0]
            } else {
                'flac'
            }
        } else {
            'aac'
        }
    }

    # bitrate 不是 concat copy 的兼容性参数;转码时优先“每段沿用该段输入码率”,全局 Min/Max 仅作为兜底。
    $全局备用视频码率 = 取_极值或空 -Values ($媒体信息列表 | ForEach-Object { $_.Video.BitRate }) -Mode $不兼容策略
    $全局备用音频码率 = 取_极值或空 -Values ($媒体信息列表 | ForEach-Object { if ($_.Audio) { $_.Audio.BitRate } else { $null } }) -Mode $不兼容策略

    # 仅在“需要重新编码视频”时使用:一律优先选择可用 GPU 编码器(NVENC/QSV/AMF)
    $选定视频编码器 = 选择_视频编码器 -目标视频编码 $目标视频编码
    $转码视频编码器名 = [string]$选定视频编码器.Encoder

    $需要标准化视频 = -not $视频可直接拷贝
    $需要标准化音频 = -not $canCopyAudio

    # Max 模式无损音频输出可能与容器不兼容(例如 mp4 不支持 flac)。此处确保“实际容器”可承载目标音频/字幕。
    $容器需要修正 = $false
    if ($存在字幕 -and (-not (测试_容器是否支持_字幕编码 -容器扩展名 $实际输出扩展名 -字幕编码 $目标字幕编码))) { $容器需要修正 = $true }
    if (-not (测试_容器是否支持_音频编码 -容器扩展名 $实际输出扩展名 -音频编码 $目标音频编码)) { $容器需要修正 = $true }

    if ($容器需要修正 -and $实际输出扩展名 -ne '.mkv') {
        $oldExt = $实际输出扩展名
        $实际输出扩展名 = '.mkv'

        # 重新计算输出文件名(规则不变:用户指定扩展名时最终仍保持用户扩展名,仅内部容器改为 mkv)
        if ($用户明确指定输出文件) {
            $finalDir = Split-Path -Parent $用户输出文件
            if ([string]::IsNullOrWhiteSpace($finalDir)) { $finalDir = (Get-Location).Path }
            $finalBase = [System.IO.Path]::GetFileNameWithoutExtension($用户输出文件)
            $finalExt = [System.IO.Path]::GetExtension($用户输出文件)
            if ([string]::IsNullOrWhiteSpace($finalExt)) {
                $最终输出文件 = Join-Path $finalDir ($finalBase + $实际输出扩展名)
            } else {
                $最终输出文件 = $用户输出文件
            }
            $内部输出文件 = Join-Path $finalDir ($finalBase + $实际输出扩展名)
        } else {
            $d = Split-Path -Parent $默认输出文件
            if ([string]::IsNullOrWhiteSpace($d)) { $d = (Get-Location).Path }
            $b = [System.IO.Path]::GetFileNameWithoutExtension($默认输出文件)
            $最终输出文件 = Join-Path $d ($b + $实际输出扩展名)
            $内部输出文件 = $最终输出文件
        }

        $输出文件 = $内部输出文件

        Write-Host ("检测到目标音频/字幕与容器 {0} 不兼容,已将实际输出容器切换为 {1}(最终扩展名规则不变)" -f $oldExt, $实际输出扩展名) -ForegroundColor Yellow
    }

    # 字幕规则(按目标字幕编码):
    $subtitleCopyPossible = $false
    if ($存在字幕) {
        if ($目标字幕为位图) {
            # 位图字幕在外部裁切工具已强制要求全部相同,必然可 copy
            $subtitleCopyPossible = $true
        } else {
            $allHaveSubtitle = ((@($媒体信息列表 | Where-Object { $null -eq $_.Subtitle })).Count -eq 0)
            if ($allHaveSubtitle) {
                $subCodecsAll = @($媒体信息列表 | ForEach-Object { if ($_.Subtitle) { $_.Subtitle.Codec } else { $null } })
                $uniqSubAll = 取_非空字符串去重 -Values $subCodecsAll
                if ($uniqSubAll.Count -eq 1) {
                    $c = $uniqSubAll[0]
                    if ($目标字幕编码 -eq 'ass' -and $c -eq 'ass') { $subtitleCopyPossible = $true }
                    if ($目标字幕编码 -eq 'subrip' -and ($c -in @('subrip','srt'))) { $subtitleCopyPossible = $true }
                }
            }
        }
    }
    $需要转码字幕 = ($存在字幕 -and (-not $subtitleCopyPossible))

    $选取词 = if ($不兼容策略 -eq 'Max') { '最大' } else { '最小' }

    Write-Host "兼容性判定:" -ForegroundColor Cyan
    $视频兼容性摘要 = if ($视频可直接拷贝) {
        if ($视频分辨率可动态变更 -and (-not $视频分辨率兼容)) { "兼容→保留各段原视频参数(copy,动态分辨率)" } else { "兼容→保留原编码/参数(copy)" }
    } else {
        "不兼容→仅标准化不兼容参数"
    }
    Write-Host ("- 视频:" + $视频兼容性摘要)
    Write-Host ("- 音频:" + $(if ($canCopyAudio) { "兼容→保留原编码/参数(copy)" } else { "不兼容/缺失→仅标准化不兼容参数" }))
    if ($存在字幕) {
        $subMsg = if ($目标字幕编码 -eq 'ass') { 'ASS' } elseif ($目标字幕编码 -eq 'subrip') { 'SRT' } else { $目标字幕编码 }
        Write-Host ("- 字幕:" + $(if ($subtitleCopyPossible) { "全部为 $subMsg→copy" } else { "统一转为 $subMsg(含补空字幕)" }))
    }

    if (-not $视频分辨率兼容) {
        $codecText = if ($视频编码兼容 -and $唯一视频编码.Count -eq 1) { $唯一视频编码[0] } else { '当前编码组合' }
        if ($视频分辨率可动态变更 -and $视频可直接拷贝) {
            Write-Host ("视频分辨率不一致:{0};{1} 支持动态分辨率 -> 保留各段原分辨率(copy)" -f (格式化_值列表 -Values $视频分辨率值集合), $codecText) -ForegroundColor Yellow
        } elseif ($视频分辨率可动态变更) {
            Write-Host ("视频分辨率不一致:{0};{1} 支持动态分辨率,但视频仍需因其它参数重编码 -> 选择{2}:{3}x{4}" -f (格式化_值列表 -Values $视频分辨率值集合), $codecText, $选取词, $目标视频宽度, $目标视频高度) -ForegroundColor Yellow
        } else {
            Write-Host ("视频分辨率不兼容:{0} -> 选择{1}:{2}x{3}" -f (格式化_值列表 -Values $视频分辨率值集合), $选取词, $目标视频宽度, $目标视频高度) -ForegroundColor Yellow
        }
    }
    if (-not $视频帧率兼容) {
        Write-Host ("视频帧率不兼容:" + (格式化_值列表 -Values $视频帧率原始值集合) + (" -> 选择{0}:{1:0.###}" -f $选取词, $目标视频帧率数值)) -ForegroundColor Yellow
    }
    if (-not $视频像素格式兼容) {
        Write-Host ("视频像素格式不兼容:" + (格式化_值列表 -Values $视频像素格式值集合) + " -> 选择:$目标视频像素格式") -ForegroundColor Yellow
    }
    if (-not $视频编码兼容) {
        Write-Host ("视频编码不兼容:" + (格式化_值列表 -Values ($媒体信息列表 | ForEach-Object { $_.Video.Codec })) + " -> 输出编码:$转码视频编码器名") -ForegroundColor Yellow
    }

    if (-not $全部有音轨) {
        Write-Host "存在缺失音轨:将补静音并按目标采样率/声道标准化" -ForegroundColor Yellow
    }
    if ($全部有音轨 -and (-not $audioCodecCompatible)) {
        Write-Host ("音频编码不兼容:" + (格式化_值列表 -Values ($媒体信息列表 | ForEach-Object { $_.Audio.Codec })) + " -> 输出编码:$目标音频编码") -ForegroundColor Yellow
    }
    if ($全部有音轨 -and (-not $audioSrCompatible)) {
        Write-Host ("音频采样率不兼容:" + (格式化_值列表 -Values $音频采样率值集合) + (" -> 选择{0}:{1}" -f $选取词, $目标音频采样率)) -ForegroundColor Yellow
    }
    if ($全部有音轨 -and (-not $audioChCompatible)) {
        Write-Host ("音频声道数不兼容:" + (格式化_值列表 -Values $音频声道数值集合) + (" -> 选择{0}:{1}" -f $选取词, $目标音频声道数)) -ForegroundColor Yellow
    }

    $targetVideoBps = $null
    $targetAudioBps = $null

    # 由于现在可能“每段目标码率不同”,这里用时长加权平均生成 BPS_TARGET(用于输出文件 tags)。
    $videoTargetSum = [double]0
    $videoTargetDur = [double]0
    $audioTargetSum = [double]0
    $audioTargetDur = [double]0

    New-Item -ItemType Directory -Force -Path $工作目录 | Out-Null
    $分段目录 = Join-Path $工作目录 'segments'
    $辅助目录 = Join-Path $工作目录 'aux'
    New-Item -ItemType Directory -Force -Path $分段目录 | Out-Null
    New-Item -ItemType Directory -Force -Path $辅助目录 | Out-Null

    Write-Host "标准化分段文件(用于兼容 concat)..." -ForegroundColor Cyan
    Write-Host ("策略:兼容就 copy;不兼容只标准化对应参数(视频/音频/字幕分别决策)")

    $segPaths = @()

    for ($i = 0; $i -lt $媒体信息列表.Count; $i++) {
        $info = $媒体信息列表[$i]
        $inPath = $info.Path
        $index = $i + 1
        $segOut = Join-Path $分段目录 ("seg_{0:0000}.mkv" -f $index)

        $segmentDuration = [double]$info.Duration

        $inputArgs = @('-nostdin','-stats','-i', $inPath)
        $outputArgs = @('-map','0:v:0')
        $nextInputIndex = 1

        if ($null -ne $info.Audio) {
            $outputArgs += @('-map','0:a:0')
        } else {
            $audioInputIndex = $nextInputIndex
            $inputArgs += @('-f','lavfi','-t', ([string]$segmentDuration), '-i', ("anullsrc=r={0}:cl=stereo" -f $目标音频采样率))
            $nextInputIndex++
            $outputArgs += @('-map', ("{0}:a:0" -f $audioInputIndex))
        }

        if ($存在字幕) {
            if ($null -ne $info.Subtitle) {
                $outputArgs += @('-map','0:s:0')
            } else {
                if ($目标字幕编码 -eq 'subrip') {
                    $blank = Join-Path $辅助目录 ("blank_{0:0000}.srt" -f $index)
                    写入_空白SRT -Path $blank -DurationSeconds $segmentDuration
                    $subInputIndex = $nextInputIndex
                    $inputArgs += @('-f','srt','-i', $blank)
                    $nextInputIndex++
                    $outputArgs += @('-map', ("{0}:s:0" -f $subInputIndex))
                } else {
                    $blank = Join-Path $辅助目录 ("blank_{0:0000}.ass" -f $index)
                    写入_空白ASS -Path $blank -DurationSeconds $segmentDuration
                    $subInputIndex = $nextInputIndex
                    $inputArgs += @('-f','ass','-i', $blank)
                    $nextInputIndex++
                    $outputArgs += @('-map', ("{0}:s:0" -f $subInputIndex))
                }
            }
        }

        # ---- 视频:能 copy 就 copy;否则仅为“本段不符合目标参数”的片段做标准化 ----
        $本段视频已符合目标 = $true
        if ($null -eq $info.Video) { $本段视频已符合目标 = $false }
        if ($本段视频已符合目标 -and (-not [string]::IsNullOrWhiteSpace($目标视频编码)) -and ($info.Video.Codec -ne $目标视频编码)) { $本段视频已符合目标 = $false }
        if ($本段视频已符合目标 -and ($info.Video.Width -ne $目标视频宽度)) { $本段视频已符合目标 = $false }
        if ($本段视频已符合目标 -and ($info.Video.Height -ne $目标视频高度)) { $本段视频已符合目标 = $false }
        if ($本段视频已符合目标 -and (-not [string]::IsNullOrWhiteSpace($目标视频像素格式)) -and ($info.Video.PixFmt -ne $目标视频像素格式)) { $本段视频已符合目标 = $false }
        if ($本段视频已符合目标) {
            $fpsNow = [double]$info.Video.Fps
            if (-not $fpsNow -or $fpsNow -le 0) { $本段视频已符合目标 = $false }
            elseif ([math]::Abs($fpsNow - [double]$目标视频帧率数值) -gt 0.0001) { $本段视频已符合目标 = $false }
        }

        $本段需要标准化视频 = ($需要标准化视频 -and (-not $本段视频已符合目标))

        if ($本段需要标准化视频) {
            $vfParts = @()
            if (-not ($视频宽度兼容 -and $视频高度兼容)) {
                $vfParts += ("scale=w={0}:h={1}:force_original_aspect_ratio=decrease,pad={0}:{1}:(ow-iw)/2:(oh-ih)/2" -f $目标视频宽度, $目标视频高度)
            }
            if (-not $视频帧率兼容) {
                $vfParts += ("fps={0:0.###}" -f [double]$目标视频帧率数值)
            }

            # 需要重新编码时:优先使用 GPU(若可用),否则回退 CPU
            $vEnc = $转码视频编码器名
            $outputArgs += @('-c:v', $vEnc)

            if ($vEnc -eq 'libx264') {
                $outputArgs += @('-preset','veryfast','-crf','23')
            } elseif ($vEnc -eq 'libx265') {
                # hevc 的 CRF 经验值通常更高一些
                $outputArgs += @('-preset','medium','-crf','28')
            } elseif ($vEnc -like '*_nvenc') {
                # NVENC:默认用 CQ(类 CRF);若指定了目标码率则使用 VBR
                $outputArgs += @('-preset','p5')
            } elseif ($vEnc -like '*_qsv') {
                # QSV:默认用 global_quality;若指定码率则直接按码率控制
                # (不强加 preset 参数,避免不同版本 ffmpeg 行为差异)
            } elseif ($vEnc -like '*_amf') {
                # AMF:默认用 CQP;若指定码率则按码率控制
            }

            # 尽量保留兼容的像素格式;否则回退 yuv420p
            $pix = $目标视频像素格式
            $supportedPix = @('yuv420p','yuv420p10le')
            if ([string]::IsNullOrWhiteSpace($pix) -or ($supportedPix -notcontains $pix)) {
                $pix = 'yuv420p'
            }
            $outputArgs += @('-pix_fmt', $pix)

            if ($vfParts.Count -gt 0) { $outputArgs += @('-vf', ($vfParts -join ',')) }

            $本段视频目标码率 = $info.Video.BitRate
            if (-not $本段视频目标码率 -or $本段视频目标码率 -le 0) {
                try {
                    $est = 获取_媒体码率估算 -Path $inPath
                    if ($est -and $est.VideoBps -and $est.VideoBps -gt 0) { $本段视频目标码率 = [int64]$est.VideoBps }
                } catch {
                    $global:LASTEXITCODE = 0
                }
            }
            if (-not $本段视频目标码率 -or $本段视频目标码率 -le 0) {
                $本段视频目标码率 = $全局备用视频码率
            }

            # 简单合理性钳制,避免异常值影响编码器
            if ($本段视频目标码率 -and $本段视频目标码率 -gt 0) {
                $minV = [int64]100000
                $maxV = [int64]200000000
                if ($本段视频目标码率 -lt $minV) { $本段视频目标码率 = $minV }
                if ($本段视频目标码率 -gt $maxV) { $本段视频目标码率 = $maxV }
            }

            if ($null -ne $本段视频目标码率 -and $本段视频目标码率 -gt 0) {
                $maxrate = [int64]$本段视频目标码率
                $buf = [int64]($maxrate * 2)
                # 统一用 bitrate 约束(不同编码器都支持 -b:v/-maxrate/-bufsize)
                $outputArgs += @('-b:v', "$maxrate", '-maxrate', "$maxrate", '-bufsize', "$buf")
                if ($vEnc -like '*_nvenc') {
                    $outputArgs += @('-rc','vbr')
                }

                if ($segmentDuration -gt 0) {
                    $videoTargetSum += ([double]$maxrate * [double]$segmentDuration)
                    $videoTargetDur += [double]$segmentDuration
                }
            } else {
                # 未能从输入探测到码率:改用“质量模式”的默认参数
                if ($vEnc -like '*_nvenc') {
                    $outputArgs += @('-rc','vbr','-cq','23')
                } elseif ($vEnc -like '*_qsv') {
                    $outputArgs += @('-global_quality','23')
                } elseif ($vEnc -like '*_amf') {
                    $outputArgs += @('-rc','cqp','-qp_i','23','-qp_p','23','-qp_b','23')
                }
            }
        } else {
            $outputArgs += @('-c:v','copy')
        }

        # ---- 音频:能 copy 就 copy;否则仅为“本段不符合目标参数”的片段做标准化(含缺失补静音) ----
        $segmentHasAudio = ($null -ne $info.Audio)
        $本段音频已符合目标 = $segmentHasAudio
        if ($本段音频已符合目标 -and (-not [string]::IsNullOrWhiteSpace($目标音频编码)) -and ($info.Audio.Codec -ne $目标音频编码)) { $本段音频已符合目标 = $false }
        if ($本段音频已符合目标 -and ($info.Audio.SampleRate -ne $目标音频采样率)) { $本段音频已符合目标 = $false }
        if ($本段音频已符合目标 -and ($info.Audio.Channels -ne $目标音频声道数)) { $本段音频已符合目标 = $false }

        $本段需要标准化音频 = ((-not $segmentHasAudio) -or ($需要标准化音频 -and (-not $本段音频已符合目标)))

        if ($本段需要标准化音频) {
            $outputArgs += @('-c:a', $目标音频编码, '-ar', "$目标音频采样率", '-ac', "$目标音频声道数")
            if ($目标音频编码 -eq 'aac') {
                $本段音频目标码率 = if ($info.Audio) { $info.Audio.BitRate } else { $null }
                if (-not $本段音频目标码率 -or $本段音频目标码率 -le 0) {
                    try {
                        $est = 获取_媒体码率估算 -Path $inPath
                        if ($est -and $est.AudioBps -and $est.AudioBps -gt 0) { $本段音频目标码率 = [int64]$est.AudioBps }
                    } catch {
                        $global:LASTEXITCODE = 0
                    }
                }
                if (-not $本段音频目标码率 -or $本段音频目标码率 -le 0) {
                    $本段音频目标码率 = $全局备用音频码率
                }

                if ($本段音频目标码率 -and $本段音频目标码率 -gt 0) {
                    $minA = [int64]64000
                    $maxA = [int64]512000
                    if ($本段音频目标码率 -lt $minA) { $本段音频目标码率 = $minA }
                    if ($本段音频目标码率 -gt $maxA) { $本段音频目标码率 = $maxA }
                }

                if ($null -ne $本段音频目标码率 -and $本段音频目标码率 -gt 0) {
                    $abr = [int64]$本段音频目标码率
                    $outputArgs += @('-b:a', "$abr")
                    if ($segmentDuration -gt 0) {
                        $audioTargetSum += ([double]$abr * [double]$segmentDuration)
                        $audioTargetDur += [double]$segmentDuration
                    }
                } else {
                    $outputArgs += @('-b:a','128k')
                    if ($segmentDuration -gt 0) {
                        $audioTargetSum += (128000.0 * [double]$segmentDuration)
                        $audioTargetDur += [double]$segmentDuration
                    }
                }
            }
        } else {
            $outputArgs += @('-c:a','copy')
        }

        # ---- 字幕:全部 ass 且都有字幕 → copy;否则统一 ass(含补空字幕) ----
        if ($存在字幕) {
            if ($需要转码字幕) {
                if ($目标字幕编码 -eq 'subrip') {
                    $outputArgs += @('-c:s','subrip')
                } else {
                    $outputArgs += @('-c:s','ass')
                }
            } else {
                $outputArgs += @('-c:s','copy')
            }
        }

        # 强制每段 pts 归零:外部裁切工具裁切工具若未重置时间戳,段内包 pts 可能仍是原片绝对时间,
        # 会导致 concat demuxer 按末尾 pts 计算时长,把输出撑到原片级别时长。
        # +genpts 允许在缺失 pts 时重建;make_zero 把最小 pts 偏移到 0。
        $outputArgs += @('-fflags','+genpts','-avoid_negative_ts','make_zero')

        $outputArgs += @('-y', $segOut)

        Write-Host ("[{0}/{1}] {2}" -f $index, $媒体信息列表.Count, (Split-Path -Leaf $inPath))
        调用_外部命令 -Exe 'ffmpeg' -ArgumentList @($inputArgs + $outputArgs) | Out-Null

        $segPaths += ([System.IO.Path]::GetFullPath($segOut))
    }

    Write-Host "生成 concat 列表并合并..." -ForegroundColor Cyan
    $concatList = Join-Path $工作目录 'concat_list.txt'
    $lines = foreach ($p in $segPaths) { "file '$(转义_ConCat路径 -Path $p)'" }
    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllLines($concatList, $lines, $utf8NoBom)


    $finalArgs = @('-nostdin','-stats','-f','concat','-safe','0','-i',$concatList,'-map','0:v:0','-map','0:a:0')
    if ($存在字幕) { $finalArgs += @('-map','0:s:0') }
    $finalArgs += @('-c','copy','-y',$输出文件)

    调用_外部命令 -Exe 'ffmpeg' -ArgumentList $finalArgs | Out-Null
    Write-Host "完成:$输出文件" -ForegroundColor Green

    if ($videoTargetDur -gt 0) { $targetVideoBps = [int64]([math]::Round($videoTargetSum / $videoTargetDur)) }
    if ($audioTargetDur -gt 0) { $targetAudioBps = [int64]([math]::Round($audioTargetSum / $audioTargetDur)) }

    try {
        $pd = 获取_输出探测数据_快速 -Path $输出文件
        $fpsStr = [string]$pd.VideoFps

        try {
            通过Remux写入_流标签 -InputPath $输出文件 -OutputPath $输出文件 `
                -VideoBps $pd.VideoBps -AudioBps $pd.AudioBps -VideoTargetBps $targetVideoBps -AudioTargetBps $targetAudioBps -DurationSeconds $pd.DurationSeconds -VideoFps $fpsStr -VideoBytes $pd.VideoBytes -AudioBytes $pd.AudioBytes `
                -AudioLanguage $pd.AudioLanguage -SubtitleLanguage $pd.SubtitleLanguage
        } catch {
            Write-Warning "写入 stream tags 失败(不影响合并结果):$($_.Exception.Message)"
        }

        if (-not [string]::IsNullOrWhiteSpace($fpsStr)) {
            try { 尝试_修正Matroska_FrameRate元素 -Path $输出文件 -Fps $fpsStr | Out-Null } catch { Write-Warning "修正 Matroska FrameRate 失败:$($_.Exception.Message)" }
            try { 尝试_修正Matroska_DefaultDuration -Path $输出文件 -Fps $fpsStr | Out-Null } catch { Write-Warning "修正 Matroska DefaultDuration 失败:$($_.Exception.Message)" }
        }
    } catch {
        Write-Warning "写入输出文件元数据失败(不影响合并结果):$($_.Exception.Message)"
    }

    # 若内部输出文件与用户指定输出文件不同:最终移动成用户指定的文件名(扩展名保持用户要求)。
    if ($输出文件 -ne $最终输出文件) {
        try {
            Move-Item -LiteralPath $输出文件 -Destination $最终输出文件 -Force
            $输出文件 = $最终输出文件
            Write-Host ("已将结果文件命名为:{0}(注意:容器实际为 {1})" -f $输出文件, $实际输出扩展名) -ForegroundColor Yellow
        } catch {
            Write-Warning "移动结果文件失败:$($_.Exception.Message)"
        }
    }

    if (-not $详细模式) {
        try { Remove-Item -LiteralPath $工作目录 -Recurse -Force -ErrorAction SilentlyContinue } catch { }
    } else {
        Write-Host "已保留临时目录:$工作目录" -ForegroundColor Yellow
    }

    # 防止中途某次外部命令失败被捕获后遗留 $LASTEXITCODE,导致 powershell.exe 进程退出码非 0。
    $global:LASTEXITCODE = 0
}