Skip to content

Instantly share code, notes, and snippets.

@joshooaj
Last active January 11, 2026 03:00
Show Gist options
  • Select an option

  • Save joshooaj/133e04568ca4491a6eea8816946d760d to your computer and use it in GitHub Desktop.

Select an option

Save joshooaj/133e04568ca4491a6eea8816946d760d to your computer and use it in GitHub Desktop.
Create carousel views for XProtect Smart Client
#Requires -Modules MilestonePSTools
<#
.SYNOPSIS
Creates a Smart Client view containing one or more camera carousel view items.
.DESCRIPTION
The New-VmsCarouselView script automates the creation of Milestone XProtect Smart
Client views containing carousel view items. A carousel view item automatically
rotates through a list of cameras at a specified interval, displaying each camera
for a set duration before moving to the next.
This script distributes the provided cameras evenly across multiple carousel view
items arranged in a grid layout. For example, if you provide 12 cameras and specify
a 2x2 layout (2 rows, 2 columns), the script creates 4 carousel view items, each
rotating through 3 cameras.
.PARAMETER ViewGroup
Specifies the ViewGroup object where the new view will be created. Use Get-VmsViewGroup
to retrieve a ViewGroup object.
.PARAMETER Name
Specifies the name for the new view. If not provided, the view will be named using
the pattern "{Rows}x{Columns} Carousel" (e.g., "2x2 Carousel").
.PARAMETER Cameras
Specifies an array of Camera objects to include in the carousel view. Cameras will
be distributed evenly across all carousel view items in the layout. Use Get-VmsCamera
to retrieve Camera objects.
.PARAMETER Rows
Specifies the number of rows in the view layout. Valid range is 1 to 10. Default is 2.
.PARAMETER Columns
Specifies the number of columns in the view layout. Valid range is 1 to 10. Default is 2.
.PARAMETER IntervalSeconds
Specifies the interval in seconds between camera rotations in each carousel. Default is 10 seconds.
.EXAMPLE
$cameras = Select-Camera -AllowFolders -RemoveDuplicates
$rootViewGroup = New-VmsViewGroup -Name 'Public Views' -Force
$subGroup = $rootViewGroup | New-VmsViewGroup -Name 'Carousels' -Force
$subGroup | .\New-VmsCarouselView.ps1 -Name 'Example View' -Cameras $cameras -Rows 2 -Columns 2 -IntervalSeconds 5
Prompt the user with a camera / camera group selection dialog, then create
a new carousel view named "Example View" in the "Public Views / Carousels"
view group with 4 carousel view items in a 2x2 grid with a 5 second display
interval.
.EXAMPLE
$rootViewGroup = New-VmsViewGroup -Name 'Public Views' -Force
$subGroup = $rootViewGroup | New-VmsViewGroup -Name 'Carousels' -Force
$cameras = Get-VmsCamera | Select-Object -First 20
$subGroup | .\New-VmsCarouselView.ps1 -Name 'Example View' -Cameras $cameras -Rows 2 -Columns 2 -IntervalSeconds 5
Creates a new view named "Example View" in the "Public Views/Carousels"
view group with a 2x2 layout (4 carousel view items), distributing 20
cameras evenly across the 4 carousels (5 cameras per carousel), rotating
every 5 seconds. If the view group already exists, the view will be added
to it.
.EXAMPLE
$vg = New-VmsViewGroup -Name 'Security' -Force | New-VmsViewGroup -Name 'All Cameras' -Force
$cameras = Get-VmsCamera -Name 'Entrance'
$vg | .\New-VmsCarouselView.ps1 -Cameras $cameras -Rows 1 -Columns 1
Creates a single carousel view item containing all cameras with names
containing "Entrance", using the default name "1x1 Carousel" and 10-second
rotation interval.
.EXAMPLE
$viewGroups = Get-VmsViewGroup -Name Operations* -Recurse | Where-Object ParentItemPath -ne '/'
$cameras = Get-VmsCamera -Name 'Perimeter'
$viewGroups | ForEach-Object {
$splat = @{
Name = 'Perimeter Cameras'
Cameras = $cameras
Rows = 3
Columns = 3
IntervalSeconds = 15
}
$_ | .\New-VmsCarouselView.ps1 @splat
}
Creates a 3x3 grid of carousel view items (9 carousels total) in all "Operations"
view groups found, distributing all cameras containing the word "perimiter"
across the 9 carousels with a 15-second rotation interval.
.EXAMPLE
$cameraGroups = Get-VmsDeviceGroup -Recurse | Where-Object { ($_ | Get-VmsDeviceGroupMember).Count -ge 18 }
$viewGroup = New-VmsViewGroup -Name 'Security' -Force | New-VmsViewGroup -Name 'Camera Groups' -Force
$cameraGroups | ForEach-Object {
$splat = @{
Name = $_.Name
Cameras = $_ | Get-VmsDeviceGroupMember
Rows = 3
Columns = 3
IntervalSeconds = 15
}
$viewGroup | .\New-VmsCarouselView.ps1 @splat
}
Find all camera groups containing at least 18 cameras, and create 3x3 views
named after those camera groups, each full of carousels with the cameras from
the matching camera group distributed evenly among them with a 15 second
display interval.
.NOTES
A single carousel view item in a Smart Client view is definited by the
following XML document. In this case, the default show-time is 10 seconds
and none of the cameras in the carousel have a custom show-time. Note that
for compatibility reasons the content of the <iteminfo> node is repeated as
the value of the <property> node with the name "carousel-item", and the
reserved XML characters have been escaped such that "<" becomes "&lt;".
<viewitem id="6a9fcafb-6ea7-4327-a0a5-56a2a59ec037" displayname="CarrouselViewItem" shortcut="" type="VideoOS.RemoteClient.Plugin.Carrousel.CarrouselViewItem, VideoOS.RemoteClient.Plugin.Carrousel" smartClientId="46909c4a-d5d8-4faf-830d-5a0df564fe7b">
<iteminfo interval="10" maintainimageaspectratio="True" usedefaultdisplaysettings="True" showtitlebar="True" usingproperties="True">
<carousel-item>
<device-id>eddd178c-3e48-45b0-8a7c-7a29f924e30f</device-id>
<show-time />
</carousel-item>
<carousel-item>
<device-id>bf49b9f2-0670-4724-b8b8-57045e2e3e16</device-id>
<show-time />
</carousel-item>
<carousel-item>
<device-id>2df8e3fb-4d34-451a-b452-9b2d2f25a897</device-id>
<show-time />
</carousel-item>
</iteminfo>
<properties>
<property name="carousel-item" value="&lt;carousel-items&gt;&lt;carousel-item&gt;&lt;device-id&gt;eddd178c-3e48-45b0-8a7c-7a29f924e30f&lt;/device-id&gt;&lt;show-time /&gt;&lt;/carousel-item&gt;&lt;carousel-item&gt;&lt;device-id&gt;bf49b9f2-0670-4724-b8b8-57045e2e3e16&lt;/device-id&gt;&lt;show-time /&gt;&lt;/carousel-item&gt;&lt;carousel-item&gt;&lt;device-id&gt;2df8e3fb-4d34-451a-b452-9b2d2f25a897&lt;/device-id&gt;&lt;show-time /&gt;&lt;/carousel-item&gt;&lt;/carousel-items&gt;" />
<property name="interval" value="10" />
</properties>
</viewitem>
#>
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[VideoOS.Platform.ConfigurationItems.ViewGroup]
$ViewGroup,
[Parameter()]
[string]
$Name,
[Parameter(Mandatory)]
[VideoOS.Platform.ConfigurationItems.Camera[]]
$Cameras,
[Parameter()]
[ValidateRange(1, 10)]
[int]
$Rows = 2,
[Parameter()]
[ValidateRange(1, 10)]
[int]
$Columns = 2,
[Parameter()]
[int]
$IntervalSeconds = 10
)
function New-CarouselItem {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[VideoOS.Platform.ConfigurationItems.Camera[]]
$Camera,
[Parameter()]
[int]
$IntervalSeconds = 10
)
begin {
$cameraList = [collections.generic.list[[object]]]::new()
}
process {
foreach ($cam in $Camera) {
$cameraList.Add($cam)
}
}
end {
$doc = [xml.xmldocument]::new()
$viewItem = $doc.AppendChild($doc.CreateElement('viewitem'))
$viewItem.Attributes.Append($doc.CreateAttribute('id')).Value = (New-Guid).ToString()
$viewItem.Attributes.Append($doc.CreateAttribute('displayname')).Value = 'CarrouselViewItem'
$viewItem.Attributes.Append($doc.CreateAttribute('shortcut')).Value = ''
$viewItem.Attributes.Append($doc.CreateAttribute('type')).Value = 'VideoOS.RemoteClient.Plugin.Carrousel.CarrouselViewItem, VideoOS.RemoteClient.Plugin.Carrousel'
$viewItem.Attributes.Append($doc.CreateAttribute('smartClientId')).Value = '46909c4a-d5d8-4faf-830d-5a0df564fe7b'
$itemInfo = $viewItem.AppendChild($doc.CreateElement('iteminfo'))
$itemInfo.Attributes.Append($doc.CreateAttribute('interval')).Value = $IntervalSeconds.ToString()
$itemInfo.Attributes.Append($doc.CreateAttribute('maintainimageaspectratio')).Value = $true.ToString()
$itemInfo.Attributes.Append($doc.CreateAttribute('usedefaultdisplaysettings')).Value = $true.ToString()
$itemInfo.Attributes.Append($doc.CreateAttribute('showtitlebar')).Value = $true.ToString()
$itemInfo.Attributes.Append($doc.CreateAttribute('usingproperties')).Value = $true.ToString()
$parent = $doc.CreateElement('carousel-items')
foreach ($cam in $cameraList) {
$item = $itemInfo.AppendChild($doc.CreateElement('carousel-item'))
$item.AppendChild($doc.CreateElement('device-id')).InnerText = $cam.Id.ToLower()
$null = $item.AppendChild($doc.CreateElement('show-time'))
$null = $parent.AppendChild($item.CloneNode($true))
}
$properties = $viewItem.AppendChild($doc.CreateElement('properties'))
$itemProperty = $properties.AppendChild($doc.CreateElement('property'))
$itemProperty.Attributes.Append($doc.CreateAttribute('name')).Value = 'carousel-item'
$itemProperty.Attributes.Append($doc.CreateAttribute('value')).Value = $parent.OuterXml | ConvertFrom-PrettyXml -Compress -OmitXmlDeclaration
$intervalProperty = $properties.AppendChild($doc.CreateElement('property'))
$intervalProperty.Attributes.Append($doc.CreateAttribute('name')).Value = 'interval'
$intervalProperty.Attributes.Append($doc.CreateAttribute('value')).Value = $IntervalSeconds.ToString()
$doc.OuterXml
}
}
function ConvertTo-PrettyXml {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string]
$Text,
[Parameter()]
[switch]
$Unescape
)
process {
try {
if ($Unescape) {
$Text = [net.webutility]::HtmlDecode($Text)
}
$ms = [io.memorystream]::new()
$xml = [xml]$Text
$xml.Save($ms)
$ms.Position = 0
$reader = [io.streamreader]::new($ms)
$reader.ReadToEnd()
} finally {
$reader.Dispose()
}
}
}
function ConvertFrom-PrettyXml {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string]
$Text,
[Parameter()]
[switch]
$Escape,
[Parameter()]
[switch]
$Compress,
[Parameter()]
[switch]
$OmitXmlDeclaration,
[Parameter()]
[System.Text.Encoding]
$Encoding = [text.encoding]::UTF8
)
process {
$xml = [xml]$Text
$settings = [xml.xmlwritersettings]@{
Encoding = $Encoding
OmitXmlDeclaration = $OmitXmlDeclaration
}
if ($Compress) {
$settings.NewLineChars = ''
$settings.Indent = $false
}
$sb = [text.stringbuilder]::new()
$writer = [xml.xmlwriter]::Create($sb, $settings)
$xml.Save($writer)
$result = $sb.ToString()
if ($Escape) {
$result = [net.webutility]::HtmlEncode($result)
}
$result
}
}
function New-CarouselView {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[VideoOS.Platform.ConfigurationItems.ViewGroup]
$ViewGroup,
[Parameter()]
[string]
$Name,
[Parameter(Mandatory)]
[VideoOS.Platform.ConfigurationItems.Camera[]]
$Cameras,
[Parameter()]
[ValidateRange(1, 10)]
[int]
$Rows = 2,
[Parameter()]
[ValidateRange(1, 10)]
[int]
$Columns = 2,
[Parameter()]
[int]
$IntervalSeconds = 10
)
process {
if ([string]::IsNullOrWhiteSpace($Name)) {
$Name = "$($Rows)x$($Columns) Carousel"
}
$view = $ViewGroup | New-VmsView -Name $Name -Columns $Rows -Rows $Columns
# Create one list per view item
$viewItemCount = $Rows * $Columns
$groups = @{}
for ($i = 0; $i -lt $viewItemCount; $i++) {
$groups[$i] = [collections.generic.list[[object]]]::new()
}
# Divide cameras into the camera lists evenly
for ($i = 0; $i -lt $Cameras.Count; $i++) {
$groups[$i % $viewItemCount].Add($Cameras[$i])
}
for ($i = 0; $i -lt $view.ViewItemChildItems.Count; $i++) {
$view.ViewItemChildItems[$i].ViewItemDefinitionXml = New-CarouselItem -Camera $groups[$i] -IntervalSeconds $IntervalSeconds
}
$view.Save()
$view
}
}
$splat = @{
ViewGroup = $ViewGroup
Cameras = $Cameras
Rows = $Rows
Columns = $Columns
IntervalSeconds = $IntervalSeconds
}
if (![string]::IsNullOrWhiteSpace($Name)) {
$splat.Name = $Name
}
New-CarouselView @splat
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment