private/LLC过程.ps1
|
function 读取_LLC项目 { param([Parameter(Mandatory)] [string]$Path) $项目路径 = (Resolve-Path -LiteralPath $Path).Path $文本 = [System.IO.File]::ReadAllText($项目路径, [System.Text.Encoding]::UTF8) # 仅删除字符串外的尾随逗号,保留 LLC 的字段顺序和字符串内容。 $结果文本 = New-Object System.Text.StringBuilder $在字符串内 = $false $已转义 = $false for ($i = 0; $i -lt $文本.Length; $i++) { $字符 = $文本[$i] if ($在字符串内) { [void]$结果文本.Append($字符) if ($已转义) { $已转义 = $false } elseif ($字符 -eq '\') { $已转义 = $true } elseif ($字符 -eq '"') { $在字符串内 = $false } } elseif ($字符 -eq '"') { $在字符串内 = $true [void]$结果文本.Append($字符) } elseif ($字符 -eq ',' -and $文本.Substring($i + 1) -match '^\s*[}\]]') { continue } else { [void]$结果文本.Append($字符) } } $项目 = $结果文本.ToString() | ConvertFrom-Json if ([int]$项目.version -ne 2) { throw "仅支持 version 2 的 LLC 项目:$项目路径" } $媒体文件名 = [string]$项目.mediaFileName if ([string]::IsNullOrWhiteSpace($媒体文件名)) { throw 'LLC 项目缺少 mediaFileName。' } $媒体路径 = if ([System.IO.Path]::IsPathRooted($媒体文件名)) { $媒体文件名 } else { Join-Path (Split-Path -Parent $项目路径) $媒体文件名 } if (-not (Test-Path -LiteralPath $媒体路径 -PathType Leaf)) { throw "LLC 项目引用的媒体文件不存在:$媒体路径" } $片段列表 = New-Object 'System.Collections.Generic.List[object]' $序号 = 0 foreach ($片段 in $项目.cutSegments) { $序号++ if ($null -ne $片段.PSObject.Properties['selected'] -and -not [bool]$片段.selected) { continue } $开始 = [double]$片段.start $结束 = [double]$片段.end if ([double]::IsNaN($开始) -or [double]::IsInfinity($开始) -or [double]::IsNaN($结束) -or [double]::IsInfinity($结束) -or $开始 -lt 0 -or $结束 -le $开始) { throw "第 $序号 个片段的起止时间无效。" } $片段列表.Add([pscustomobject]@{ 序号 = $序号; 开始 = $开始; 结束 = $结束; 名称 = [string]$片段.name }) } if ($片段列表.Count -eq 0) { throw 'LLC 项目中没有 selected 为 true 的有效剪切片段。' } return [pscustomobject]@{ 项目路径 = $项目路径; 媒体路径 = (Resolve-Path -LiteralPath $媒体路径).Path; 片段列表 = $片段列表.ToArray() } } function 查找_RealMedia编辑器路径 { $命令 = Get-Command 'rmeditor.exe' -ErrorAction SilentlyContinue if ($命令) { return $命令.Source } $候选路径 = @( (Join-Path ${env:ProgramFiles(x86)} 'Real\RealProducer Basic\rmeditor.exe'), (Join-Path $env:ProgramFiles 'Real\RealProducer Basic\rmeditor.exe'), (Join-Path $env:TEMP 'losslesscut-realmedia-tools\producer-files\Default_File_Group\rmeditor.exe') ) foreach ($路径 in $候选路径) { if (Test-Path -LiteralPath $路径 -PathType Leaf) { return $路径 } } return $null } function 解决_RealMedia编辑器路径 { $编辑器路径 = 查找_RealMedia编辑器路径 if ($编辑器路径) { return $编辑器路径 } Write-Host '未找到 RealMedia Editor,正在自动安装 RealProducer Basic 11……' -ForegroundColor Yellow 调用_提权安装脚本 -文件名 '安装RealMedia工具.ps1' -提示 '需要管理员权限以安装 RealMedia Editor,将弹出 UAC 提示……' -失败消息 '提权安装 RealMedia Editor 失败' 刷新_会话PATH $编辑器路径 = 查找_RealMedia编辑器路径 if (-not $编辑器路径) { throw 'RealProducer 安装完成,但仍未找到 rmeditor.exe。' } return $编辑器路径 } function 格式化_RealMedia时间 { param([Parameter(Mandatory)] [double]$秒) $总毫秒 = [int64][Math]::Round($秒 * 1000, [MidpointRounding]::AwayFromZero) $时间 = [TimeSpan]::FromMilliseconds($总毫秒) return '{0}:{1:00}:{2:00}:{3:00}.{4:000}' -f [Math]::Floor($时间.TotalDays), $时间.Hours, $时间.Minutes, $时间.Seconds, $时间.Milliseconds } function 剪切_RealMedia片段 { param( [Parameter(Mandatory)] [string]$编辑器路径, [Parameter(Mandatory)] [string]$源路径, [Parameter(Mandatory)] [string]$输出路径, [Parameter(Mandatory)] [double]$开始, [Parameter(Mandatory)] [double]$结束 ) $工作目录 = Join-Path ([System.IO.Path]::GetTempPath()) ('llc-rm-' + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $工作目录 -Force | Out-Null try { $临时源路径 = Join-Path $工作目录 ('source' + [System.IO.Path]::GetExtension($源路径)) $临时输出路径 = Join-Path $工作目录 ('segment' + [System.IO.Path]::GetExtension($源路径)) Copy-Item -LiteralPath $源路径 -Destination $临时源路径 $参数 = @( '-i', $临时源路径, '-o', $临时输出路径, '-s', (格式化_RealMedia时间 -秒 $开始), '-e', (格式化_RealMedia时间 -秒 $结束) ) Write-Host "> $编辑器路径 $($参数 -join ' ')" -ForegroundColor DarkGray [void](调用_外部命令 -Exe $编辑器路径 -ArgumentList $参数 -CaptureOutput) if (-not (Test-Path -LiteralPath $临时输出路径 -PathType Leaf) -or (Get-Item -LiteralPath $临时输出路径).Length -le 0) { throw "rmeditor 未生成有效片段:$输出路径" } Move-Item -LiteralPath $临时输出路径 -Destination $输出路径 -Force } finally { Remove-Item -LiteralPath $工作目录 -Recurse -Force -ErrorAction SilentlyContinue } } function 合并_RealMedia片段 { param( [Parameter(Mandatory)] [string[]]$输入文件列表, [Parameter(Mandatory)] [string]$输出文件 ) $编辑器路径 = 解决_RealMedia编辑器路径 $输出目录 = Split-Path -Parent ([System.IO.Path]::GetFullPath($输出文件)) if ($输出目录 -and -not (Test-Path -LiteralPath $输出目录 -PathType Container)) { New-Item -ItemType Directory -Path $输出目录 -Force | Out-Null } $工作目录 = Join-Path ([System.IO.Path]::GetTempPath()) ('merge-rm-' + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $工作目录 -Force | Out-Null try { $临时输出 = Join-Path $工作目录 'merged.rmvb' $参数 = New-Object 'System.Collections.Generic.List[string]' foreach ($输入文件 in $输入文件列表) { $临时输入 = Join-Path $工作目录 ('input-{0:D3}.rmvb' -f $参数.Count) Copy-Item -LiteralPath $输入文件 -Destination $临时输入 $参数.Add('-i') $参数.Add($临时输入) } $参数.Add('-o') $参数.Add($临时输出) Write-Host "> $编辑器路径 $($参数 -join ' ')" -ForegroundColor DarkGray [void](调用_外部命令 -Exe $编辑器路径 -ArgumentList $参数.ToArray() -CaptureOutput) if (-not (Test-Path -LiteralPath $临时输出 -PathType Leaf) -or (Get-Item -LiteralPath $临时输出).Length -le 0) { throw 'rmeditor 未生成有效合并文件。' } Move-Item -LiteralPath $临时输出 -Destination $输出文件 -Force } finally { Remove-Item -LiteralPath $工作目录 -Recurse -Force -ErrorAction SilentlyContinue } } function 获取_视频关键帧 { param([Parameter(Mandatory)] [string]$Path) $结果 = 调用_外部命令 -Exe 'ffprobe' -ArgumentList @( '-v', 'error', '-select_streams', 'v:0', '-show_entries', 'packet=pts_time,flags', '-of', 'csv=p=0', $Path ) -CaptureOutput $视频包列表 = New-Object 'System.Collections.Generic.List[object]' foreach ($行 in ($结果.StdOut -split "`r?`n")) { if ([string]$行 -notmatch '^\s*(-?\d+(?:\.\d+)?),([A-Z_]+)') { continue } $视频包列表.Add([pscustomobject]@{ 时间 = [double]::Parse($Matches[1], [Globalization.CultureInfo]::InvariantCulture) 是关键帧 = $Matches[2].StartsWith('K') }) } $关键帧列表 = New-Object 'System.Collections.Generic.List[object]' $前一显示时间 = $null foreach ($包 in ($视频包列表.ToArray() | Sort-Object -Property 时间)) { if ($包.是关键帧) { # rmeditor 按帧序号选择起点,提前半个显示帧以避免漏掉目标首关键帧。 $编辑器起点 = if ($null -eq $前一显示时间) { $包.时间 } else { ($前一显示时间 + $包.时间) / 2 } $关键帧列表.Add([pscustomobject]@{ 时间 = $包.时间; 编辑器起点 = $编辑器起点 }) } if ($null -eq $前一显示时间 -or $包.时间 -gt $前一显示时间) { $前一显示时间 = $包.时间 } } if ($关键帧列表.Count -eq 0) { throw "未能读取视频关键帧时间:$Path" } return $关键帧列表.ToArray() } function 取_最近关键帧 { param( [Parameter(Mandatory)] [object[]]$关键帧列表, [Parameter(Mandatory)] [double]$目标时间 ) $下界 = 0 $上界 = $关键帧列表.Count - 1 while ($下界 -le $上界) { $中点 = [int](($下界 + $上界) / 2) if ($关键帧列表[$中点].时间 -eq $目标时间) { return $关键帧列表[$中点] } if ($关键帧列表[$中点].时间 -lt $目标时间) { $下界 = $中点 + 1 } else { $上界 = $中点 - 1 } } if ($下界 -eq 0) { return $关键帧列表[0] } if ($下界 -eq $关键帧列表.Count) { return $关键帧列表[$关键帧列表.Count - 1] } $前一帧 = $关键帧列表[$下界 - 1] $后一帧 = $关键帧列表[$下界] if ($目标时间 - $前一帧.时间 -le $后一帧.时间 - $目标时间) { return $前一帧 } return $后一帧 } function 格式化_LLC时间戳 { param([Parameter(Mandatory)] [double]$秒) return [TimeSpan]::FromSeconds($秒).ToString('hh\.mm\.ss\.fff', [Globalization.CultureInfo]::InvariantCulture) } |