private/通用过程.ps1
|
function 断言_命令存在 { param([Parameter(Mandatory)] [string]$Name) if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { throw "未找到命令:$Name。请先安装并确保 $Name 在 PATH 中。" } } function 测试_管理员权限 { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) return $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } function 刷新_会话PATH { $m = [Environment]::GetEnvironmentVariable('Path','Machine') $u = [Environment]::GetEnvironmentVariable('Path','User') $env:Path = @($m,$u | Where-Object { $_ }) -join ';' } function 添加_机器PATH { param([Parameter(Mandatory)][string]$Dir) $cur = [Environment]::GetEnvironmentVariable('Path','Machine') $parts = @() if ($cur) { $parts = $cur.Split(';') | Where-Object { $_ } } if ($parts -notcontains $Dir) { $new = (@($parts) + $Dir) -join ';' [Environment]::SetEnvironmentVariable('Path', $new, 'Machine') } 刷新_会话PATH } function 安装_ffmpeg_通过Winget { $winget = Get-Command winget -ErrorAction SilentlyContinue if (-not $winget) { return $false } Write-Host "正在通过 winget 为所有用户安装 ffmpeg (Gyan.FFmpeg)..." -ForegroundColor Cyan $wingetArgs = @( 'install','--id','Gyan.FFmpeg','--source','winget', '--scope','machine','--silent', '--accept-package-agreements','--accept-source-agreements' ) try { Start-Process -FilePath $winget.Source -ArgumentList $wingetArgs -Wait -NoNewWindow | Out-Null # winget 对“已安装/无更新”会返回非零;放宽判定:看命令可用性 } catch { Write-Warning "winget 执行失败:$($_.Exception.Message)" return $false } 刷新_会话PATH return [bool](Get-Command ffmpeg -ErrorAction SilentlyContinue) } function 安装_ffmpeg_通过下载 { $installRoot = Join-Path $env:ProgramFiles 'ffmpeg' $binDir = Join-Path $installRoot 'bin' $tmpZip = Join-Path $env:TEMP 'ffmpeg-release-essentials.zip' $tmpExtract = Join-Path $env:TEMP ("ffmpeg-extract-" + [Guid]::NewGuid().ToString('N')) $url = 'https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip' Write-Host "正在从 gyan.dev 下载 ffmpeg 静态构建包到 $tmpZip ..." -ForegroundColor Cyan $oldPref = $ProgressPreference try { $ProgressPreference = 'SilentlyContinue' Invoke-WebRequest -Uri $url -OutFile $tmpZip -UseBasicParsing } finally { $ProgressPreference = $oldPref } Write-Host "正在解压到 $installRoot ..." -ForegroundColor Cyan if (-not (Test-Path $tmpExtract)) { New-Item -ItemType Directory -Path $tmpExtract -Force | Out-Null } Expand-Archive -LiteralPath $tmpZip -DestinationPath $tmpExtract -Force $extracted = Get-ChildItem -Path $tmpExtract -Directory | Select-Object -First 1 if (-not $extracted) { throw "ffmpeg 压缩包内容异常,未发现顶层目录。" } if (Test-Path $installRoot) { # 覆盖安装:先清理 bin/presets 等子目录,避免残留 Get-ChildItem -Path $installRoot -Force | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } else { New-Item -ItemType Directory -Path $installRoot -Force | Out-Null } Get-ChildItem -Path $extracted.FullName -Force | Move-Item -Destination $installRoot -Force Remove-Item -Path $tmpZip -Force -ErrorAction SilentlyContinue Remove-Item -Path $tmpExtract -Recurse -Force -ErrorAction SilentlyContinue if (-not (Test-Path (Join-Path $binDir 'ffmpeg.exe'))) { throw "下载安装失败:未找到 $binDir\ffmpeg.exe。" } 添加_机器PATH -Dir $binDir return $true } function 获取_提权安装脚本路径 { param([Parameter(Mandatory)] [string]$文件名) $脚本路径 = Join-Path -Path $script:视频工坊根目录 -ChildPath ("private\提权安装\{0}" -f $文件名) if (-not (Test-Path -LiteralPath $脚本路径)) { throw "提权安装脚本不存在:$脚本路径" } return $脚本路径 } function 调用_提权安装脚本 { param( [Parameter(Mandatory)] [string]$文件名, [Parameter(Mandatory)] [string]$提示, [Parameter(Mandatory)] [string]$失败消息 ) $脚本路径 = 获取_提权安装脚本路径 -文件名 $文件名 $exe = (Get-Process -Id $PID).Path if (-not $exe) { $exe = 'powershell.exe' } $procArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"{0}"' -f $脚本路径)) Write-Host $提示 -ForegroundColor Yellow $进程 = Start-Process -FilePath $exe -ArgumentList $procArgs -Verb RunAs -Wait -PassThru if ($进程.ExitCode -ne 0) { throw ("{0}(ExitCode={1})。" -f $失败消息, $进程.ExitCode) } 刷新_会话PATH } function 以管理员执行_安装ffmpeg { 调用_提权安装脚本 ` -文件名 '安装ffmpeg.ps1' ` -提示 '需要管理员权限以为所有用户安装 ffmpeg,将弹出 UAC 提示...' ` -失败消息 '提权安装 ffmpeg 失败' } function 安装_mkvmerge_通过Winget { $winget = Get-Command winget -ErrorAction SilentlyContinue if (-not $winget) { return $false } Write-Host "正在通过 winget 为所有用户安装 MKVToolNix (MoritzBunkus.MKVToolNix)..." -ForegroundColor Cyan $wingetArgs = @( 'install','--id','MoritzBunkus.MKVToolNix','--source','winget', '--scope','machine','--silent', '--accept-package-agreements','--accept-source-agreements' ) try { Start-Process -FilePath $winget.Source -ArgumentList $wingetArgs -Wait -NoNewWindow | Out-Null } catch { Write-Warning "winget 执行失败:$($_.Exception.Message)" return $false } 刷新_会话PATH # MKVToolNix 默认安装到 %ProgramFiles%\MKVToolNix,未必会自动加到 PATH,主动补一把 $defaultBin = Join-Path $env:ProgramFiles 'MKVToolNix' if (Test-Path (Join-Path $defaultBin 'mkvmerge.exe')) { 添加_机器PATH -Dir $defaultBin } return [bool](Get-Command mkvmerge -ErrorAction SilentlyContinue) } function 以管理员执行_安装mkvmerge { 调用_提权安装脚本 ` -文件名 '安装mkvmerge.ps1' ` -提示 '需要管理员权限以为所有用户安装 MKVToolNix,将弹出 UAC 提示...' ` -失败消息 '提权安装 MKVToolNix 失败' } function 确保_mkvmerge_可用 { 刷新_会话PATH if (Get-Command mkvmerge -ErrorAction SilentlyContinue) { return } Write-Host "检测到缺少 mkvmerge,将尝试自动安装 MKVToolNix(机器级,对所有 Windows 用户可用)。" -ForegroundColor Yellow if (测试_管理员权限) { [void](安装_mkvmerge_通过Winget) } else { 以管理员执行_安装mkvmerge } 刷新_会话PATH if (-not (Get-Command mkvmerge -ErrorAction SilentlyContinue)) { throw "自动安装完成但仍未找到 mkvmerge。请重启 PowerShell 会话后重试,或手动从 https://mkvtoolnix.download 安装。" } Write-Host "mkvmerge 已就绪。" -ForegroundColor Green } function 确保_ffmpeg_ffprobe_可用 { # 先从注册表刷新会话 PATH,吸收 Machine/User 级别的最新变更 # (典型场景:父终端是在 ffmpeg 安装之前启动的,进程环境里的 PATH 已陈旧) 刷新_会话PATH $need = @(@('ffmpeg','ffprobe') | Where-Object { -not (Get-Command $_ -ErrorAction SilentlyContinue) }) if ($need.Count -eq 0) { return } Write-Host "检测到缺少命令:$($need -join ', '),将尝试自动安装(机器级,对所有 Windows 用户可用)。" -ForegroundColor Yellow if (测试_管理员权限) { if (-not (安装_ffmpeg_通过Winget)) { [void](安装_ffmpeg_通过下载) } } else { 以管理员执行_安装ffmpeg } 刷新_会话PATH foreach ($n in @('ffmpeg','ffprobe')) { if (-not (Get-Command $n -ErrorAction SilentlyContinue)) { throw "自动安装完成但仍未找到 $n。请手动检查 PATH,或重启 PowerShell 会话后重试。" } } Write-Host "ffmpeg / ffprobe 已就绪。" -ForegroundColor Green } function 获取_ffmpeg_编码器集合 { $cached = Get-Variable -Name 'ffmpeg编码器集合' -Scope Script -ErrorAction SilentlyContinue if ($cached -and $null -ne $cached.Value) { return $script:ffmpeg编码器集合 } $res = 调用_外部命令 -Exe 'ffmpeg' -ArgumentList @('-hide_banner','-encoders') -CaptureOutput $set = New-Object 'System.Collections.Generic.HashSet[string]' foreach ($line in ($res.StdOut -split "`r?`n")) { # 示例:" V..... h264_nvenc NVIDIA NVENC H.264 encoder" if ($line -match '^\s*[A-Z\.]{6}\s+([0-9A-Za-z_]+)\s+') { [void]$set.Add($matches[1]) } } $script:ffmpeg编码器集合 = $set return $set } function 选择_视频编码器 { param( [Parameter(Mandatory)] [string]$目标视频编码 ) $codec = $目标视频编码.ToLowerInvariant() $encSet = 获取_ffmpeg_编码器集合 $gpuCandidates = @() $cpuFallback = $null switch ($codec) { 'h264' { $gpuCandidates = @('h264_nvenc','h264_qsv','h264_amf') $cpuFallback = 'libx264' } 'hevc' { $gpuCandidates = @('hevc_nvenc','hevc_qsv','hevc_amf') $cpuFallback = 'libx265' } default { # 其它编码:尽量保持 CPU 回退策略 $cpuFallback = 'libx264' } } # 一律优先 GPU(NVENC/QSV/AMF) foreach ($cand in $gpuCandidates) { if ($encSet.Contains($cand)) { return [pscustomobject]@{ Encoder = $cand; IsGpu = $true } } } Write-Warning "未检测到可用 GPU 编码器(NVENC/QSV/AMF)。将回退到 CPU 编码:$cpuFallback" return [pscustomobject]@{ Encoder = $cpuFallback; IsGpu = $false } } function 调用_外部命令 { param( [Parameter(Mandatory)] [string]$Exe, [Parameter(Mandatory)] [string[]]$ArgumentList, [switch]$CaptureOutput, [switch]$显示捕获输出, # 允许视为"成功"的额外退出码(除 0 外)。mkvmerge 约定 1=仅警告、2=错误,所以调用它时传 @(1)。 [int[]]$AllowedExitCodes = @() ) $oldEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { if ($script:输出外部进程信息 -and -not $CaptureOutput) { & $Exe @ArgumentList $exit = $LASTEXITCODE if ($exit -ne 0 -and ($AllowedExitCodes -notcontains $exit)) { throw "外部命令失败:$Exe $($ArgumentList -join ' ') (exit=$exit)" } return [pscustomobject]@{ StdOut = ''; StdErr = '' } } $all = & $Exe @ArgumentList 2>&1 $exit = $LASTEXITCODE $text = ($all | Out-String) if ($script:输出外部进程信息 -and $CaptureOutput -and $显示捕获输出 -and -not [string]::IsNullOrWhiteSpace($text)) { Write-Host $text } if ($exit -ne 0 -and ($AllowedExitCodes -notcontains $exit)) { throw "外部命令失败:$Exe $($ArgumentList -join ' ') (exit=$exit)`n$text" } return [pscustomobject]@{ StdOut = $text; StdErr = '' } } finally { $ErrorActionPreference = $oldEap } } function 取_对象属性值 { param( [Parameter(Mandatory)] $Obj, [Parameter(Mandatory)] [string]$Name ) if ($null -eq $Obj) { return $null } $p = $Obj.PSObject.Properties[$Name] if ($null -eq $p) { return $null } return $p.Value } function 解析_整数或空 { param([string]$s) if ([string]::IsNullOrWhiteSpace($s)) { return $null } try { return [int64]$s } catch { return $null } } function 解析_有理数 { param([string]$r) if ([string]::IsNullOrWhiteSpace($r)) { return $null } if ($r -match '^\s*(\d+)\s*/\s*(\d+)\s*$') { $n = [double]$matches[1] $d = [double]$matches[2] if ($d -eq 0) { return $null } return $n / $d } if ($r -match '^\s*\d+(\.\d+)?\s*$') { return [double]$r } return $null } function 格式化_值列表 { param([Parameter(Mandatory)] [object[]]$Values) $s = @($Values | ForEach-Object { if ($null -eq $_) { 'N/A' } else { [string]$_ } }) return ($s -join ', ') } function 取_非空字符串去重 { param([object[]]$Values) $arr = @( $Values | ForEach-Object { if ($null -eq $_) { $null } else { ([string]$_).Trim() } } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique ) return ,$arr } function 取_按流序号统计包字节数 { param( [Parameter(Mandatory)] [string]$Path, [int[]]$StreamIndices ) # 性能优化:一次 show_packets 扫描同时统计视频/音频 packet.size(避免扫两遍)。 $need = @($StreamIndices | Where-Object { $null -ne $_ } | Select-Object -Unique) if ($need.Count -eq 0) { return @{} } $res = 调用_外部命令 -Exe 'ffprobe' -ArgumentList @( '-v','error', '-show_packets', '-show_entries','packet=stream_index,size', '-of','compact=p=0:nk=1', $Path ) -CaptureOutput $sums = @{} foreach ($i in $need) { $sums[[int]$i] = [int64]0 } foreach ($line in ($res.StdOut -split "`r?`n")) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $t = $line.Trim() # compact nk=1 通常输出:"<stream_index>|<size>" $parts = $t -split '\|' if ($parts.Count -lt 2) { $parts = $t -split ',' } if ($parts.Count -lt 2) { continue } $si = 0 $sz = [int64]0 if (-not [int]::TryParse($parts[0].Trim(), [ref]$si)) { continue } if (-not [int64]::TryParse($parts[1].Trim(), [ref]$sz)) { continue } if ($sums.ContainsKey($si)) { $sums[$si] = [int64]($sums[$si] + $sz) } } return $sums } function 取_最小值或空 { param([object[]]$Values) $vals = @($Values | Where-Object { $null -ne $_ }) if ($vals.Count -eq 0) { return $null } return ($vals | Measure-Object -Minimum).Minimum } function 取_极值或空 { param( [AllowNull()] [object[]]$Values, [Parameter(Mandatory)] [ValidateSet('Min','Max')] [string]$Mode ) if ($null -eq $Values) { return $null } $vals = @($Values | Where-Object { $null -ne $_ }) if ($vals.Count -eq 0) { return $null } if ($Mode -eq 'Max') { return ($vals | Measure-Object -Maximum).Maximum } return ($vals | Measure-Object -Minimum).Minimum } function 转换_码率到bps { param([Parameter(Mandatory)] [string]$Value) $v = $Value.Trim() if ($v -match '^\d+$') { return [int64]$v } if ($v -match '^(\d+(?:\.\d+)?)\s*([kKmMgG])$') { $num = [double]$matches[1] $unit = $matches[2].ToLowerInvariant() switch ($unit) { 'k' { return [int64]([math]::Round($num * 1000)) } 'm' { return [int64]([math]::Round($num * 1000 * 1000)) } 'g' { return [int64]([math]::Round($num * 1000 * 1000 * 1000)) } } } return $null } function 转换_fps到DefaultDuration_ns { param([Parameter(Mandatory)] [string]$Fps) $s = $Fps.Trim() if ($s -match '^(\d+)\s*/\s*(\d+)$') { $num = [double]$matches[1] $den = [double]$matches[2] if ($num -le 0 -or $den -le 0) { return $null } return [int64]([math]::Round(($den * 1000000000.0) / $num)) } if ($s -match '^(\d+(?:\.\d+)?)$') { $num = [double]$matches[1] if ($num -le 0) { return $null } return [int64]([math]::Round(1000000000.0 / $num)) } return $null } function 获取_可执行命令路径 { param([Parameter(Mandatory)] [string]$名称) $命令 = Get-Command $名称 -ErrorAction SilentlyContinue if (-not $命令) { return $null } return $命令.Source } function 测试_ffmpeg过滤器可用 { param( [Parameter(Mandatory)] [string]$FfmpegPath, [Parameter(Mandatory)] [string]$过滤器名称 ) try { $结果 = 调用_外部命令 -Exe $FfmpegPath -ArgumentList @('-hide_banner', '-filters') -CaptureOutput } catch { return $false } return ($结果.StdOut -match ("(?m)^\s*\S+\s+{0}\s" -f [regex]::Escape($过滤器名称))) } function 查找_支持过滤器的ffmpeg { param([Parameter(Mandatory)] [string]$过滤器名称) $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包路径, (获取_可执行命令路径 -名称 'ffmpeg')) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -Unique foreach ($路径 in $候选列表) { if (测试_ffmpeg过滤器可用 -FfmpegPath $路径 -过滤器名称 $过滤器名称) { return $路径 } } return $null } function 确保_ffmpeg支持过滤器 { param([Parameter(Mandatory)] [string]$过滤器名称) $ffmpeg路径 = 查找_支持过滤器的ffmpeg -过滤器名称 $过滤器名称 if ($ffmpeg路径) { return $ffmpeg路径 } Write-Host "未找到支持 $过滤器名称 的 ffmpeg,将尝试安装 Gyan.FFmpeg。" -ForegroundColor Yellow if (测试_管理员权限) { if (-not (安装_ffmpeg_通过Winget)) { [void](安装_ffmpeg_通过下载) } } else { 以管理员执行_安装ffmpeg } 刷新_会话PATH $ffmpeg路径 = 查找_支持过滤器的ffmpeg -过滤器名称 $过滤器名称 if (-not $ffmpeg路径) { throw "安装后仍未找到支持 $过滤器名称 的 ffmpeg,请重新打开终端后再试。" } return $ffmpeg路径 } function 测试_ffmpeg视频编码器可用 { param( [Parameter(Mandatory)] [string]$FfmpegPath, [Parameter(Mandatory)] [string]$编码器名称 ) try { $结果 = 调用_外部命令 -Exe $FfmpegPath -ArgumentList @('-hide_banner', '-encoders') -CaptureOutput } catch { return $false } return ($结果.StdOut -match ("(?m)^\s*[A-Z\.]{6}\s+{0}\s" -f [regex]::Escape($编码器名称))) } function 获取_媒体流探测数据 { param([Parameter(Mandatory)] [string]$Path) $结果 = 调用_外部命令 -Exe 'ffprobe' -ArgumentList @( '-v', 'quiet', '-print_format', 'json', '-show_streams', $Path ) -CaptureOutput return ($结果.StdOut | ConvertFrom-Json) } |