-
-
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 | |
} | |
} | |
} | |
} |
@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!
BRILLIANT! Worked great!
Some gotchas:
$PSScriptRoot
and-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.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!!!