Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ONVIF PTZ Control in C#

0.00/5 (No votes)
20 Jun 2017 1  
A C# class to implement a Pan and Tilt controller using ONVIF

Introduction

Some time ago, I developed an application to interface to an IP camera but now I need to drop in pan and tilt control. The camera came with its own SDK but it's only C++ and seems fiendishly complicated; in any case I didn't want my code to be tied to a specific camera or manufacturer. (The camera also supports zoom but it's only digital so I didn't bother to implement that. It would be easy to add.)

Background

The camera supports ONVIF so that seemed the obvious choice for a generic control. I could find no useful source code; almost all searches ended up at the web site for a particular SDK that is apparently quite expensive. I needed to be able to develop and maintain my own code so I collected bits of information from many different sources and started experimenting. The code below represents a working implementation. I'm not entirely happy with it but I ran out of time to improve it before it had to be installed on a test rig.

Using the Code

To use the code, you need to add two service references:

  • Right-click the "References" folder in Visual Studio
  • Choose "Add Service Reference..."
  • Type or paste that address into the Address control
  • Type or paste the namespace into the Namespace control

The service references are:

AddressNamespace
http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdlOnvifMedia10
http://onvif.org/onvif/ver20/ptz/wsdl/ptz.wsdlOnvifPTZService

The code only provides the communication interface to the camera. It exposes public functions and properties to be used by the UI. That means you can drop in the same class for WPF, Windows Forms or whatever else you are using.

Here's the Drop-in Class

