Skip to content

Instantly share code, notes, and snippets.

@joshooaj
Last active March 19, 2024 13:36
Show Gist options
  • Save joshooaj/201708f8077cf530bdd8c08dc4e3b88b to your computer and use it in GitHub Desktop.
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
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
}
}
}
}
@ChicagoJay
Copy link

ChicagoJay commented Sep 22, 2021

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