Last active
March 19, 2024 13:36
-
-
Save joshooaj/201708f8077cf530bdd8c08dc4e3b88b to your computer and use it in GitHub Desktop.
Create a timelapse video using MilestonePSTools to retrieve jpegs from a Milestone VMS and ffmpeg to compile those snapshots into an h264 timelapse video
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} | |
} |
It's hard to say exactly what's happening without knowing more about the available frame rate for the time period being exported and the verbose output with the calculated $sampleInterval. There could be a miscalculation there, or the call to ffmpeg needs to be updated to work properly?
You might check out the timelapse plugin from Visual Networks @ http://www.visualnetworks.co.nz/SoftwarePage.html to see if that is a bit more reliable
I'm checking out the plugin, but it's not free.
How can I get you the requested info?
Here is the script I use to call the program:
$PSScriptRoot="C:\Program Files\WindowsPowerShell\Scripts"
# This line will "dot-source" the New-Timelapse.ps1 file and basically load that function up in memory making it ready to use in your current PowerShell script/session.
. $PSScriptRoot\New-Timelapse.ps1
# Change this if you're running it on a different machine than the Management Server
Connect-ManagementServer -Force -Server milestone.fqdn -AcceptEula
$timelapseParams = @{
Camera = Select-Camera -SingleSelect
Start = Get-Date -Date 6/1/21 -Hour 6 -Minute 17 -Second 37
End = Get-Date -Date 6/30/21 -Hour 13 -Minute 25 -Second 53
OutputLength = New-TimeSpan -Seconds 180
OutputFps = 30
OutputPath = 'C:\Temp\Timelapse.mp4'
}
New-Timelapse @timelapseParams
Thanks again!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@jhendricks123 - the script appears to be ignoring the OutputLength, as I put
OutputLength = New-TimeSpan -Minutes 3
(I also tried-seconds 180
) and still got a 38 second video.The script throws an error at the end, but I don't think it has anything to do with this. It says the temp file doesn't exist. I think it's getting cleaned up twice by the script.
Anyway - how can we get the
OutputLength
parameter to be obeyed?Thanks!
Here is the transcript: