Files
Brain/brain-h5/scripts/extract-video-frames.ps1
MingNian b0fce840a3 首次提交:脑晴测 H5 项目
包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、
Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。

技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。
DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
2026-07-28 10:24:47 +08:00

123 lines
3.5 KiB
PowerShell

param(
[Parameter(Mandatory = $true)]
[string]$VideoPath,
[Parameter(Mandatory = $true)]
[string]$OutputDirectory,
[int]$FrameCount = 12,
[double]$StartSeconds = 0,
[double]$EndSeconds = -1
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
function Invoke-DispatcherWait {
param([int]$Milliseconds)
$frame = [System.Windows.Threading.DispatcherFrame]::new()
$timer = [System.Windows.Threading.DispatcherTimer]::new(
[System.Windows.Threading.DispatcherPriority]::Background
)
$timer.Interval = [TimeSpan]::FromMilliseconds($Milliseconds)
$timer.Add_Tick({
$timer.Stop()
$frame.Continue = $false
})
$timer.Start()
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
}
$resolvedVideo = (Resolve-Path -LiteralPath $VideoPath).Path
$resolvedOutput = [System.IO.Path]::GetFullPath($OutputDirectory)
[System.IO.Directory]::CreateDirectory($resolvedOutput) | Out-Null
$player = [System.Windows.Media.MediaPlayer]::new()
$player.ScrubbingEnabled = $true
$player.Open([Uri]::new($resolvedVideo))
$player.Play()
for ($attempt = 0; $attempt -lt 100 -and $player.NaturalVideoWidth -eq 0; $attempt++) {
Invoke-DispatcherWait -Milliseconds 100
}
if ($player.NaturalVideoWidth -eq 0 -or -not $player.NaturalDuration.HasTimeSpan) {
$player.Close()
throw "Unable to decode video: $resolvedVideo"
}
$width = $player.NaturalVideoWidth
$height = $player.NaturalVideoHeight
$duration = $player.NaturalDuration.TimeSpan.TotalSeconds
$rangeStart = [Math]::Min([Math]::Max($StartSeconds, 0), $duration)
$rangeEnd = if ($EndSeconds -lt 0) {
$duration
} else {
[Math]::Min([Math]::Max($EndSeconds, $rangeStart), $duration)
}
$metadata = [ordered]@{
path = $resolvedVideo
width = $width
height = $height
durationSeconds = [Math]::Round($duration, 3)
frames = @()
}
$player.Stop()
$player.Position = [TimeSpan]::Zero
$player.SpeedRatio = 16
$player.Play()
for ($index = 0; $index -lt $FrameCount; $index++) {
$ratio = if ($FrameCount -eq 1) { 0.5 } else { $index / ($FrameCount - 1) }
$seconds = $rangeStart + (($rangeEnd - $rangeStart) * $ratio)
$seconds = [Math]::Min([Math]::Max($seconds, 0), [Math]::Max($duration - 0.08, 0))
$deadline = [DateTime]::UtcNow.AddSeconds(45)
while ($player.Position.TotalSeconds -lt $seconds -and [DateTime]::UtcNow -lt $deadline) {
Invoke-DispatcherWait -Milliseconds 40
}
Invoke-DispatcherWait -Milliseconds 80
$visual = [System.Windows.Media.DrawingVisual]::new()
$context = $visual.RenderOpen()
$context.DrawVideo(
$player,
[Windows.Rect]::new(0, 0, $width, $height)
)
$context.Close()
$bitmap = [System.Windows.Media.Imaging.RenderTargetBitmap]::new(
$width,
$height,
96,
96,
[System.Windows.Media.PixelFormats]::Pbgra32
)
$bitmap.Render($visual)
$milliseconds = [long][Math]::Round($seconds * 1000)
$fileName = "frame-{0:D2}-{1:D6}ms.png" -f ([int]($index + 1)), $milliseconds
$filePath = Join-Path $resolvedOutput $fileName
$encoder = [System.Windows.Media.Imaging.PngBitmapEncoder]::new()
$encoder.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($bitmap))
$stream = [System.IO.File]::Open($filePath, [System.IO.FileMode]::Create)
try {
$encoder.Save($stream)
} finally {
$stream.Dispose()
}
$metadata.frames += [ordered]@{
file = $fileName
seconds = [Math]::Round($seconds, 3)
actualSeconds = [Math]::Round($player.Position.TotalSeconds, 3)
}
}
$player.Close()
$metadata | ConvertTo-Json -Depth 4