-
-
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 | |
} | |
} | |
} | |
} |
Hi @ChicagoJay! So to use this function you would put it in your script, and then call it with the necessary parameters. Here's an example. Let's say I have the script above in a file called New-Timelapse.ps1
, and I create the following script in a new file called CreateTimelapse.ps1
in the same folder.
# 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 -Server localhost -AcceptEula
$timelapseParams = @{
Camera = Select-Camera -SingleSelect
Start = (Get-Date).AddDays(-7)
End = Get-Date
OutputLength = New-TimeSpan -Seconds 90
OutputFps = 30
OutputPath = 'C:\Temp\Timelapse.mp4'
}
New-Timelapse @timelapseParams
In this example, we're assuming it's running on the Management Server so if it's not, change Server to the IP/hostname of the Management Server and maybe put in a credential if "Current User" doesn't work. To have PowerShell ask you for a credential you can use Connect-ManagementServer -Server myserver -Credential (Get-Credential)
. There are also better ways of managing credentials for automation like using Microsoft's SecretManagement module or even Export/Import-Clixml to save/load an encrypted credential object on disk.
Assuming you have ffmpeg in your Path environment variable, this should generate up to a 90 second timelapse from the last 7 days of recordings once you select a camera from the dialog that is going to pop-up (select-camera will pop up an item picker dialog).
You can test to see if ffmpeg is in your path environment variable by typing ffmpeg -version
in a console window (and not while you're in the folder ffmpeg is in). It should dump some information to the console. If not, make sure you have downloaded ffmpeg first of all, and then wherever you place the EXE, you can manually add it to your path environment variable or you could add it temporarily by putting something like this at the top of your script:
$env:path += ";C:\Path\To\Folder\Where\ffmpeg.exe\is"
It's been a while since I made a timelapse with this so I gave it a shot on one of our public demo cameras. So far so good!
BRILLIANT! Worked great!
Some gotchas:
- I needed to add a line to define
$PSScriptRoot
and - You may want to add some error-checking, or use
-Force
in the invocation, as the script will throw an error (but continue)Connect-ManagementServer : Already connected to a Management Server. Include the -Force switch parameter to automatically disconnect from previous sessions.
after the first time the script is run. - There doesn't appear to be a way to control the length of the output video. There is a line, that says
OutputLength = New-TimeSpan - Seconds 30
but changing that to other values seems to have no effect.
Point is, the script did almost exactly what I needed it to do, and it did so quickly (one month of source into 30 seconds of video took less than 5 minutes) and easily! So thanks very much, @jhendricks123!!!
@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
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!
@jhendriks123 - Do you have some demo command lines, so we can see how to use this? Also, how do we point it to ffmpeg's location, since it's not usually in the $PATH?
Thanks!