1

I am trying to develop a unity application that, among other things, controls a PTZ camera remotely. I'm having trouble actually getting the connection set up though.

I am using the Onvif Unity library made by Oneiros90: https://github.com/Oneiros90/UnityOnvif

When I go into the test scene in Unity-OnvifMain > core > test > scenes (which only has an empty game object and the test script) and hit play, the script initializes all clients without errors but upon reaching the mediaClient.getProfiles() line, it throws an error "WebException: There was an error on processing web request: Status code 400(BadRequest): Bad Request"

Here is the relevant test script code for reference:

using System.Linq;
using Onvif.Core;
using UnityEngine;


public class OnvifClientTest : MonoBehaviour
{
    [SerializeField]
    private string host = "";

    [SerializeField]
    private string username = "admin";

    [SerializeField]
    private string password = "";

    [SerializeField]
    private string ptzServiceAddress = "";

    [SerializeField]
    private string mediaServiceAddress = "";


    private Vector2 ptzPanSpeedLimits;
    private Vector2 ptzTiltSpeedLimits;
    private Vector2 ptzZoomSpeedLimits;


    private OnvifClient onvif;
    private string currentProfile;
    private PTZClient ptzClient;
    private MediaClient mediaClient;

    private async void Start()
    {
        onvif = new OnvifClient(host, username, password);
        var success = await onvif.Init();
        if (!success)
        {
            Debug.LogError($"Cannot connect to {host}");
            return;
        }

        ptzClient = await onvif.CreatePTZClient(ptzServiceAddress);
        mediaClient = await onvif.CreateMediaClient(mediaServiceAddress);

        var profiles = mediaClient.GetProfiles();
        currentProfile = profiles.First().token;

        var ptzConf = mediaClient.GetProfile(currentProfile).PTZConfiguration;
        Space2DDescription panTiltSpace = null;
        Space1DDescription zoomSpace = null;

        //other stuff that does not matter yet because it doesn't get this far
    }
}

And the OnvifClient code also:

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading.Tasks;
using Onvif.Security;
using UnityEngine;

namespace Onvif.Core
{

    public class OnvifClient
    {
        private DeviceClient device;
        private Binding binding;
        private SoapSecurityHeaderBehavior securityHeader;

        public Uri OnvifUri { get; private set; }
        public string Host { get; private set; }
        public string Username { get; private set; }
        public string Password { get; private set; }
        public Capabilities Capabilities { get; private set; }

        public OnvifClient(string host, string username, string password)
        {
            Host = host;
            OnvifUri = new Uri(host); //($"http://{host}/onvif/device_service");
            Username = username;
            Password = password;
        }

        public async Task<bool> Init()
        {
            try
            {
                //Have to set http continue to false since the server is not expecting it: https://forum.unity.com/threads/http-error-417-expectation-failed-unitywebrequest.651256/
                System.Net.ServicePointManager.Expect100Continue = false;

                binding = CreateBinding();
                var endpoint = new EndpointAddress(OnvifUri);
                device = new DeviceClient(binding, endpoint);
                var timeShift = await GetDeviceTimeShift(device);
                securityHeader = new SoapSecurityHeaderBehavior(Username, Password, timeShift);

                device = new DeviceClient(binding, endpoint);
                device.ChannelFactory.Endpoint.EndpointBehaviors.Clear();
                device.ChannelFactory.Endpoint.EndpointBehaviors.Add(securityHeader);
                device.Open();

                Capabilities = device.GetCapabilities(new CapabilityCategory[] { CapabilityCategory.All });
                return true;
            }
            catch (Exception e)
            {
                Debug.LogError(e.Message);
                return false;
            }
        }

        public void Ping()
        {
            device.GetCapabilities(new CapabilityCategory[] { CapabilityCategory.All });
        }

        public void Close()
        {
            if (device != null)
                device.Close();
        }

        public async Task<DeviceClient> GetDeviceClient()
        {
            if (device == null)
                await Init();

            return device;
        }

        //original
        public async Task<MediaClient> CreateMediaClient()
        {
            if (device == null)
                await Init();

            var media = new MediaClient(binding, new EndpointAddress(new Uri(Capabilities.Media.XAddr)));
            media.ChannelFactory.Endpoint.EndpointBehaviors.Clear();
            media.ChannelFactory.Endpoint.EndpointBehaviors.Add(securityHeader);

            return media;
        }

