Last active
March 13, 2026 23:18
-
-
Save joshooaj/5231f115dd46316394fecd3c96bb36c5 to your computer and use it in GitHub Desktop.
Save raw video data from an XProtect VMS to disk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #requires -Modules MilestonePSTools | |
| <# | |
| .SYNOPSIS | |
| Exports raw video data from a camera on a Milestone XProtect VMS to a file. | |
| .DESCRIPTION | |
| Retrieves recorded video frames from a Milestone XProtect recording server | |
| using the RawVideoSource API and writes the raw compressed frame data to a | |
| file. By default, the 32-byte VMS GenericByteData headers are stripped, | |
| producing a raw Annex B H.264/H.265 bitstream. Use -IncludeVmsHeaders to | |
| keep the proprietary XProtect headers intact for debugging. | |
| The export starts from the GOP containing StartTime and continues fetching | |
| sequential GOPs until EndTime is reached or the media database ends. | |
| .PARAMETER Camera | |
| A Camera object typically obtained using Get-VmsCamera or Select-Camera. | |
| .PARAMETER StartTime | |
| The start of the export range. Can be local or UTC; will be converted to UTC internally. | |
| .PARAMETER EndTime | |
| The end of the export range. Can be local or UTC; will be converted to UTC internally. | |
| .PARAMETER Path | |
| The output file path for the exported raw video data. There is no requirement for file | |
| extension but conventionally a ".h264" or ".h265" extension is a good idea when not | |
| including the VMS headers. When including VMS headers, consider using ".gbd" for "Generic Byte Data". | |
| .PARAMETER IncludeVmsHeaders | |
| When specified, the 32-byte GenericByteData headers are included in the | |
| output. Without this switch, headers are stripped so only the raw codec | |
| bitstream is written. If passing the data to FFmpeg, do not include the | |
| VMS headers. | |
| .EXAMPLE | |
| $cam = Get-VmsCamera -Name 'Front Door' | |
| .\Export-RawVideo.ps1 -Camera $cam -StartTime '2026-03-13 10:00' -EndTime '2026-03-13 10:01' -Path .\frontdoor.h264 | |
| Exports one minute of raw H.264 video from the "Front Door" camera. | |
| .EXAMPLE | |
| Get-VmsCamera -Name 'Lobby' | .\Export-RawVideo.ps1 -StartTime (Get-Date).AddMinutes(-5) -EndTime (Get-Date) -Path .\lobby.h264 -Verbose | |
| Exports the last 5 minutes of video from the "Lobby" camera with verbose GOP logging. | |
| .EXAMPLE | |
| $cam = Get-VmsCamera -Name 'Parking' | |
| .\Export-RawVideo.ps1 -Camera $cam -StartTime '2026-03-13 09:00' -EndTime '2026-03-13 09:00:30' -Path .\parking.gbd -IncludeVmsHeaders | |
| Exports 30 seconds of raw video with VMS GenericByteData headers preserved. | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory, ValueFromPipeline)] | |
| [VideoOS.Platform.ConfigurationItems.Camera] | |
| $Camera, | |
| [Parameter(Mandatory)] | |
| [datetime] | |
| $StartTime, | |
| [Parameter(Mandatory)] | |
| [datetime] | |
| $EndTime, | |
| [Parameter(Mandatory)] | |
| [string] | |
| $Path, | |
| [Parameter()] | |
| [switch] | |
| $IncludeVmsHeaders | |
| ) | |
| process { | |
| $ErrorActionPreference = 'Stop' | |
| $timeFormat = 'HH:mm:ss.fff' | |
| $StartTime = $StartTime.ToUniversalTime() | |
| $EndTime = $EndTime.ToUniversalTime() | |
| $item = $Camera | Get-VmsVideoOSItem -Kind Camera | |
| $src = $src = [videoos.Platform.Data.rawvideosource]::new($item) | |
| $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) | |
| $file = [io.file]::OpenWrite($Path) | |
| $offset = 32 | |
| if ($IncludeVmsHeaders) { | |
| $offset = 0; | |
| } | |
| try { | |
| $src.Init() | |
| # Try to get the first GOP containing StartTime | |
| $data = $src.GetAtOrBefore($StartTime) | |
| if ($null -eq $data -or $data.List.Count -eq 0) { | |
| # No video available containing StartTime | |
| # See if next available frame is available and between StartTime and EndTime | |
| $data = $src.GetNext($StartTime) | |
| if ($null -eq $data -or $data.List.Count -eq 0) { | |
| Write-Error "No recordings available" | |
| return | |
| } elseif ($data.List[0].DateTime -ge $EndTime) { | |
| Write-Error "No recordings available in the specified range" | |
| return | |
| } | |
| } | |
| Write-Verbose "StartTime: $($StartTime.ToLocalTime().ToString('o')), EndTime: $($EndTime.ToLocalTime().ToString('o'))" | |
| $gopCount = 0 | |
| do { | |
| $gopCount++ | |
| if ($gopCount -eq 1) { | |
| Write-Verbose "Initial GOP starts $(($StartTime - $data.List[0].DateTime).TotalMilliseconds) ms before the requested start time." | |
| } | |
| Write-Verbose ("GOP $($gopCount.ToString('d3'))" + | |
| ", DateTime: $($data.List[0].DateTime.ToLocalTime().ToString($timeFormat))" + | |
| ", Duration: $($data.List[-1].DateTime - $data.List[0].DateTime)" + | |
| ", Length: $($data.List.Count.ToString('d3')) frames") | |
| foreach ($frame in $data.List) { | |
| $file.Write($frame.Content, $offset, $frame.Content.Length - $offset) | |
| } | |
| if (!$data.List[0].IsNextAvailable) { | |
| Write-Warning "End of media database reached" | |
| break; | |
| } | |
| if ($data.List[-1].NextDateTime -le $data.List[0].DateTime) { | |
| Write-Verbose "Pausing video retrieval for a moment..." | |
| Start-Sleep -Seconds 1 | |
| } | |
| if ($data.List[-1].DateTime -gt $EndTime) { | |
| break | |
| } | |
| $data = $src.GetNext() | |
| } while ($data.List.Count -gt 0) | |
| } finally { | |
| $src.Close() | |
| $file.Flush() | |
| $file.Close() | |
| } | |
| Get-Item -LiteralPath $Path | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment