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:
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");
controller.PanLeft();
controller.Stop();
Points of Interest
- 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.
- 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.