Last active
January 11, 2026 03:00
-
-
Save joshooaj/133e04568ca4491a6eea8816946d760d to your computer and use it in GitHub Desktop.
Create carousel views for XProtect Smart Client
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 | |
| 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 "<". | |
| <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="<carousel-items><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></carousel-items>" /> | |
| <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