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 21, 2021

@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:

> .\TimeLapseTest.ps1
ffmpeg version n4.4-78-g031c0cb0b4-20210630 Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 10-win32 (GCC) 20210408
configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-libvmaf --enable-vulkan --enable-amf --enable-libaom --enable-avisynth --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-libglslang --enable-libgme --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libmfx --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --enable-libvidstab --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --extra-cflags=-DLIBTWOLAME_STATIC --extra-cxxflags= --extra-ldflags=-pthread --extra-ldexeflags= --extra-libs=-lgomp --extra-version=20210630
libavutil      56. 70.100 / 56. 70.100
libavcodec     58.134.100 / 58.134.100
libavformat    58. 76.100 / 58. 76.100
libavdevice    58. 13.100 / 58. 13.100
libavfilter     7.110.100 /  7.110.100
libswscale      5.  9.100 /  5.  9.100
libswresample   3.  9.100 /  3.  9.100
libpostproc    55.  9.100 / 55.  9.100
ffmpeg version n4.4-78-g031c0cb0b4-20210630 Copyright (c) 2000-2021 the FFmpeg developers
  built with gcc 10-win32 (GCC) 20210408
  configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-libvmaf --enable-vulkan --enable-amf --enable-libaom --enable-avisynth --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-libglslang --enable-libgme --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libmfx --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --enable-libvidstab --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --extra-cflags=-DLIBTWOLAME_STATIC --extra-cxxflags= --extra-ldflags=-pthread --extra-ldexeflags= --extra-libs=-lgomp --extra-version=20210630
  libavutil      56. 70.100 / 56. 70.100
  libavcodec     58.134.100 / 58.134.100
  libavformat    58. 76.100 / 58. 76.100
  libavdevice    58. 13.100 / 58. 13.100
  libavfilter     7.110.100 /  7.110.100
  libswscale      5.  9.100 /  5.  9.100
  libswresample   3.  9.100 /  3.  9.100
  libpostproc    55.  9.100 / 55.  9.100
Input #0, image2, from 'C:\New-Timelapse-x3mtwbrt.r43\image_%10d.jpg':
  Duration: 00:00:19.20, start: 0.000000, bitrate: N/A
  Stream #0:0: Video: mjpeg (Baseline), yuvj420p(pc, bt470bg/unknown/unknown), 1920x1080 [SAR 30000:30000 DAR 16:9], 60 fps, 60 tbr, 60 tbn, 60 tbc
Stream mapping:
  Stream #0:0 -> #0:0 (mjpeg (native) -> h264 (libx264))
Press [q] to stop, [?] for help
[swscaler @ 000001c132256f40] deprecated pixel format used, make sure you did set range correctly
[libx264 @ 000001c13277f0c0] using SAR=1/1
[libx264 @ 000001c13277f0c0] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 000001c13277f0c0] profile High, level 4.0, 4:2:0, 8-bit
[libx264 @ 000001c13277f0c0] 264 - core 163 - H.264/MPEG-4 AVC codec - Copyleft 2003-2021 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=24 lookahead_threads=4 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00
Output #0, mp4, to 'C:\Temp\Timelapse.mp4':
  Metadata:
    encoder         : Lavf58.76.100
  Stream #0:0: Video: h264 (avc1 / 0x31637661), yuv420p(tv, bt470bg/unknown/unknown, progressive), 1920x1080 [SAR 1:1 DAR 16:9], q=2-31, 30 fps, 15360 tbn
    Metadata:
      encoder         : Lavc58.134.100 libx264
    Side data:
      cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
frame= 1152 fps= 51 q=-1.0 Lsize=   43038kB time=00:00:38.30 bitrate=9205.4kbits/s speed= 1.7x
video:43023kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.034731%
[libx264 @ 000001c13277f0c0] frame I:13    Avg QP:23.73  size:174571
[libx264 @ 000001c13277f0c0] frame P:296   Avg QP:25.99  size: 86388
[libx264 @ 000001c13277f0c0] frame B:843   Avg QP:29.39  size: 19234
[libx264 @ 000001c13277f0c0] consecutive B-frames:  2.0%  1.0%  0.8% 96.2%
[libx264 @ 000001c13277f0c0] mb I  I16..4:  1.9% 75.3% 22.8%
[libx264 @ 000001c13277f0c0] mb P  I16..4:  0.6% 11.4%  3.3%  P16..4: 42.0% 17.7% 15.6%  0.0%  0.0%    skip: 9.4%
[libx264 @ 000001c13277f0c0] mb B  I16..4:  0.1%  1.7%  0.7%  B16..8: 34.6%  4.9%  1.1%  direct: 4.3%  skip:52.4%  L0:48.6% L1:41.5% BI: 9.9%
[libx264 @ 000001c13277f0c0] 8x8 transform intra:72.4% inter:76.8%
[libx264 @ 000001c13277f0c0] coded y,uvDC,uvAC intra: 86.9% 50.4% 10.9% inter: 27.4% 14.2% 0.2%
[libx264 @ 000001c13277f0c0] i16 v,h,dc,p: 14% 50% 11% 24%
[libx264 @ 000001c13277f0c0] i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 12% 38% 13%  5%  4%  4%  7%  5% 12%
[libx264 @ 000001c13277f0c0] i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 13% 40%  9%  4%  7%  6%  9%  5%  8%
[libx264 @ 000001c13277f0c0] i8c dc,h,v,p: 61% 22% 14%  2%
[libx264 @ 000001c13277f0c0] Weighted P-Frames: Y:42.6% UV:19.6%
[libx264 @ 000001c13277f0c0] ref P L0: 38.9% 16.0% 22.1% 16.9%  6.0%
[libx264 @ 000001c13277f0c0] ref B L0: 60.2% 30.8%  9.0%
[libx264 @ 000001c13277f0c0] ref B L1: 84.8% 15.2%
[libx264 @ 000001c13277f0c0] kb/s:9178.09
Get-Item : Cannot find path 'C:\New-Timelapse-x3mtwbrt.r43' because it does not exist.
At C:\Program Files\WindowsPowerShell\Scripts\New-Timelapse.ps1:70 char:13
+             Get-Item $tempFolder | Remove-Item -Force
+             ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\New-Timelapse-x3mtwbrt.r43:String) [Get-Item], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand

@joshooaj
Copy link
Author

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

@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