Public/Reports/Get-CameraReport.ps1
function Get-CameraReport { <# .SYNOPSIS Gets detailed information for all cameras in the current site .DESCRIPTION This camera report is a rewrite of the previous camera report available in version 20.3.450930 and earlier. This version makes use of runspaces to significantly improve the time taken to generate the report, and includes several additional properties in each row that were not present in prior versions. Use Get-CameraReport to get a snapshot in time of most of the configuration properties for each camera and the state of cameras in the VMS. For example, you can use this report to see the current video retention for each camera, or whether any cameras are in media overflow or have communication errors. You might pipe this report to CSV and open the report in Excel, group by Recording Server, and get a sense for the number of cameras per Recording Server or create a pivot table on cameras and their resolutions and frame rates. Thanks to the 60+ columns of information provided for each camera in this report, there are innumerable ways in which you could use it to help identify problems, maintain a consistent configuration, or report data to other departments. Column Notes - State: This is returned by Get-ItemState and represents the Milestone Event Server's understanding of the state of the camera. The physical camera may be responding to ping but still be 'Not Responding' in Milestone due to other issues. - MediaOverflow: This means the Recording Server is unable to keep up with the amount of video being recorded, and is dropping video frames from one or more cameras. Any time this value is true, you should investigate Recording Server performance. - GpsCoordinates: This value will be returned as 'Unknown' if not set. The value comes from the Camera object's GisPoint property. - MediaDatabaseBegin: This timestamp represents the time and date, in UTC, of the first image in the media database. If your video retention is 30 days, you should expect this timestamp to be at least 30 days ago under normal circumstances. - MediaDatabaseEnd: This timestamp represents the time and date, in UTC, of the last recorded image in the media database. If your camera is set to record always, this timestamp should be very recent. - UsedSpaceInBytes: This value represents the disk space utilization for the camera on the Recording Server to which it belongs, including data in any live and archive storage to which the camera is assigned. - PercentRecordedOneWeek: This optional value is only set when you include the -IncludeRecordingStats parameter. It may take significantly longer to generate the report when including this information. The value will be the percentage of time over the previous 7 days for which recordings exist. When recording on motion, in an environment with 50% motion, this value will normally be a little higher than 50% to account for pre and post recording buffers. - LastModified: This should represent the last configuration change made to the camera. - Password: This optional value is only set when you include the -IncludePasswords parameter. - HTTPSEnabled: For cameras supporting HTTPS and where HTTPS is enabled at the hardware level in Management Client, you will see the value "YES". Otherwise, you will see the value "NO". - Firmware: This value may NOT be accurate. For example, if the firmware has been upgraded since the camera was added to Milestone, the value may still reflect the old firmware version. This is only because Milestone may only update our record of the firmware when the camera is added or replaced in Management Client. You might consider this value as an indication that the firmware is 'at least' X.Y for example. - DriverNumber: This represents the Milestone ID number for the device driver used for this camera. When adding hardware using Import-HardwareCsv or Add-Hardware, this is the number Milestone would expect when specifying which driver to use. - DriverRevision: This is an internal value representing the driver number for the specific device driver within the currently installed device pack. - RecordingPath: This value is always relative to the Recording Server. If you see a drive letter F:, for example, then it refers to the F: drive on the Recording Server to which the camera is assigned. - Snapshot: This optional value is only set when you include the -IncludeSnapshots parameter. The value will be an [image] object and it will be up to you to decide what to do with the value once you have it. Exporting to CSV for example will result in the string [drawing.image] rather than an actual image or image data. .EXAMPLE PS C:\> Get-CameraReport | Export-Csv -Path .\camera-report.csv -NoTypeInformation Gets a camera report and saves the contents to a CSV file in the current folder. .EXAMPLE PS C:\> Get-CameraReport -IncludeRecordingStats | Export-Csv -Path .\camera-report.csv -NoTypeInformation Gets a camera report with an additional call to Get-CameraRecordingStats which will provide the percentage of time recorded over the last 7 days for each camera. This uses runspaces to help process the requests in parallel and uses Get-SequenceData under the surface to gather the recording sequence data to determine how much time out of the last week was spent recording for each camera. .NOTES If you see the value "NotAvailable" it typically means the camera was disabled, there were no recordings available, the state of the camera is not "Responding", or the property value requested, such as Resolution, does not exist in the stream properties for the camera. #> [CmdletBinding()] param ( # Specifies one or more Recording Servers from which to generate a camera report [Parameter(ValueFromPipeline)] [VideoOS.Platform.ConfigurationItems.RecordingServer[]] $RecordingServer, # Include plain text hardware passwords in the report [Parameter()] [switch] $IncludePlainTextPasswords, # Specifies that disabled cameras should be excluded from the results [Parameter()] [switch] $IncludeDisabled, # Specifies that a live JPEG snapshot should be requested for each camera. The snapshot will be attached as an [drawing.image] object # and it is up to you to decide how to process these images. Some 3rd party modules may allow you to pipe the camera report to Excel # for example. Alternatively, you may manually process each row of the report to save the images to disk in a path of your choice. [Parameter()] [switch] $IncludeSnapshots, # Specifies the height of the snapshots included if the IncludeSnapshots switch is provided. The default is 300 pixels high. The width will be determined based on the aspect ratio of the original image. # A value of 0 will result in snapshots at the original image resolution of the live stream at the time the report is run. [Parameter()] [ValidateRange(0, [int]::MaxValue)] [int] $SnapshotHeight = 300, # Specifies that you want to add the percentage of time recorded in the past week for each camera [Parameter()] [switch] $IncludeRecordingStats ) begin { $null = Get-ManagementServer -ErrorAction Stop $initialSessionState = [initialsessionstate]::CreateDefault() foreach ($functionName in @('Get-StreamProperties', 'ConvertFrom-StreamUsage', 'Get-ValueDisplayName', 'ConvertFrom-Snapshot', 'ConvertFrom-GisPoint')) { $definition = Get-Content Function:\$functionName -ErrorAction Stop $sessionStateFunction = [System.Management.Automation.Runspaces.SessionStateFunctionEntry]::new($functionName, $definition) $initialSessionState.Commands.Add($sessionStateFunction) } $runspacepool = [runspacefactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1), $initialSessionState, $Host) $runspacepool.Open() $threads = New-Object System.Collections.Generic.List[pscustomobject] $processDevice = { param( [VideoOS.Platform.Messaging.ItemState[]]$States, [VideoOS.Platform.ConfigurationItems.RecordingServer]$RecordingServer, [hashtable]$VideoDeviceStatistics, [hashtable]$CurrentDeviceStatus, [hashtable]$RecordingStats, [hashtable]$StorageTable, [VideoOS.Platform.ConfigurationItems.Hardware]$Hardware, [VideoOS.Platform.ConfigurationItems.Camera]$Camera, [bool]$IncludePasswords, [bool]$IncludeSnapshots, [int]$SnapshotHeight ) $returnResult = [pscustomobject]@{ Data = $null ErrorRecord = $null } try { $cameraEnabled = $Hardware.Enabled -and $Camera.Enabled $streamUsages = $Camera | Get-Stream -All $liveStreamName = $streamUsages | Where-Object LiveDefault | ConvertFrom-StreamUsage $recordStreamName = $streamUsages | Where-Object Record | ConvertFrom-StreamUsage $liveStreamSettings = $Camera | Get-StreamProperties -StreamName $liveStreamName $recordedStreamSettings = if ($liveStreamName -eq $recordStreamName) { $liveStreamSettings } else { $Camera | Get-StreamProperties -StreamName $recordStreamName } $motionDetection = $Camera.MotionDetectionFolder.MotionDetections[0] $hardwareSettings = $Hardware | Get-HardwareSetting $playbackInfo = @{ Begin = 'NotAvailable'; End = 'NotAvailable'} if ($cameraEnabled -and $camera.RecordingEnabled) { $tempPlaybackInfo = $Camera | Get-PlaybackInfo -ErrorAction Ignore -WarningAction Ignore if ($null -ne $tempPlaybackInfo) { $playbackInfo = $tempPlaybackInfo } } $driver = $Hardware | Get-HardwareDriver $password = '' if ($IncludePasswords) { try { $password = $Hardware | Get-HardwarePassword -ErrorAction Ignore } catch { $password = $_.Message } } $cameraState = if ($cameraEnabled -and $null -ne $States) { $States | Where-Object { $_.FQID.ObjectId -eq $Camera.Id } | Select-Object -ExpandProperty State } else { 'NotAvailable' } $cameraStatus = $CurrentDeviceStatus.$($RecordingServer.Id).CameraDeviceStatusArray | Where-Object DeviceId -eq $Camera.Id $statistics = $VideoDeviceStatistics.$($RecordingServer.Id) | Where-Object DeviceId -eq $Camera.Id $currentLiveFps = $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object LiveStreamDefault | Select-Object -ExpandProperty FPS -First 1 $currentRecFps = $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object RecordingStream | Select-Object -ExpandProperty FPS -First 1 $expectedRetention = New-Timespan -Minutes ($StorageTable.$($Camera.RecordingStorage) | ForEach-Object { $_; $_.ArchiveStorageFolder.ArchiveStorages } | Sort-Object RetainMinutes -Descending | Select-Object -First 1 -ExpandProperty RetainMinutes) $snapshot = $null if ($IncludeSnapshots -and $cameraEnabled -and $cameraStatus.Started -and $cameraState -eq 'Responding') { $snapshot = $Camera | Get-Snapshot -Live -Quality 100 -ErrorAction Ignore | ConvertFrom-Snapshot if ($SnapshotHeight -ne 0 -and $null -ne $snapshot) { $snapshot = $snapshot | Resize-Image -Height $SnapshotHeight -DisposeSource } } elseif (!$IncludeSnapshots) { $snapshot = 'NotRequested' } $returnResult.Data = [pscustomobject]@{ Name = $Camera.Name Channel = $Camera.Channel Enabled = $cameraEnabled State = $cameraState MediaOverflow = if ($cameraEnabled) { $cameraStatus.ErrorOverflow } else { 'NotAvailable' } DbRepairInProgress = if ($cameraEnabled) { $cameraStatus.DbRepairInProgress } else { 'NotAvailable' } DbWriteError = if ($cameraEnabled) { $cameraStatus.ErrorWritingGop } else { 'NotAvailable' } GpsCoordinates = $Camera | ConvertFrom-GisPoint MediaDatabaseBegin = $playbackInfo.Begin MediaDatabaseEnd = $playbackInfo.End UsedSpaceInBytes = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty UsedSpaceInBytes } else { 'NotAvailable' } PercentRecordedOneWeek = if ($cameraEnabled -and $RecordingStats.$($Camera.Id).PercentRecorded -is [double]) { $RecordingStats.$($Camera.Id).PercentRecorded } else { 'NotAvailable' } LastModified = $Camera.LastModified Id = $Camera.Id HardwareName = $Hardware.Name Address = $Hardware.Address Username = $Hardware.UserName Password = $password HTTPSEnabled = if ($null -ne $hardwareSettings.HTTPSEnabled) { $hardwareSettings.HTTPSEnabled.ToUpper() } else { 'NO' } MAC = $hardwareSettings.MacAddress Firmware = $hardwareSettings.FirmwareVersion Model = $Hardware.Model Driver = $driver.Name DriverNumber = $driver.Number.ToString() DriverRevision = $driver.DriverRevision HardwareId = $Hardware.Id RecorderName = $RecordingServer.Name RecorderUri = $RecordingServer.WebServerUri RecorderId = $RecordingServer.Id ConfiguredLiveResolution = Get-ValueDisplayName -PropertyList $liveStreamSettings -PropertyName 'Resolution', 'StreamProperty' ConfiguredLiveCodec = Get-ValueDisplayName -PropertyList $liveStreamSettings -PropertyName 'Codec' ConfiguredLiveFPS = Get-ValueDisplayName -PropertyList $liveStreamSettings -PropertyName 'FPS', 'Framerate' LiveMode = $streamUsages | Where-Object LiveDefault | Select-Object -ExpandProperty LiveMode ConfiguredRecordResolution = Get-ValueDisplayName -PropertyList $recordedStreamSettings -PropertyName 'Resolution', 'StreamProperty' #GetResolution -PropertyList $recordedStreamSettings ConfiguredRecordCodec = Get-ValueDisplayName -PropertyList $recordedStreamSettings -PropertyName 'Codec' ConfiguredRecordFPS = Get-ValueDisplayName -PropertyList $recordedStreamSettings -PropertyName 'FPS', 'Framerate' CurrentLiveResolution = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object LiveStreamDefault | Select-Object -ExpandProperty ImageResolution -First 1 | Foreach-Object { "$($_.Width)x$($_.Height)" } } else { 'NotAvailable' } CurrentLiveFPS = if ($cameraEnabled -and $currentLiveFps -is [double]) { [math]::Round($currentLiveFps, 1) } else { 'NotAvailable' } CurrentLiveBPS = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object LiveStreamDefault | Select-Object -ExpandProperty BPS -First 1 } else { 'NotAvailable' } CurrentRecordedResolution = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object RecordingStream | Select-Object -ExpandProperty ImageResolution -First 1 | Foreach-Object { "$($_.Width)x$($_.Height)" } } else { 'NotAvailable' } CurrentRecordedFPS = if ($cameraEnabled -and $currentRecFps -is [double]) { [math]::Round($currentRecFps, 1) } else { 'NotAvailable' } CurrentRecordedBPS = if ($cameraEnabled) { $statistics | Select-Object -ExpandProperty VideoStreamStatisticsArray | Where-Object RecordingStream | Select-Object -ExpandProperty BPS -First 1 } else { 'NotAvailable' } RecordingEnabled = $Camera.RecordingEnabled RecordKeyframesOnly = $Camera.RecordKeyframesOnly RecordOnRelatedDevices = $Camera.RecordOnRelatedDevices PrebufferEnabled = $Camera.PrebufferEnabled PrebufferSeconds = $Camera.PrebufferSeconds PrebufferInMemory = $Camera.PrebufferInMemory RecordingStorageName = $StorageTable.$($Camera.RecordingStorage).Name RecordingPath = [io.path]::Combine($StorageTable.$($Camera.RecordingStorage).DiskPath, $StorageTable.$($Camera.RecordingStorage).Id) ExpectedRetention = $expectedRetention ActualRetention = if ($playbackInfo.Begin -is [string]) { 'NotAvailable' } else { [datetime]::UtcNow - $playbackInfo.Begin } MeetsRetentionPolicy = if ($playbackInfo.Begin -is [string]) { 'NotAvailable' } else { ([datetime]::UtcNow - $playbackInfo.Begin) -ge $expectedRetention } MotionEnabled = $motionDetection.Enabled MotionKeyframesOnly = $motionDetection.KeyframesOnly MotionProcessTime = $motionDetection.ProcessTime MotionSensitivityMode = if ($motionDetection.ManualSensitivityEnabled) { 'Manual' } else { 'Automatic' } MotionManualSensitivity = $motionDetection.ManualSensitivity MotionMetadataEnabled = $motionDetection.GenerateMotionMetadata MotionExcludeRegions = if ($motionDetection.UseExcludeRegions) { 'Yes' } else { 'No' } MotionHardwareAccelerationMode = $motionDetection.HardwareAccelerationMode PrivacyMaskEnabled = $Camera.PrivacyProtectionFolder.PrivacyProtections[0].Enabled Snapshot = $snapshot } } catch { $returnResult.ErrorRecord = $_ } return $returnResult } } process { $progressParams = @{ Activity = 'Camera Report' CurrentOperation = '' Status = 'Preparing to run report' PercentComplete = 0 Completed = $false } if ($null -eq $RecordingServer) { Write-Verbose "Getting a list of all recording servers on $((Get-ManagementServer).Name)" $progressParams.CurrentOperation = 'Getting Recording Servers' Write-Progress @progressParams $RecordingServer = Get-RecordingServer } Write-Verbose 'Getting the current state of all cameras' $progressParams.CurrentOperation = 'Calling Get-ItemState' Write-Progress @progressParams $itemState = Get-ItemState if ($null -eq $itemState) { Write-Warning 'Get-ItemState failed which indicates the Milestone Event Server service may not be 100% functional. It may take longer than normal to run this report if any servers or cameras are not responding.' } Write-Verbose 'Discovering all cameras and retrieving status and statistics' try { $progressParams.CurrentOperation = 'Calling Get-VideoDeviceStatistics on all responding recording servers' Write-Progress @progressParams $respondingRecordingServers = if ($null -eq $itemState) { $RecordingServer.Id } else { $RecordingServer.Id | Where-Object { $id = $_; $id -in $itemState.FQID.ObjectId -and ($itemState | Where-Object { $id -eq $_.FQID.ObjectId }).State -eq 'Server Responding' } } $respondingCameras = if ($null -eq $itemState) { (Get-PlatformItem -Kind ([videoos.platform.kind]::camera)).fqid.objectid } else { ($itemState | Where-Object { $_.FQID.Kind -eq [videoos.platform.kind]::Camera -and $_.State -eq 'Responding' }).FQID.ObjectId } $videoDeviceStatistics = Get-VideoDeviceStatistics -AsHashtable -RecordingServerId $respondingRecordingServers -RunspacePool $runspacepool $progressParams.CurrentOperation = 'Calling Get-CurrentDeviceStatus on all responding recording servers' Write-Progress @progressParams $currentDeviceStatus = Get-CurrentDeviceStatus -AsHashtable -RecordingServerId $respondingRecordingServers -RunspacePool $runspacepool $recordingStats = @{} if ($IncludeRecordingStats -and $respondingCameras.Count -gt 0) { $progressParams.CurrentOperation = "Retrieving 7 days of recording stats for $($respondingCameras.Count) cameras using Get-CameraRecordingStats" Write-Progress @progressParams $recordingStats = Get-CameraRecordingStats -Id $respondingCameras -AsHashTable -RunspacePool $runspacepool } $progressParams.CurrentOperation = 'Adding camera information requests to the queue' Write-Progress @progressParams $storageTable = @{} foreach ($rs in $RecordingServer) { $rs.StorageFolder.Storages | Foreach-Object { $_.FillChildren('StorageArchive') $storageTable.$($_.Path) = $_ } foreach ($hw in $rs | Get-Hardware) { foreach ($cam in $hw | Get-Camera) { if (!$IncludeDisabled -and -not ($cam.Enabled -and $hw.Enabled)) { continue } $ps = [powershell]::Create() $ps.RunspacePool = $runspacepool $asyncResult = $ps.AddScript($processDevice).AddParameters(@{ States = $itemState RecordingServer = $rs VideoDeviceStatistics = $videoDeviceStatistics CurrentDeviceStatus = $currentDeviceStatus RecordingStats = $recordingStats StorageTable = $storageTable Hardware = $hw Camera = $cam IncludePasswords = $IncludePlainTextPasswords IncludeSnapshots = $IncludeSnapshots SnapshotHeight = $SnapshotHeight }).BeginInvoke() $threads.Add([pscustomobject]@{ PowerShell = $ps Result = $asyncResult Camera = $cam.Name }) } } } if ($threads.Count -eq 0) { return } $progressParams.CurrentOperation = 'Processing' $completedThreads = New-Object System.Collections.Generic.List[pscustomobject] $totalDevices = $threads.Count while ($threads.Count -gt 0) { $progressParams.PercentComplete = ($totalDevices - $threads.Count) / $totalDevices * 100 $progressParams.Status = "Processed $($totalDevices - $threads.Count) out of $totalDevices cameras" Write-Progress @progressParams foreach ($thread in $threads) { if ($thread.Result.IsCompleted) { $result = $thread.PowerShell.EndInvoke($thread.Result) if ($null -ne $result.ErrorRecord) { Write-Error -ErrorRecord $result.ErrorRecord } if ($null -ne $result.Data) { Write-Output $result.Data } elseif ($null -eq $result.ErrorRecord) { Write-Error "Expected to receive a row of data for camera named `"$($thread.Camera)`" but received a `$null response instead. This shouldn't happen. Consider reporting an issue on GitHub at https://github.com/MilestoneSystemsInc/PowerShellSamples" } $thread.PowerShell.Dispose() $completedThreads.Add($thread) } } $completedThreads | Foreach-Object { [void]$threads.Remove($_)} $completedThreads.Clear() if ($threads.Count -eq 0) { break; } Start-Sleep -Seconds 1 } } finally { if ($threads.Count -gt 0) { Write-Warning "Stopping $($threads.Count) running PowerShell instances. This may take a minute. . ." foreach ($thread in $threads) { $thread.PowerShell.Dispose() } } $runspacepool.Close() $runspacepool.Dispose() $progressParams.Completed = $true Write-Progress @progressParams } } } |