namespace PTZController
    {
    using System;
    using System.Net;
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using System.Timers;
    using PTZController.OnvifMedia10;
    using PTZController.OnvifPTZService;

    public class Controller
        {
        private enum Direction { None, Up, Down, Left, Right };

        MediaClient mediaClient;
        PTZClient ptzClient;
        Profile profile;
        OnvifPTZService.PTZSpeed velocity;
        PTZVector vector;
        PTZConfigurationOptions options;
        bool relative = false;
        bool initialised = false;
        Timer timer;
        Direction direction;
        float panDistance;
        float tiltDistance;

        public string ErrorMessage { get; private set; }

        public bool Initialised { get { return initialised; } }

        public int PanIncrements { get; set; } = 20;

        public int TiltIncrements { get; set; } = 20;

        public double TimerInterval { get; set; } = 1500;

        public Controller(bool relative = false)
            {
            this.relative = relative;
            }

        public bool Initialise(string cameraAddress, string userName, string password)
            {
            bool result = false;

            try
                {
                var messageElement = new TextMessageEncodingBindingElement()
                    {
                    MessageVersion = MessageVersion.CreateVersion(
                      EnvelopeVersion.Soap12, AddressingVersion.None)
                    };
                HttpTransportBindingElement httpBinding = new HttpTransportBindingElement()
                    {
                    AuthenticationScheme = AuthenticationSchemes.Digest
                    };
                CustomBinding bind = new CustomBinding(messageElement, httpBinding);
                mediaClient = new MediaClient(bind,
                  new EndpointAddress($"http://{cameraAddress}/onvif/Media"));
                mediaClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel =
                  System.Security.Principal.TokenImpersonationLevel.Impersonation;
                mediaClient.ClientCredentials.HttpDigest.ClientCredential.UserName = userName;
                mediaClient.ClientCredentials.HttpDigest.ClientCredential.Password = password;
                ptzClient = new PTZClient(bind,
                  new EndpointAddress($"http://{cameraAddress}/onvif/PTZ"));
                ptzClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel =
                  System.Security.Principal.TokenImpersonationLevel.Impersonation;
                ptzClient.ClientCredentials.HttpDigest.ClientCredential.UserName = userName;
                ptzClient.ClientCredentials.HttpDigest.ClientCredential.Password = password;

                var profs = mediaClient.GetProfiles();
                profile = mediaClient.GetProfile(profs[0].token);

                var configs = ptzClient.GetConfigurations();

                options = ptzClient.GetConfigurationOptions(configs[0].token);

                velocity = new OnvifPTZService.PTZSpeed()
                    {
                    PanTilt = new OnvifPTZService.Vector2D()
                        {
                        x = 0,
                        y = 0,
                        space = options.Spaces.ContinuousPanTiltVelocitySpace[0].URI,
                        },
                    Zoom = new OnvifPTZService.Vector1D()
                        {
                        x = 0,
                        space = options.Spaces.ContinuousZoomVelocitySpace[0].URI,
                        }
                    };
                if (relative)
                    {
                    timer = new Timer(TimerInterval);
                    timer.Elapsed += Timer_Elapsed;
                    velocity.PanTilt.space = options.Spaces.RelativePanTiltTranslationSpace[0].URI;
                    panDistance = (options.Spaces.RelativePanTiltTranslationSpace[0].XRange.Max -
                      options.Spaces.RelativePanTiltTranslationSpace[0].XRange.Min) / PanIncrements;
                    tiltDistance = (options.Spaces.RelativePanTiltTranslationSpace[0].YRange.Max -
                      options.Spaces.RelativePanTiltTranslationSpace[0].YRange.Min) / TiltIncrements;
                    }

                vector = new PTZVector()
                {
                PanTilt = new OnvifPTZService.Vector2D()
                    {
                    x = 0, y = 0, space = options.Spaces.RelativePanTiltTranslationSpace[0].URI
                    }
                };

                ErrorMessage = "";
                result = initialised = true;
                }
            catch (Exception ex)
                {
                ErrorMessage = ex.Message;
                }
            return result;
            }

        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
            {
            Move();
            }

        public void PanLeft()
            {
            if (initialised)
                {
                if (relative)
                    {
                    direction = Direction.Left;
                    Move();
                    }
                else
                    {
                    velocity.PanTilt.x = options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Min;
                    velocity.PanTilt.y = 0;
                    ptzClient.ContinuousMoveAsync(profile.token, velocity, "PT10S");
                    }
                }
            }

        public void PanRight()
            {
            if (initialised)
                {
                if (relative)
                    {
                    direction = Direction.Right;
                    Move();
                    }
                else
                    {
                    velocity.PanTilt.x = options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Max;
                    velocity.PanTilt.y = 0;
                    ptzClient.ContinuousMoveAsync(profile.token, velocity, "PT10S");
                    }
                }
            }

        public void TiltUp()
            {
            if (initialised)
                {
                if (relative)
                    {
                    direction = Direction.Up;
                    Move();
                    }
                else
                    {
                    velocity.PanTilt.x = 0;
                    velocity.PanTilt.y = options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Max;
                    ptzClient.ContinuousMoveAsync(profile.token, velocity, "PT10S");
                    }
                }
            }

        public void TiltDown()
            {
            if (initialised)
                {
                if (relative)
                    {
                    direction = Direction.Down;
                    Move();
                    }
                else
                    {
                    velocity.PanTilt.x = 0;
                    velocity.PanTilt.y = options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Min;
                    ptzClient.ContinuousMoveAsync(profile.token, velocity, "PT10S");
                    }
                }
            }

        public void Stop()
            {
            if (initialised)
                {
                if (relative)
                    timer.Enabled = false;
                direction = Direction.None;
                ptzClient.Stop(profile.token, true, true);
                }
            }

        private void Move()
            {
            bool move = true;

            switch (direction)
                {
                case Direction.Up:
                    velocity.PanTilt.x = 0;
                    velocity.PanTilt.y = options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Max;
                    vector.PanTilt.x = 0;
                    vector.PanTilt.y = tiltDistance;
                    break;

                case Direction.Down:
                    velocity.PanTilt.x = 0;
                    velocity.PanTilt.y = options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Max;
                    vector.PanTilt.x = 0;
                    vector.PanTilt.y = -tiltDistance;
                    break;

                case Direction.Left:
                    velocity.PanTilt.x = options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Max;
                    velocity.PanTilt.y = 0;
                    vector.PanTilt.x = -panDistance;
                    vector.PanTilt.y = 0;
                    break;

                case Direction.Right:
                    velocity.PanTilt.x = options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Max;
                    velocity.PanTilt.y = 0;
                    vector.PanTilt.x = panDistance;
                    vector.PanTilt.y = 0;
                    break;

                case Direction.None:
                default:
                    move = false;
                    break;
                }
            if (move)
                {
                ptzClient.RelativeMove(profile.token, vector, velocity);
                }
            timer.Enabled = true;
            }
        }
    }

To use it from the UI create and instance, set any properties where you want something different from the default, and then call its Initialise function. Proceed to call the other member functions as necessary, in response to user input. For example:

using PTZController;

// ...

  Controller controller;

// ...
  controller = new Controller(Properties.Settings.Default.PTZRelative);
  controller.Initialise(Properties.Settings.Default.CameraAddress, "UserName", "password");
// ...

// in a button down handler
  controller.PanLeft();

// in a button up handler
  controller.Stop();

Points of Interest

  1. Since I could find no clear documentation anywhere on the specific use of ONVIF, the code was developed by experiment. One conclusion I reached (rightly or wrongly) was that, even though I'm using the ver20 version of the ptz interface, I still needed to use ver10 for the media interface. There's probably a way to use ver20 but it seemed much more straightforward to get the information I needed from ver10 and it worked so I stayed with it. I may revisit this if I get enough time, in case it helps with point 2.
  2. I first tried using Continuous moves but the camera response was very laggy, making the control effectively unusable. This may be something to do with the way that ONVIF is implemented in the camera I'm using; the camera response to the PTZ controls in the web app supplied with it is very good, with almost zero lag. I only spent a short time trying to identify and solve the problem before deciding that it would be better to use relative moves. This is still not very responsive but it's good enough for present needs. For that reason, the code for Continuous moves has been left not fully developed but I think it still works.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here