-
-
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!
I can't get the script to generate any output.
$env:path += ";C:\ffmpeg\bin"
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 180
OutputFps = 30
OutputPath = 'g:\test\Timelapse.mp4'
}
.\New-Timelapse @timelapseParams
I can't get the script to generate any output.
Late reply here I know, but the issue might be that you had this New-Timelapse function defined in New-Timelapse.ps1, and then tried to call the ps1 file directly using .\New-Timelapse.ps1 @timelapseParams.
The way this gist was written, a function named New-Timelapse is defined but not called or executed. It doesn't do anything but describe a function with a name, and what should happen when someone uses it.
What you would do is "dot-source" the file which would define that function in your current session or script, then you can call it. For example...
$env:path += ";C:\ffmpeg\bin"
$timelapseParams = @{
Camera = Select-Camera -SingleSelect
Start = (Get-Date).AddDays(-7)
End = Get-Date
OutputLength = New-TimeSpan -Seconds 180
OutputFps = 30
OutputPath = 'g:\test\Timelapse.mp4'
}
# Dot-source the file - this is effectively like copying and pasting the contents into this part of your script.
. .\New-Timelapse.ps1
# Call the function defined by dot-sourcing in the previous line.
New-Timelapse @timelapseParamsTo make it possible for this script to be called like you did originally, you could also strip the first and last lines from the file. Without the function New-Timelapse { } wrapper, PowerShell will treat the file itself like it's a PowerShell function and you can call it like you did before.
BRILLIANT! Worked great!
Some gotchas:
$PSScriptRootand-Forcein 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 30but 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!!!