        //custom
        public async Task<MediaClient> CreateMediaClient(string endpoint)
        {
            if (device == null)
                await Init();

            var media = new MediaClient(binding, new EndpointAddress(new Uri(endpoint)));
            media.ChannelFactory.Endpoint.EndpointBehaviors.Clear();
            media.ChannelFactory.Endpoint.EndpointBehaviors.Add(securityHeader);

            return media;
        }

        //original
        public async Task<PTZClient> CreatePTZClient()
        {
            if (device == null)
                await Init();

            var ptz = new PTZClient(binding, new EndpointAddress(new Uri(Capabilities.PTZ.XAddr)));
            ptz.ChannelFactory.Endpoint.EndpointBehaviors.Clear();
            ptz.ChannelFactory.Endpoint.EndpointBehaviors.Add(securityHeader);

            return ptz;
        }

        //custom
        public async Task<PTZClient> CreatePTZClient(string endpoint)
        {
            if (device == null)
                await Init();

            var ptz = new PTZClient(binding, new EndpointAddress(new Uri(endpoint)));
            ptz.ChannelFactory.Endpoint.EndpointBehaviors.Clear();
            ptz.ChannelFactory.Endpoint.EndpointBehaviors.Add(securityHeader);

            return ptz;
        }

        public async Task<ImagingPortClient> CreateImagingClient()
        {
            if (device == null)
                await Init();

            var imaging = new ImagingPortClient(binding, new EndpointAddress(new Uri(Capabilities.Imaging.XAddr)));
            imaging.ChannelFactory.Endpoint.EndpointBehaviors.Clear();
            imaging.ChannelFactory.Endpoint.EndpointBehaviors.Add(securityHeader);

            return imaging;
        }

        private static Binding CreateBinding()
        {
            var binding = new CustomBinding();
            var textBindingElement = new TextMessageEncodingBindingElement
            {
                MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None)
            };
            var httpBindingElement = new HttpTransportBindingElement
            {
                AllowCookies = true,
                MaxBufferSize = int.MaxValue,
                MaxReceivedMessageSize = int.MaxValue
            };

            binding.Elements.Add(textBindingElement);
            binding.Elements.Add(httpBindingElement);

            return binding;
        }

        private static Task<TimeSpan> GetDeviceTimeShift(DeviceClient device)
        {
            return Task.Run(() =>
            {
                var utc = device.GetSystemDateAndTime().UTCDateTime;
                var dt = new System.DateTime(utc.Date.Year, utc.Date.Month, utc.Date.Day,
                                  utc.Time.Hour, utc.Time.Minute, utc.Time.Second);
                return dt - System.DateTime.UtcNow;
            });
        }
    }
}

I'm very new to .net client connection and so forth, so I'm really not sure why it would throw a 400 error, as the username and password are both correct. I also know for a fact that connecting to a camera works as I am working off of an existing project that does it through c# (outside of unity though).

Alternatively, I have tried avoiding using the helper scripts provided by Oneiros90 and connecting to the devices using the core onfiv scripts, as per this post: C# .NET Framework 4.52 Zoom/Focus for PELCO Camera via ONVIF But when I do that, it reaches the mediaClient.getProfiles() line and instead produces an "InvalidOperationException: Use ClientCredentials to specify a user name for required HTTP Digest authentication." error, even though I am clearly setting the http digest client credentials just above. This is also how it works in the aforementioned existing project, although there isn't as much going on with the security headers and so on in that.

My Code in unity:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Onvif.Core;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;

public class PTZController_Local : MonoBehaviour
{
    public string hostAddress = "http://<secret>/onvif/device_service";
    public string mediaAddress = "http://<secret>/onvif/media_service";
    public string PTZAddress = "http://<secret>/onvif/ptz_service";
    public string username = "";
    public string password = "";


    private OnvifClient onvifClient;
    private string currentProfile;
    private PTZClient ptzClient;
    private MediaClient mediaClient;

    private DeviceClient device;
    private CustomBinding binding;

    private CustomBinding mediaBinding;

