Hikvision camera picture download Windows ISAPI utility... downloads pictures from SD card ready for timelapse

Code:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'                                                        HIK CAMERA PICTURE DOWNLOAD UTILITY
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Strict On
Option Explicit On

Imports System.ComponentModel
Imports System.IO
Imports System.Net
Imports System.Net.Http
Imports System.Text

Public Class Form1

    Dim PicName(1000000) As String
    Dim PicAddress(1000000) As String
    Dim PicDTime(1000000) As DateTime
    Dim PicSize(1000000) As String
    Dim PicPlaybackURI(1000000) As String

    Dim picIndex, newStartNum, newEndNum As Integer
    Dim searchFrom As DateTime
    Dim requestCancel As Boolean = False

    Dim cameraCredentials As New NetworkCredential
    Dim cameraImage As Drawing.Image

    Dim iniFile As FileStream

    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    '                                                               MAIN FORM LOAD
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        If Not File.Exists("HikCameraPictureDownload.ini") Then
            UpdateIniFile()
        End If
        iniFile = File.Open("HikCameraPictureDownload.ini", CType(FileAccess.ReadWrite, FileMode))
        Dim iniReader As New StreamReader(iniFile)
        Username.Text = iniReader.ReadLine()
        DownloadFolder.Text = iniReader.ReadLine()
        Do While Not iniReader.EndOfStream
            IpInfo.Items.Add(iniReader.ReadLine())
        Loop
        iniFile.Close()

        UtcOffset.Value = Convert.ToInt32((DateTime.Now - DateTime.UtcNow).TotalHours)
        cameraCredentials.UserName = Username.Text
        cameraCredentials.Password = Password.Text


    End Sub

    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    '                                                             CONNECT BUTTON CLICK
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Async Sub ConnectButton_Click(sender As Object, e As EventArgs) Handles ConnectButton.Click

        If Not OkToConnect() Then
            Return
        End If
        EnableConnectInputs(False)
        requestCancel = False
        CancelButton.Enabled = True

        Try                                                                     ' REQUEST CAMERA NAME
            Dim cameraHandler As New HttpClientHandler With {.Credentials = cameraCredentials}
            Dim cameraClient As New HttpClient(cameraHandler) With {.Timeout = TimeSpan.FromSeconds(30)}
            Dim cameraRequest As New HttpRequestMessage With {
                .Method = HttpMethod.Get,
                .RequestUri = New Uri("http://" + IpInfo.Text + "/ISAPI/Streaming/channels/101/capabilities"),
                .Content = New StringContent("", Encoding.UTF8)
            }
            Dim cameraResponse = cameraClient.Send(cameraRequest)
            cameraResponse.EnsureSuccessStatusCode()
            Dim cameraStream = Await cameraResponse.Content.ReadAsStringAsync().ConfigureAwait(True)
            Dim Xml = cameraStream.ToString
            Xml = Replace(Xml, " version=""2.0"" xmlns=""http://www.hikvision.com/ver20/XMLSchema""", "", 1, 1)
            Dim xElement As XElement = XElement.Parse(Xml, options:=LoadOptions.None)
            CameraName.Text = VerifyCameraName(xElement.Descendants("channelName").Value).ToString
            If CameraName.Text = "" Then
                Throw New Exception("Camera has no name.")
            Else
                CameraName.BackColor = Color.Khaki
                If Not IpInfo.Items.Contains(IpInfo.Text) Then
                    IpInfo.Items.Add(IpInfo.Text)
                    UpdateIniFile()
                End If
            End If
        Catch exceptionError As Exception
            MsgBox(String.Format("Error: {0}", exceptionError.Message), vbDefaultButton2, "Error connecting...")
            Disconnect()
            EnableConnectInputs(True)
            CancelButton.Enabled = False
            Return
        End Try

        If SkipPrevious.Checked Then                                      ' CHECK FILES ON DISK PREVIOUSLY
            If Directory.Exists(DownloadFolder.Text + "\" + CameraName.Text) Then
                OldCount.Value = Directory.GetFiles(DownloadFolder.Text + "\" + CameraName.Text).Length

                If OldCount.Value > 0 Then
                    Dim oldestFile = Directory.GetFiles(DownloadFolder.Text + "\" + CameraName.Text).
                                              OrderByDescending(Function(f) New FileInfo(f).LastWriteTime).Last()
                    oldestFile = Replace(oldestFile, DownloadFolder.Text + "\" + CameraName.Text +
                                                                            "\" + CameraName.Text + "_", "")
                    oldestFile = Replace(oldestFile, ".jpg", "")
                    OldStart.Text = StrDateToDT(oldestFile).ToString("yyyy/MM/dd HH:mm:ss")

                    Dim newestFile = Directory.GetFiles(DownloadFolder.Text + "\" + CameraName.Text).
                                               OrderByDescending(Function(f) New FileInfo(f).LastWriteTime).First()
                    CameraMainPicture.Load(newestFile) 'Load the picture of just the newest existing file
                    LoadPicture(CameraMainPicture.Image, False)
                    newestFile = Replace(newestFile, DownloadFolder.Text + "\" + CameraName.Text + "\" +
                                                                                    CameraName.Text + "_", "")
                    newestFile = Replace(newestFile, ".jpg", "")
                    OldEnd.Text = StrDateToDT(newestFile).ToString("yyyy/MM/dd HH:mm:ss")

                    PicDTime(1) = StrDateToDT(newestFile) 'This is needed to keep the progress bar accurate
                    searchFrom = StrDateToDT(newestFile).AddSeconds(1)
                    picIndex = CInt(OldCount.Value + 1)
                End If
            End If
        End If
        SkipPrevious.Enabled = False
        SkipPrevious.Checked = False

        Try                                                               ' REQUEST NEW PICTURE BATCHES
            Dim searchTo = Now
            Dim picBatchAmt = 50
            While picBatchAmt = 50
                Dim cameraHandler As New HttpClientHandler With {.Credentials = cameraCredentials}
                Dim cameraClient As New HttpClient(cameraHandler) With {.Timeout = TimeSpan.FromSeconds(30)}
                Dim cameraRequest As New HttpRequestMessage With {
                    .Method = HttpMethod.Post,
                    .RequestUri = New Uri("http://" + IpInfo.Text + "/ISAPI/contentMgmt/search"),
                    .Content = New StringContent("<CMSearchDescription><searchID>ID</searchID><trackIDList>" +
                                        "<trackID>103</trackID></trackIDList><timeSpanList><timeSpan>" +
                                        "<startTime>" + HikDate(searchFrom.AddHours(-UtcOffset.Value)) + "</startTime>" +
                                        "<endTime>" + HikDate(searchTo.AddHours(-UtcOffset.Value)) + "</endTime>" +
                                        "</timeSpan></timeSpanList><contentTypeList>" +
                                        "<contentType>metadata</contentType></contentTypeList>" +
                                        "<maxResults>50</maxResults><searchResultPostion>0</searchResultPostion>" +
                                        "<metadataList><metadataDescriptor>/recordType.meta.std-cgi.com/CMR" +
                                        "</metadataDescriptor></metadataList></CMSearchDescription>", Encoding.UTF8)
                }
                Dim cameraResponse = cameraClient.Send(cameraRequest)
                Dim cameraStream = Await cameraResponse.Content.ReadAsStringAsync().ConfigureAwait(True)
                Dim Xml = cameraStream.ToString
                Xml = Replace(Xml, " version=""2.0"" xmlns=""http://www.hikvision.com/ver20/XMLSchema""", "", 1, 1)
                Dim xElement As XElement = XElement.Parse(Xml, options:=LoadOptions.None)
                picBatchAmt = CInt(xElement.Descendants("numOfMatches").Value)
                If picBatchAmt = 0 And picIndex = 0 Then
                    Throw New Exception("No pictures on " + CameraName.Text + ".")
                End If

                Dim CMSearchResult = xElement.Descendants("playbackURI")          ' CREATE ARRAY OF PICTURES
                For Each playbackURI As XElement In CMSearchResult
                    picIndex += 1
                    PicPlaybackURI(picIndex) = playbackURI.Value

                    Dim SearchWithinThis = playbackURI.Value
                    Dim SearchForThis = "http://"
                    Dim FirstChar = SearchWithinThis.IndexOf(SearchForThis)
                    SearchForThis = "/ISAPI"
                    Dim LastChar = SearchWithinThis.IndexOf(SearchForThis)
                    If LastChar = -1 Then
                        SearchForThis = "/Streaming"
                        LastChar = SearchWithinThis.IndexOf(SearchForThis)
                    End If
                    PicAddress(picIndex) = Mid(playbackURI.Value, FirstChar + 8, LastChar - FirstChar - 7)

                    SearchWithinThis = playbackURI.Value
                    SearchForThis = "starttime="
                    FirstChar = SearchWithinThis.IndexOf(SearchForThis)
                    PicDTime(picIndex) = StrDateToDT(Mid(playbackURI.Value, FirstChar + 11, 8) +
                                                               Mid(playbackURI.Value, FirstChar + 20, 6))
                    PicDTime(picIndex) = PicDTime(picIndex).AddHours(UtcOffset.Value)

                    SearchWithinThis = playbackURI.Value
                    SearchForThis = "size="
                    FirstChar = SearchWithinThis.IndexOf(SearchForThis)
                    PicSize(picIndex) = Mid(playbackURI.Value, FirstChar + 6, playbackURI.Value.Length - FirstChar - 4)

                    SearchWithinThis = playbackURI.Value
                    SearchForThis = "name="
                    FirstChar = SearchWithinThis.IndexOf(SearchForThis)
                    SearchForThis = "size="
                    LastChar = SearchWithinThis.IndexOf(SearchForThis)
                    PicName(picIndex) = Mid(playbackURI.Value, FirstChar + 6, LastChar - FirstChar - 6)

                    Dim jpgFilename = DownloadFolder.Text + "\" + CameraName.Text + "\" + CameraName.Text + "_" +
                                      PicDTime(picIndex).ToString("yyyyMMddHHmmss") + ".jpg"

                    If File.Exists(jpgFilename) Then
                        OldCount.Value += 1
                        If OldCount.Value = 1 Then
                            OldStart.Text = PicDTime(picIndex).ToString("yyyy/MM/dd HH:mm:ss")
                        End If
                        OldEnd.Text = PicDTime(picIndex).ToString("yyyy/MM/dd HH:mm:ss")
                        If PreviewOldCheckBox.Checked = True Then
                            CameraMainPicture.Load(jpgFilename)
                            LoadPicture(CameraMainPicture.Image, False)
                        End If
                    Else
                        PreviewOldCheckBox.Enabled = False
                        PreviewOldCheckBox.Checked = False
                        NewCount.Value += 1
                        If newStartNum = 0 Then
                            newStartNum = picIndex
                            NewStart.Text = PicDTime(picIndex).ToString("yyyy/MM/dd HH:mm:ss")
                            RequestPicture(picIndex) 'Load just first new pic while connecting
                            LoadPicture(cameraImage, True)
                        End If
                        newEndNum = picIndex
                        NewEnd.Text = PicDTime(picIndex).ToString("yyyy/MM/dd HH:mm:ss")
                    End If
                    searchFrom = PicDTime(picIndex).AddSeconds(1)

                    ProgressBar.Value = CInt(100 * (1 - (DateDiff("n", PicDTime(picIndex), searchTo) /
                                                         DateDiff("n", PicDTime(1), searchTo))))
                    My.Application.DoEvents()
                    System.Threading.Thread.Sleep((100 - CpuDelay.Value) * 10)
                    If requestCancel = True Then
                        Disconnect()
                        EnableConnectInputs(True)
                        Return
                    End If
                Next
            End While
        Catch exceptionError As Exception
            MsgBox(String.Format("Error: {0}", exceptionError.Message), vbDefaultButton2, "Error Connecting")
            Disconnect()
            EnableConnectInputs(True)
            CancelButton.Enabled = False
            Return
        End Try

        PreviewOldCheckBox.Enabled = False
        PreviewOldCheckBox.Checked = False
        If NewCount.Value > 0 Then
            DownloadButton.Enabled = True
            PreviewNewCheckBox.Enabled = True
            PreviewNewCheckBox.Checked = True
        Else
            EnableConnectInputs(True)
            SkipPrevious.Enabled = True
            SkipPrevious.Checked = True
        End If

    End Sub

    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    '                                                          DOWNLOAD BUTTON CLICK
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub DownloadButton_Click(sender As Object, e As EventArgs) Handles DownloadButton.Click

        DownloadButton.Enabled = False
        requestCancel = False
        Try
            If Not Directory.Exists(DownloadFolder.Text + "\" + CameraName.Text) Then
                My.Computer.FileSystem.CreateDirectory(DownloadFolder.Text + "\" + CameraName.Text)
            End If
            For picIndex = newStartNum To newEndNum

                RequestPicture(picIndex)

                Dim jpgFilename = DownloadFolder.Text + "\" + CameraName.Text + "\" + CameraName.Text + "_" +
                                                              StrDate(PicDTime(picIndex)) + ".jpg"
                cameraImage.Save(jpgFilename)
                If PreviewNewCheckBox.Checked = True Or picIndex = newStartNum Then
                    LoadPicture(cameraImage, True)
                End If

                If picIndex = newStartNum And OldStart.Text = "" Then
                    OldStart.Text = PicDTime(picIndex).ToString("yyyy/MM/dd HH:mm:ss")
                End If
                OldEnd.Text = PicDTime(picIndex).ToString("yyyy/MM/dd HH:mm:ss")
                OldCount.Value = OldCount.Value + 1
                If picIndex <> newEndNum Then
                    NewStart.Text = PicDTime(picIndex + 1).ToString("yyyy/MM/dd HH:mm:ss")
                    NewCount.Value = NewCount.Value - 1
                    ProgressBar.Value = CInt(100 * (1 - ((newEndNum - picIndex) / (newEndNum - newStartNum))))
                Else
                    NewStart.Text = ""
                    NewEnd.Text = ""
                    NewCount.Value = NewCount.Value - 1
                    newStartNum = 0
                    ProgressBar.Value = 100
                End If

                My.Application.DoEvents()
                Threading.Thread.Sleep((100 - CpuDelay.Value) * 5)
                If requestCancel = True Then
                    Disconnect()
                    EnableConnectInputs(True)
                    Return
                End If
            Next
        Catch exceptionError As Exception
            MsgBox(String.Format("Error: {0}", exceptionError.Message), vbDefaultButton2, "Error Downloading")
            Disconnect()
            EnableConnectInputs(True)
            CancelButton.Enabled = False
            Return
        End Try
        EnableConnectInputs(True)
        CancelButton.Enabled = False

    End Sub

    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    '                                                         CANCEL BUTTON CLICK
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub CancelButton_Click(sender As Object, e As EventArgs) Handles CancelButton.Click
        requestCancel = True
        DownloadButton.Enabled = False
        Disconnect()
        EnableConnectInputs(True)
    End Sub


    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    '                                                         REQUEST PICTURE FROM CAMERA
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Async Sub RequestPicture(picIndex As Integer)
        Dim requestRetry As Integer = 0
        Dim noError As Boolean
        While requestRetry <= 4
            requestRetry += 1
            noError = True
            Try
                Dim cameraHandler As New HttpClientHandler With {.Credentials = cameraCredentials}
                Dim cameraClient As New HttpClient(cameraHandler) With {.Timeout = TimeSpan.FromSeconds(30)}
                Dim cameraRequest As New HttpRequestMessage With {
                .Method = HttpMethod.Get,
                .RequestUri = New Uri("http://" + IpInfo.Text + "/ISAPI/contentMgmt/download"),
                .Content = New StringContent("<?xml version='1.0'?><downloadRequest><playbackURI>rtsp:/" +
                                       PicAddress(picIndex) + "/Streaming/tracks/120?starttime=" +
                                       HikDate(PicDTime(picIndex).AddDays(-UtcOffset.Value)) + "&amp;endtime=" +
                                       HikDate(PicDTime(picIndex).AddDays(-UtcOffset.Value)) + "&amp;name=" +
                                       PicName(picIndex) + "&amp;size=" +
                                       PicSize(picIndex) + "</playbackURI></downloadRequest>", Encoding.UTF8)
                }
                Dim cameraResponse = cameraClient.Send(cameraRequest)
                cameraResponse.EnsureSuccessStatusCode()
                Dim cameraByteStream = Await cameraResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(True)
                Dim cameraMemoryStream = New MemoryStream(cameraByteStream)

                cameraImage = Drawing.Image.FromStream(cameraMemoryStream)

            Catch exceptionError As Exception
                If requestRetry = 4 Then
                    MsgBox(String.Format("Error: {0}", exceptionError.Message), vbDefaultButton2, "Error Requesting Picture")
                    Return
                Else
                    noError = False
                    My.Application.DoEvents()
                    Threading.Thread.Sleep(15000)
                End If
            End Try
            If noError Then Return
        End While
    End Sub

    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    '                                                              FUNCTIONS
    '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
    Private Sub EnableConnectInputs(TurnOn As Boolean)
        ConnectButton.Enabled = TurnOn
        CancelButton.Enabled = Not TurnOn
        Username.Enabled = TurnOn
        Password.Enabled = TurnOn
        IpInfo.Enabled = TurnOn
        DownloadFolder.Enabled = TurnOn
        UtcOffset.Enabled = TurnOn
        If NewCount.Value = 0 Then
            PreviewNewCheckBox.Enabled = False
            PreviewNewCheckBox.Checked = False
        End If
        If TurnOn Then
            Me.AcceptButton = ConnectButton
        Else
            Me.AcceptButton = DownloadButton
        End If
    End Sub

    Private Sub Disconnect()
        CameraName.Text = "DISCONNECTED"
        CameraName.BackColor = Color.Firebrick
        CameraMainPicture.Image = Nothing
        CameraTimePicture.Image = Nothing
        ProgressBar.Value = 0
        picIndex = 0
        searchFrom = New DateTime(2020, 1, 1, 0, 0, 0, 0)
        OldStart.Text = ""
        OldEnd.Text = ""
        OldCount.Value = 0
        NewStart.Text = ""
        NewEnd.Text = ""
        NewCount.Value = 0
        PreviewOldCheckBox.Enabled = False
        PreviewOldCheckBox.Checked = False
        SkipPrevious.Enabled = True
        SkipPrevious.Checked = True
        newStartNum = 0
        newEndNum = 0
    End Sub

    Private Sub SkipPrevious_CheckedChanged(sender As Object, e As EventArgs) Handles SkipPrevious.CheckedChanged
        If SkipPrevious.Checked Then
            PreviewOldCheckBox.Enabled = False
            PreviewOldCheckBox.Checked = False
        Else
            PreviewOldCheckBox.Enabled = True
            PreviewOldCheckBox.Checked = True
        End If
    End Sub

    Private Function OkToConnect() As Boolean
        If Username.Text = "" Then
            MsgBox("Enter Username to connect", vbDefaultButton2, "Username missing")
            Return False
        End If
        If Password.Text = "" Then
            MsgBox("Enter Password to connect", vbDefaultButton2, "Password missing")
            Return False
        End If
        If IpInfo.Text = "" Then
            MsgBox("Enter IP Address:Port to connect", vbDefaultButton2, "IP Address:Port missing")
            Return False
        End If
        If DownloadFolder.Text = "" Then
            MsgBox("Select Download Folder to connect", vbDefaultButton2, "Download Folder missing")
            Return False
        End If
        Return True
    End Function

    Private Sub UpdateIniFile()
        Dim iniFileText As String = Username.Text + Environment.NewLine + DownloadFolder.Text
        For x = 0 To IpInfo.Items.Count - 1
            iniFileText += Environment.NewLine + IpInfo.Items.Item(x).ToString
        Next
        File.WriteAllText("HikCameraPictureDownload.ini", iniFileText)
    End Sub

    Private Sub DownloadFolder_Click(sender As Object, e As EventArgs) Handles DownloadFolder.Click
        FolderBrowser.InitialDirectory = DownloadFolder.Text
        FolderBrowser.ShowDialog()
        DownloadFolder.Text = FolderBrowser.SelectedPath
        Disconnect()
        UpdateIniFile()
    End Sub

    Private Sub Username_Leave(sender As Object, e As EventArgs) Handles Username.Leave
        cameraCredentials.UserName = Username.Text
        Disconnect()
        UpdateIniFile()
    End Sub

    Private Sub Password_Leave(sender As Object, e As EventArgs) Handles Password.Leave
        cameraCredentials.Password = Password.Text
        Disconnect()
    End Sub

    Private Sub IpInfo_SelectedValueChanged(sender As Object, e As EventArgs) Handles IpInfo.SelectedValueChanged
        Disconnect()
    End Sub

    Private Sub GmtOffset_ValueChanged(sender As Object, e As EventArgs) Handles UtcOffset.ValueChanged
        Disconnect()
    End Sub

    Private Function VerifyCameraName(cameraName As String) As String
        cameraName = Replace(cameraName, " ", "_")
        cameraName = Replace(cameraName, "\", "_")
        cameraName = Replace(cameraName, "/", "_")
        cameraName = Replace(cameraName, ":", "_")
        cameraName = Replace(cameraName, "*", "_")
        cameraName = Replace(cameraName, "?", "_")
        cameraName = Replace(cameraName, "<", "_")
        cameraName = Replace(cameraName, ">", "_")
        cameraName = Replace(cameraName, "!", "_")
        Return cameraName
    End Function

    Private Function HikDate(normalDate As DateTime) As String  ' takes datetime returns "yyyy-mm-ddThh:mm:ssZ"
        HikDate = normalDate.ToString("yyyy-MM-dd") + "T" + normalDate.ToString("HH:mm:ss") + "Z"
    End Function

    Private Function StrDateToDT(dateStr As String) As DateTime ' takes "yyyymmddhhmmss" returns datetime
        dateStr = Mid(dateStr, 1, 4) + "/" + Mid(dateStr, 5, 2) + "/" + Mid(dateStr, 7, 2) + " " +
                  Mid(dateStr, 9, 2) + ":" + Mid(dateStr, 11, 2) + ":" + Mid(dateStr, 13, 2)
        StrDateToDT = Convert.ToDateTime(dateStr)
    End Function

    Private Function StrDate(aDate As DateTime) As String       ' takes datetime returns "yyyyMMddHHmmss"
        StrDate = aDate.ToString("yyyyMMddHHmmss")
    End Function

    Private Sub LoadPicture(cameraImage As Image, mainAndTime As Boolean)
        Dim cameraBitmap As System.Drawing.Bitmap
        Dim cameraGraphics As Graphics
        If mainAndTime Then
            CameraMainPicture.Image = cameraImage
        End If
        cameraBitmap = CType(cameraImage, Bitmap)
        CameraTimePicture.Image = New Bitmap(950, 55)
        cameraGraphics = Graphics.FromImage(CameraTimePicture.Image)
        cameraGraphics.DrawImage(image:=cameraBitmap,
                                 destRect:=New Rectangle(x:=0, y:=0, width:=950, height:=55),
                                 srcRect:=New Rectangle(x:=0, y:=0, width:=950, height:=55),
                                 srcUnit:=GraphicsUnit.Pixel)
    End Sub

End Class
 
When you get older the simplest things become the hardest... I can't seem to attach a zip file.
 
Hoping for some suggestions on what I'm doing wrong.

I've set up the HikPictureDownload utility as described but I keep getting a prompt saying there are no picture to download.

I've checked and have snapshots saved on my Cams internal SDcard. The IP address used provides direct connection to the camera although it sits behind an NVR. I can access the camera web interface from the same IP address.

I suspect there is a setting/permission in the camera settings that is preventing the utility from getting access to the snapshots but not sure what.

1733278215545.png
 
Hi @venturis ...

The IP address used provides direct connection to the camera although it sits behind an NVR. I can access the camera web interface from the same IP address.

I have some LAN connected cameras that I access with the direct IP address and the utility works, but...

I can't access my NVR connected cameras using a direct IP address so I can't try that... have you tried connecting to the camera using the virtual host IP address for the camera ( NVR_IP_address : virtual_camera_number ... like 192.168.100.100:65004 )?
 
Hi @venturis ...



I have some LAN connected cameras that I access with the direct IP address and the utility works, but...

I can't access my NVR connected cameras using a direct IP address so I can't try that... have you tried connecting to the camera using the virtual host IP address for the camera ( NVR_IP_address : virtual_camera_number ... like 192.168.100.100:65004 )?

After confirming I could reach the camera from my desktop PC via the NVR virtual host IP on port 65001 I also tried using these settings in the app however, exactly the same result as the screenshot provided earlier.

1733442238877.png
 
Here is a screenshot from the "Picture Download" menu on one of my cameras:

Screenshot 2024-12-05 190219.png

Does yours look similar? I notice the "File Type" for pictures on mine is continuous... I think the download utility would download any "File Type", but I have not tried myself... if you are trying to download other types such as "Motion" or "Line Crossing" let me know and I will test here to confirm those download...

The other think I can think of... I have not tested the utility on cameras running the newest G5 firmware V5.7.18 released 240826... is this the firmware you are using? ... if so, I will put an SD card in one of the cameras I have running that firmware and see if that is the issue.
 
Here is a screenshot from the "Picture Download" menu on one of my cameras:

View attachment 208907

Does yours look similar? I notice the "File Type" for pictures on mine is continuous... I think the download utility would download any "File Type", but I have not tried myself... if you are trying to download other types such as "Motion" or "Line Crossing" let me know and I will test here to confirm those download...

The other think I can think of... I have not tested the utility on cameras running the newest G5 firmware V5.7.18 released 240826... is this the firmware you are using? ... if so, I will put an SD card in one of the cameras I have running that firmware and see if that is the issue.

The file type is "Motion Detect" in my case. See below.

I'm also still on Firmware Version 5.7.15.

Thanks for your help.

1733445399024.png
 
  • Like
Reactions: johnfitz
The file type is "Motion Detect" in my case.

OK, that may be the problem... I will see if the utility is limiting the files it selects to "Continuous", and if it is, I will update the utility... it might be a couple of days until I have a chance to do this...

In the meantime, to help confirm this is the issue, could you set your camera to temporarily capture a few "Timed Snapshots", and see if the utility downloads those:

Screenshot 2024-12-05 200727.png
 
I've set both timed image capture and motion detect capture as per the image below. I reformat the SDcard to be sure there was nothing corrupting the images.

Note that the timed image capture file names are suffixed with "_timingCap" which differs to the example you provided earlier although as described below it doesn't see to be an issue.

Upon running the app I'm able to successfully download the timed captures but the motion detect captures are ignored.


1733448468520.png1733448923796.png
 
  • Like
Reactions: johnfitz
Yes, I just did the same thing here and got the same results as you... I will take a quick look now and see if I can fix it...
 
I see I have the search condition picture file type hardcoded... it's set to "Continuous" (CMR) in this line of code:

"<metadataList><metadataDescriptor>/recordType.meta.std-cgi.com/CMR" +

If I changed "CMR" to "allPic" then all picture types would download... that's easy, BUT...

Better, I could add another input drop down selector to the utility to allow the user to select the picture file types to download (All Types, Continuous, Motion, Line Crossing, etc.) ... that would be pretty straightforward also, BUT...

There is an existing bug in the utility... it assumes there will be at most 1 picture file per second (otherwise the filename convention I use will cause pictures to be overwritten, or another error I'm not sure) ... so if the interval is set to less than 1000 milliseconds for continuous, or the interval/capture number settings for event triggered captures cause more than a picture a second things will go haywire...

I can fix this by somehow expanding the filename set for each picture, but it will take a bit longer... or maybe just use the filename that the web GUI sets? I didn't do that initially because the continuous picture filenames don't include anything in the way of a timestamp ... maybe the best/easiest/least prone to any further bugs would be to just use the web GUI filename preceded by YYYYMMDDhhmmss_ ... that should be easy and keep the files in chronologically order...

I don't think anyone else came across this issue because most are probably downloading timed images (at least a second apart) to create a time-lapse...

I intend to upload the VB source to GitHub so others can make changes but that's new to me... I normally don't futz with this stuff until the weather gets really cold :)

Let me know what you think, and I'll change something, so it works for you!
 
If I changed "CMR" to "allPic" then all picture types would download... that's easy, BUT...

Better, I could add another input drop down selector to the utility to allow the user to select the picture file types to download (All Types, Continuous, Motion, Line Crossing, etc.) ... that would be pretty straightforward also, BUT...

There is an existing bug in the utility... it assumes there will be at most 1 picture file per second (otherwise the filename convention I use will cause pictures to be overwritten, or another error I'm not sure) ... so if the interval is set to less than 1000 milliseconds for continuous, or the interval/capture number settings for event triggered captures cause more than a picture a second things will go haywire...

OK. If I understand correctly setting the picture download parameter to "allPic" may create problems if more than one picture exists within any one second interval if both continuous and event capture are both enabled.

There's a couple of considerations that might help reduce the amount of code modification needed.

First, neither the continuous capture nor the event capture intervals can be set at less than 1000 mS. Trying to enter a time interval of less than 1000mS throws a up an error. Hence, your current file naming scheme should continue to work so long as we are dealing with only one picture type any time.

If you add the drop down to nominate the picture type to be downloaded, and on the basis the picture capture interval cannot be less than 1 second (1000mS) for any picture type, there should never be more than one picture of any type within any one second interval.

I seems that if the app allows only one picture type to be downloaded based on the drop down selection, the rest should just work?


1733468734729.png
 
  • Like
Reactions: johnfitz
First, neither the continuous capture nor the event capture intervals can be set at less than 1000 mS.

I did not realize that, so that makes things easier...

Hence, your current file naming scheme should continue to work so long as we are dealing with only one picture type any time.

Almost, but I noticed in your screenshot (and confirmed with my own camera for both Motion and Intrusion Detection) that the first two pictures on an individual event are the same date/time:

Screenshot 2024-12-06 111857.png

I think the first smaller image is from checking "Linkage Method/Upload to FTP/Memory Card" on the event configuration, and the next larger image(s) are from setting "Enable-Event-Triggered Snapshot" in "Storage/Scheduled Settings/Capture Parameters".

There seems to be some inconsistency though... seen this link:

On hikvision camera what does the capture number mean in event photo/snapshot menu?


Overall, maybe the best thing to do is add the new dropdown to the utility and just append the memory card filename to the filename I'm currently using? In that case I would also be able to let the user select "All Types"... the filenames would be long but still be chronological.
 
  • Like
Reactions: venturis
Overall, maybe the best thing to do is add the new dropdown to the utility and just append the memory card filename to the filename I'm currently using? In that case I would also be able to let the user select "All Types"... the filenames would be long but still be chronological.

I assumed that if the requests to the camera were filtered by event "type” that it wouldn't matter if there was two or more snapshots with the exactly the same timestamp. I guessed the camera would only return the snapshots for the chosen event type.

Personally, I'm eternally grateful when someone such as yourself takes the time to develop software for the benefit of others.

Looking forward to the next release. :)
 
Last edited:
  • Like
Reactions: johnfitz
Here's version 2.0 ...

Screenshot 2024-12-10 202906.png


EDIT: This version had a memory leak bug... It has been fixed in latest version (further down the thread) ...

 
Last edited:
  • Like
Reactions: venturis
I'm genuinely pleased with the new Version 2.0.

I very much like the organization of the different snapshot types into separate folders and the addition of the "Play" button is an unexpected bonus.

On a quick testing I've just carried out the app seems to work flawlessly pulling down both "Motion", "Continuous" and "All Types" exactly as expected. Having different "type" snapshots with the same timestamp doesn't appear to be an issue.

I also noticed my computer's consumed memory increased by around 14GB and continued to increase by around 1GB per 30 seconds whilst downloading the images from the camera. The memory was only released once I closed down the app. That may or may not be expected behavior given the number of images your dealing with. Around 1300 images were downloaded.

I note the downloaded file names are extremely long. As a suggestion for the next release (if there is one) you might consider dropping the appended camera name from the picture file names since they are already conveniently located in a folder with the camera name. In my view you really only need the last few digits which represent the time/date to keep the downloaded files in order. But, there may be other reasons for wanting to keep the entire filename as is.

Having the ability to download snapshots so easily is a massive benefit. Particularly when it comes to event trigged snapshots since the picture files are full-frame captures, unlike images extracted from video, therefore do not suffer from loss of clarity due to quantization noise and motion blur. The captured images are very clear, even for moving objects. Details such as person's face or car number plate much clearer than images extracted from the video playback on the NVR.

This is likely to become my favorite Hikvision related app! Thanks and well done.
 
  • Like
Reactions: johnfitz
I'll look into the memory leak issue you mentioned... you can see from my screenshot above, I loaded just over 100,000 images (in one go, several hours) and did not see this... but those files are in the 400 KB range... what size are the files you were downloading?

Next step I will find a way to shorten and standardize the file naming... first on my list is just what you suggested, there's no need for the camera name since the folder identifies that... and the duplicated channel number, long sequence number, duplicated timestamp... should be easy to fix/standardize.

Did you notice how the scroll bar works during playback/paused playback?

Thanks for the feedback!