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