    private void Start()
    {
        ////connect to onvif device service
        //onvifClient = new OnvifClient(hostAddress, username, password);
        //var success = await onvifClient.Init();
        //if (!success)
        //{
        //    Debug.LogError($"PTZController: Cannot connect to {hostAddress}");
        //    return;
        //}
        //Debug.Log($"PTZController: Successfully connected to {hostAddress}");

        //device = await onvifClient.GetDeviceClient();

        System.Net.ServicePointManager.Expect100Continue = false;

        HttpTransportBindingElement httpTransport = new HttpTransportBindingElement();
        httpTransport.AuthenticationScheme = System.Net.AuthenticationSchemes.Digest;
        httpTransport.MaxReceivedMessageSize = Int32.MaxValue;
        httpTransport.MaxBufferSize = Int32.MaxValue;
        httpTransport.ProxyAddress = null;
        httpTransport.BypassProxyOnLocal = true;
        httpTransport.UseDefaultWebProxy = false;
        httpTransport.TransferMode = TransferMode.StreamedResponse;

        binding = new CustomBinding(new TextMessageEncodingBindingElement(MessageVersion.Soap12WSAddressing10, System.Text.Encoding.UTF8), httpTransport);
        binding.CloseTimeout = TimeSpan.FromSeconds(30.0);
        binding.OpenTimeout = TimeSpan.FromSeconds(30.0);
        binding.SendTimeout = TimeSpan.FromMinutes(10.0);
        binding.ReceiveTimeout = TimeSpan.FromMinutes(3.0);

        device = new DeviceClient(binding, new EndpointAddress(hostAddress));
        device.ClientCredentials.HttpDigest.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
        device.ClientCredentials.HttpDigest.ClientCredential.UserName = username;
        device.ClientCredentials.HttpDigest.ClientCredential.Password = password;

        var messageElement = new TextMessageEncodingBindingElement()
        {
            MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None)
        };
        HttpTransportBindingElement mediaHttpTransport = new HttpTransportBindingElement()
        {
            AuthenticationScheme = System.Net.AuthenticationSchemes.Digest
        };
        mediaBinding = new CustomBinding(messageElement, mediaHttpTransport);

        mediaClient = new MediaClient(mediaBinding, new EndpointAddress(mediaAddress));
        mediaClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
        mediaClient.ClientCredentials.HttpDigest.ClientCredential.UserName = username;
        mediaClient.ClientCredentials.HttpDigest.ClientCredential.Password = password;
      
        var profiles = mediaClient.GetProfiles();
        currentProfile = profiles.First().token;

        //other code that is never reached
    }
}

Original project code (i've tried copying this into unity exactly and get the same error about setting the http credentials):

using System.IO;
using System.IO.Pipes;
using System.Text;
using System.ServiceModel;
using OnvifPipeClient.OnvifMedia10;
using OnvifPipeClient.OnvifPTZService;
using System.Timers;
using System.Diagnostics;
using System.Threading;

namespace OnvifPipeClient
{
    public class PTZConnection
    {
        public static PTZClient ptzClient;
        private static MediaClient mediaClient;
        public static Profile profile;


        // establish connection to PTZ
        public static void OnvifLogin()
        {
            // create binding
            System.Net.ServicePointManager.Expect100Continue = false;
            BasicHttpBinding binding = new BasicHttpBinding();
            binding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Digest;

            // connect to PTZ interface
            ptzClient = new PTZClient(binding, new EndpointAddress("http://<secret>/onvif/ptz_service"));
            ptzClient.ClientCredentials.HttpDigest.ClientCredential.UserName = "secret";
            ptzClient.ClientCredentials.HttpDigest.ClientCredential.Password = "secret";
            ptzClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;

            // connect to media client
            mediaClient = new MediaClient(binding, new EndpointAddress("http://<secret>/onvif/media_service"));
            mediaClient.ClientCredentials.HttpDigest.ClientCredential.UserName = "secret";
            mediaClient.ClientCredentials.HttpDigest.ClientCredential.Password = "secret";
            mediaClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;

            // load profiles and configurations
            var profiles = mediaClient.GetProfiles();
            profile = mediaClient.GetProfile(profiles[0].token);

            //other code that is reached without issues
    }
}

The only thread of a possible explanation I could find is this github issue(https://github.com/jamidon/ONVIF-test/issues/1) saying that another onvif c# library that oneiros90 had previously tried did not work with pelco cameras for some reason (the camera I'm trying to connect to is a pelco camera).

So, yeah, sorry for the big code dumps here but it's very difficult to find resources about this. Thanks in advance for any help.

thielmlw
  • 11
  • 2

0 Answers0