Security Camera ONVIF Capabilities

The ONVIF specification standardises the network interface (the network layer) of network video products. It defines a communication framework based on relevant IETF and Web Services standards including security and IP configuration requirements.

After discovering a device the next step was to query it to determine its capabilities. I had some issues with .Net Core 5 application configuring the Windows Communication Foundation(WCF) to use Digest authentication (RFC2617) credentials on all bar the device management service client.

This .Net Core 5 console application queries the device management service (ONVID application programmers guide) to get the capabilities of the device then calls the media, imaging and pan tilt zoom services and displays the results.

I generated the client services using the Microsoft WCF Web Service Reference Provider.

Connected Services management dialog

The Uniform Resource Locators(URL) and namespace prefixes for each generated service are configured in the ConnectedService.json file.

First step configuring a WCF Service

Initially I used a devMobile.IoT.SecurityCameraClient prefix but after some experimentation changed to OnvifServices.

Second step configuring a WCF Service

For testing I selected “Generated Synchronous Operations” as they are easier to use in a console application while exploring the available functionality.

Third step configuring a WCF Service

The WSDL generated a number of warnings so I inspected the WSDL to see if the were easy to fix. I did consider copying the WSDL to my development box but it didn’t appear to be worth the effort.

SVCUtil warning messages about invalid Onvif WSDL

For this application I’m using the CommandLineParser NuGet package to parse and validate the client, username and password configured in the debugger tab.

Required Nuget packages
private static async Task ApplicationCore(CommandLineOptions options)
{
   Device deviceClient;
   ImagingPortClient imagingPortClient;
   MediaClient mediaClient;
   PTZClient panTiltZoomClient;

   var messageElement = new TextMessageEncodingBindingElement()
   {
      MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None),
      WriteEncoding = Encoding.UTF8
    };

    HttpTransportBindingElement httpTransportNoPassword = new HttpTransportBindingElement();
    CustomBinding bindingHttpNoPassword = new CustomBinding(messageElement, httpTransportNoPassword);
         
    HttpTransportBindingElement httpTransport = new HttpTransportBindingElement()
    {
       AuthenticationScheme = AuthenticationSchemes.Digest
    };
    CustomBinding bindingHttpPassword = new CustomBinding(messageElement, httpTransport);

    try
    {
       // Setup the imaging porting binding, use TLS, and ignore certificate errors
       deviceClient = new DeviceClient(bindingHttpNoPassword, new EndpointAddress($"http://{options.CameraUrl}/onvif/devicemgmt"));

       GetCapabilitiesResponse capabilitiesResponse = await deviceClient.GetCapabilitiesAsync(new GetCapabilitiesRequest(new CapabilityCategory[] { CapabilityCategory.All }));

       Console.WriteLine("Device capabilities");
       Console.WriteLine($"  Device: {capabilitiesResponse.Capabilities.Device.XAddr}");
       Console.WriteLine($"  Events: {capabilitiesResponse.Capabilities.Events.XAddr}"); // Not interested in events for V1
       Console.WriteLine($"  Imaging: {capabilitiesResponse.Capabilities.Imaging.XAddr}");
       Console.WriteLine($"  Media: {capabilitiesResponse.Capabilities.Media.XAddr}");
       Console.WriteLine($"  Pan Tilt Zoom: {capabilitiesResponse.Capabilities.PTZ.XAddr}");
       Console.WriteLine();
       ...
       Console.WriteLine($"Video Source Configuration");
       foreach (OnvifServices.Media.VideoSourceConfiguration videoSourceConfiguration in videoSourceConfigurations.Configurations)
      {
         Console.WriteLine($" Name: {videoSourceConfiguration.Name}");
         Console.WriteLine($" Token: {videoSourceConfiguration.token}");
         Console.WriteLine($" UseCount: {videoSourceConfiguration.UseCount}");
         Console.WriteLine($" Bounds: {videoSourceConfiguration.Bounds.x}:{videoSourceConfiguration.Bounds.y} {videoSourceConfiguration.Bounds.width}:{videoSourceConfiguration.Bounds.height}");
         Console.WriteLine($" View mode: {videoSourceConfiguration.ViewMode}");
      }
   }
   catch (Exception ex)
   {
      Console.WriteLine(ex.Message);
   }

   Console.WriteLine();
   Console.WriteLine("Press <enter> to exit");
   Console.ReadLine();
}

