Skip to content

Instantly share code, notes, and snippets.

@joshooaj
Last active March 13, 2026 23:18
Show Gist options
  • Select an option

  • Save joshooaj/5231f115dd46316394fecd3c96bb36c5 to your computer and use it in GitHub Desktop.

Select an option

Save joshooaj/5231f115dd46316394fecd3c96bb36c5 to your computer and use it in GitHub Desktop.
Save raw video data from an XProtect VMS to disk
#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