-
-
Save joshooaj/201708f8077cf530bdd8c08dc4e3b88b to your computer and use it in GitHub Desktop.
| function New-Timelapse { | |
| <# | |
| .SYNOPSIS | |
| Exports still images from XProtect and creates a timelapse video using ffmpeg. | |
| .DESCRIPTION | |
| This example function saves jpeg images from the recordings of the specified | |
| camera to a temp folder, and uses these images as input to the ffmpeg | |
| command-line utility to generate a timelapse video from the images. | |
| .PARAMETER Camera | |
| Specifies a camera object such as is returned by Get-VmsCamera or Select-Camera. | |
| .PARAMETER Start | |
| Specifies the timestamp from which snapshots will be exported from the | |
| specified camera. Example: (Get-Date).AddDays(-7) | |
| .PARAMETER End | |
| Specifies the timestamp at which the snapshot export should stop. Example: (Get-Date -Year 2022 -Month 5 -Day 5) | |
| .PARAMETER OutputLength | |
| Specifies the desired length of the resulting timelapse video. If the | |
| timespan defined by the Start and End parameters should be compressed into a | |
| 30-second video, then you can specify (New-TimeSpan -Seconds 30). | |
| .PARAMETER OutputFps | |
| Specifies the framerate for the resulting video which can either be 30, or 60 FPS. | |
| .PARAMETER OutputPath | |
| Specifies the path, including file name, for the video file. For example: C:\temp\timelapse.mp4 | |
| .EXAMPLE | |
| $params = @{ | |
| Camera = Select-Camera -SingleSelect | |
| Start = (Get-Date).AddDays(-7) | |
| End = Get-Date | |
| OutputLength = New-TimeSpan -Seconds 30 | |
| OutputFps = 30 | |
| OutputPath = 'C:\temp\timelapse.mp4' | |
| } | |
| New-Timelapse @params | |
| The parameters for the timelapse are defined in a hashtable, and then "splatted" into the New-Timelapse cmdlet. The | |
| resulting timelapse will be up to 30 seconds long, and play at 30fps. Though the resulting video can be shorter if | |
| the recordings are not continuous. | |
| .EXAMPLE | |
| $params = @{ | |
| Camera = Get-VmsCamera -Id F09B8B40-3B23-4A7F-9A56-AE13F94BA18F | |
| Start = (Get-Date).AddDays(-1) | |
| End = Get-Date | |
| OutputLength = New-TimeSpan -Seconds 30 | |
| OutputFps = 60 | |
| OutputPath = 'C:\temp\timelapse.mp4' | |
| } | |
| New-Timelapse @params | |
| Creates a 30 second long 60fps timelapse video of the last 24 hours for the camera with ID | |
| 'F09B8B40-3B23-4A7F-9A56-AE13F94BA18F'. | |
| .NOTES | |
| This sample is offered as-is and is not intended to be supported by @joshooaj | |
| or Milestone Systems, though I am happy to repond to questions/issues as and | |
| when I have the time. | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
| [VideoOS.Platform.ConfigurationItems.Camera] | |
| $Camera, | |
| [Parameter(Mandatory=$true)] | |
| [DateTime] | |
| $Start, | |
| [Parameter(Mandatory=$true)] | |
| [DateTime] | |
| $End, | |
| [Parameter()] | |
| [TimeSpan] | |
| $OutputLength, | |
| [Parameter()] | |
| [ValidateSet(30, 60)] | |
| [int] | |
| $OutputFps = 30, | |
| [Parameter(Mandatory=$true)] | |
| [string] | |
| $OutputPath | |
| ) | |
| begin { | |
| try { | |
| $null = Get-VmsManagementServer -ErrorAction Stop | |
| if ($null -eq (Get-Command ffmpeg -ErrorAction Ignore)) { | |
| throw ([io.filenotfoundexception]::new('Please download ffmpeg and ensure the folder location is added to your PATH environment variable.', 'ffmpeg.exe')) | |
| } | |
| $result = & ffmpeg.exe -version -hide_banner -loglevel error 2>&1 | |
| if (!$?) { | |
| $errorrecord = $result | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } | Select-Object -First 1 | |
| throw $errorrecord | |
| } | |
| } catch { | |
| throw | |
| } | |
| } | |
| process { | |
| if (Test-Path $OutputPath) { | |
| throw "File already exists: $($OutputPath)" | |
| } | |
| $outputFrameCount = $OutputLength.TotalSeconds * $OutputFps | |
| $sourceTimespan = $End - $Start | |
| $sampleInterval = [Math]::Floor($sourceTimespan.TotalSeconds / $outputFrameCount) | |
| $tempFolder = Join-Path ([io.path]::GetTempPath()) ([IO.Path]::GetRandomFileName()) | |
| Write-Verbose "Total Output Frames: $outputFrameCount" | |
| Write-Verbose "Original Duration: $($sourceTimespan.TotalMinutes)" | |
| Write-Verbose "Sample Interval: $sampleInterval seconds between images" | |
| Write-Verbose "Temp Folder: $tempFolder" | |
| try { | |
| $null = New-Item -Path $tempFolder -ItemType Directory -ErrorAction Stop | |
| $null = $Camera | Get-Snapshot -Timestamp $Start -EndTime $End -Interval $sampleInterval -Save -Path $tempFolder -Quality 100 -ErrorAction Stop | |
| if (-not (Get-ChildItem (Join-Path $tempFolder '*.jpg'))) { | |
| throw "Get-Snapshot failed to save any images for $($Camera.Name) between $Start and $End. Are there any recordings available during this time?" | |
| } | |
| $i = 0 | |
| foreach ($item in Get-ChildItem -Path $tempFolder | Sort-Object Name) { | |
| $item | Move-Item -Destination (Join-Path $tempFolder "image_$($i.ToString().PadLeft(10, '0')).jpg") -ErrorAction Stop | |
| $i += 1 | |
| } | |
| $inputPattern = Join-Path $tempFolder 'image_%10d.jpg' | |
| $ffmpegArgs = @( | |
| "-framerate", 60, # No idea why this is hardcoded to 60 but I think it ended up helping with the unpredicable source "frame rate" | |
| "-r", $OutputFps, # Sets the desired framerate of the resulting video | |
| "-i", """$inputPattern""", # Specifies the source folder and filename pattern for the exported jpegs | |
| "-c:v", "libx264", # Set the codec to x264 | |
| "-pix_fmt", "yuv420p", # I think this was needed when using the mjpeg transcoding option | |
| "-vf", """crop=trunc(iw/2)*2:trunc(ih/2)*2""", # Intended to ensure the output resolution is divisible by 2 | |
| $OutputPath | |
| ) | |
| & ffmpeg.exe @ffmpegArgs | |
| } | |
| catch { | |
| throw | |
| } | |
| finally { | |
| if ((Test-Path -Path $tempFolder)) { | |
| Remove-Item -Path $tempFolder -Recurse -Force | |
| } | |
| } | |
| } | |
| } |
I can't get the script to generate any output.
Late reply here I know, but the issue might be that you had this New-Timelapse function defined in New-Timelapse.ps1, and then tried to call the ps1 file directly using .\New-Timelapse.ps1 @timelapseParams.
The way this gist was written, a function named New-Timelapse is defined but not called or executed. It doesn't do anything but describe a function with a name, and what should happen when someone uses it.
What you would do is "dot-source" the file which would define that function in your current session or script, then you can call it. For example...
$env:path += ";C:\ffmpeg\bin"
$timelapseParams = @{
Camera = Select-Camera -SingleSelect
Start = (Get-Date).AddDays(-7)
End = Get-Date
OutputLength = New-TimeSpan -Seconds 180
OutputFps = 30
OutputPath = 'g:\test\Timelapse.mp4'
}
# Dot-source the file - this is effectively like copying and pasting the contents into this part of your script.
. .\New-Timelapse.ps1
# Call the function defined by dot-sourcing in the previous line.
New-Timelapse @timelapseParamsTo make it possible for this script to be called like you did originally, you could also strip the first and last lines from the file. Without the function New-Timelapse { } wrapper, PowerShell will treat the file itself like it's a PowerShell function and you can call it like you did before.
I can't get the script to generate any output.
$env:path += ";C:\ffmpeg\bin"
Change this if you're running it on a different machine than the Management Server
Connect-ManagementServer -Server localhost -AcceptEula
$timelapseParams = @{
Camera = Select-Camera -SingleSelect
Start = (Get-Date).AddDays(-7)
End = Get-Date
OutputLength = New-TimeSpan -Seconds 180
OutputFps = 30
OutputPath = 'g:\test\Timelapse.mp4'
}
.\New-Timelapse @timelapseParams