I had to do a bit of “null checking” as often if a feature wasn’t supported the root node was null. I need to get a selection of cameras (especially one with pan/tilt/zoom) to check that I’m processing the responses from the device correctly.

Console application output showing capabilities of Uniview device

After confirming the program was working on my development box I used the excellent RaspberryDebugger to download the application and run it on a Raspberry PI 3 running the Raspberry PI OS.

Security Camera ONVIF Discovery

The ONVIF specification standardises the network interface (the network layer) of network video products. It defines a communication framework based on relevant IETF and Web Services standards including security and IP configuration requirements. ONVIF uses Web Services Dynamic Discovery (WS-Discovery) to locate devices on the local network which operates over UDP port 3702 and uses IP multicast address 239.255.255.250.

The first issue was that WS-Discovery is not currently supported by the .Net Core Windows Communication Foundation(WCF) implementation CoreWCF(2021-08). So I built a proof of concept(PoC) client which used UDP to send and receive XML messages (WS-Discovery specification) to “probe” the local network.

My .Net Core 5 console application enumerates the host device’s network interfaces, then sends a “probe” message and waits for responses. The ONVID application programmers guide specifies the format of the “probe” request and response messages (One of the namespace prefixes in the sample is wrong). The client device can return its name and details of it’s capabilities in the response. Currently I only need the IP addresses of the cameras but if more information was required I would use the XML Serialisation functionality of .Net Core to generate the requests and unpack the responses.

class Program
{
	// From https://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf & http://www.onvif.org/wp-content/uploads/2016/12/ONVIF_WG-APG-Application_Programmers_Guide-1.pdf
	const string WSDiscoveryProbeMessages =
		"<?xml version = \"1.0\" encoding=\"UTF-8\"?>" +
		"<e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" " +
			"xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" " +
			"xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" " +
			"xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\"> " +
				"<e:Header>" +
					"<w:MessageID>uuid:{0}</w:MessageID>" +
					"<w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To> " +
					"<w:Action mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action> " +
				"</e:Header> " +
				"<e:Body> " +
					"<d:Probe> " +
						"<d:Types>dn:NetworkVideoTransmitter</d:Types>" +
					"</d:Probe> " +
				"</e:Body> " +
		"</e:Envelope>";

	static async Task Main(string[] args)
	{
		List<UdpClient> udpClients = new List<UdpClient>();

		foreach (var networkInterface in NetworkInterface.GetAllNetworkInterfaces())
		{
			Console.WriteLine($"Name {networkInterface.Name}");
			foreach (var unicastAddress in networkInterface.GetIPProperties().UnicastAddresses)
			{
				if (unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork)
				{
					var udpClient = new UdpClient(new IPEndPoint(unicastAddress.Address, 0)) { EnableBroadcast = true };

					udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 5000);

					udpClients.Add(udpClient);
				}
			}
		}

	var multicastEndpoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 3702);

		foreach (UdpClient udpClient in udpClients)
		{
			byte[] message = UTF8Encoding.UTF8.GetBytes(string.Format(WSDiscoveryProbeMessages, Guid.NewGuid().ToString()));

			try
			{
				await udpClient.SendAsync(message, message.Length, multicastEndpoint);

				IPEndPoint remoteEndPoint = null;

				while(true)
				{				
					message = udpClient.Receive(ref remoteEndPoint);

					Console.WriteLine($"IPAddress {remoteEndPoint.Address}");
					Console.WriteLine(UTF8Encoding.UTF8.GetString(message));

					Console.WriteLine();
				}
			}
			catch (SocketException sex)
			{
				Console.WriteLine($"Probe failed {sex.Message}");
			}
		}

		Console.WriteLine("Press enter to <exit>");
		Console.ReadKey();
	}
}

After confirming the program was working I used the excellent RaspberryDebugger to download the application and debug it on a Raspberry PI 3 running the Raspberry PI OS.