Skip to content

Commit 1b52007

Browse files
committed
F Exposed additional properties from the discovery response, API changes, fixed duplicated scopes added by each response from the server
1 parent 1833e40 commit 1b52007

File tree

7 files changed

+147
-114
lines changed

7 files changed

+147
-114
lines changed

src/OnvifClient/Program.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static async Task Main(string[] args)
1818

1919
static async Task MainAsync(string[] args)
2020
{
21-
var devices = await OnvifDiscoveryClient.DiscoverAsyncWithMake();
21+
var devices = await OnvifDiscoveryClient.DiscoverAsync();
2222

2323
if (devices == null || devices.Count == 0)
2424
{
@@ -28,18 +28,18 @@ static async Task MainAsync(string[] args)
2828

2929
foreach (var onvifDevice in devices)
3030
{
31-
Console.WriteLine($"Found device: Make = {onvifDevice.Make}, Model = {onvifDevice.Model}");
31+
Console.WriteLine($"Found device: Manufacturer = {onvifDevice.Manufacturer}, Model = {onvifDevice.Hardware}");
3232
}
3333

34-
var device = devices.FirstOrDefault(x => x.Endpoint != null && x.Endpoint.Contains("localhost"));
34+
var device = devices.FirstOrDefault(x => x.Addresses.First().Contains("localhost"));
3535

3636
if (device == null)
3737
{
3838
Console.WriteLine("Please run OnvifService on the localhost as Administrator, or use a different camera URL and credentials.");
3939
}
4040
else
4141
{
42-
using (var client = new SimpleOnvifClient(device.Endpoint, "admin", "password", true))
42+
using (var client = new SimpleOnvifClient(device.Addresses.First(), "admin", "password", true))
4343
{
4444
var services = await client.GetServicesAsync(true);
4545
var cameraDateTime = await client.GetSystemDateAndTimeUtcAsync();

src/OnvifService/appsettings.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@
4646
}
4747
],
4848
"MAC": null,
49-
"Hardware": null,
49+
"Manufacturer": "Lukas Volf",
50+
"Hardware": "OnvifService",
5051
"Name": null,
51-
"City": null
52+
"City": null,
53+
"Country": null
5254
}
5355
}

src/SharpOnvifClient/OnvifDiscoveryClient.cs

Lines changed: 58 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,9 @@
1313

1414
namespace SharpOnvifClient
1515
{
16-
17-
public class OnvifDiscoveryResult
18-
{
19-
public string Endpoint { get; set; }
20-
public string Make { get; set; }
21-
public string Model { get; set; }
22-
}
23-
24-
16+
/// <summary>
17+
/// Discovers Onvif devices on the network by sending a multicast discovery request. IPv4 only.
18+
/// </summary>
2519
public static class OnvifDiscoveryClient
2620
{
2721
public const int ONVIF_BROADCAST_TIMEOUT = 4000; // 4s timeout
@@ -38,10 +32,9 @@ public static class OnvifDiscoveryClient
3832
/// <param name="broadcastPort">Broadcast port - 0 to let the OS choose any free port.</param>
3933
/// <param name="deviceType">Device type we are searching for.</param>
4034
/// <returns>A list of discovered devices.</returns>
41-
public static async Task<IList<string>> DiscoverAsync(Action<string> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
35+
public static async Task<IList<OnvifDiscoveryResult>> DiscoverAsync(Action<OnvifDiscoveryResult> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
4236
{
43-
var results = await DiscoverAllAsync(r => onDeviceDiscovered?.Invoke(r), broadcastTimeout, broadcastPort, deviceType);
44-
return results.Select(r => r.Endpoint).ToList();
37+
return await DiscoverAllAsync(onDeviceDiscovered, broadcastTimeout, broadcastPort, deviceType);
4538
}
4639

4740
/// <summary>
@@ -53,38 +46,9 @@ public static async Task<IList<string>> DiscoverAsync(Action<string> onDeviceDis
5346
/// <param name="broadcastPort">Broadcast port - 0 to let the OS choose any free port.</param>
5447
/// <param name="deviceType">Device type we are searching for.</param>
5548
/// <returns>A list of discovered devices.</returns>
56-
public static async Task<IList<string>> DiscoverAsync(string ipAddress, Action<string> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
57-
{
58-
var results = await DiscoverAllAsync(ipAddress, r => onDeviceDiscovered?.Invoke(r), broadcastTimeout, broadcastPort, deviceType);
59-
60-
return results.Select(r => r.Endpoint).ToList();
61-
}
62-
63-
/// <summary>
64-
/// Discover ONVIF devices in the local network on all network interfaces and return detailed information (endpoint, make, model).
65-
/// </summary>
66-
/// <param name="onDeviceDiscovered">Callback to be called when a new device is discovered.</param>
67-
/// <param name="broadcastTimeout">Timeout for discovery in milliseconds.</param>
68-
/// <param name="broadcastPort">Broadcast port - 0 to let the OS choose any free port.</param>
69-
/// <param name="deviceType">Device type to search for.</param>
70-
/// <returns>A list of discovered devices with endpoint, make, and model.</returns>
71-
public static async Task<IList<OnvifDiscoveryResult>> DiscoverAsyncWithMake(Action<string> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
49+
public static async Task<IList<OnvifDiscoveryResult>> DiscoverAsync(string ipAddress, Action<OnvifDiscoveryResult> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
7250
{
73-
return await DiscoverAllAsync(r => onDeviceDiscovered?.Invoke(r), broadcastTimeout, broadcastPort, deviceType);
74-
}
75-
76-
/// <summary>
77-
/// Discover ONVIF devices in the local network using a given network interface.
78-
/// </summary>
79-
/// <param name="ipAddress">IP address of the network interface to use (IP of the host computer on the NIC you want to use for discovery).</param>
80-
/// <param name="onDeviceDiscovered">Callback to be called when a new device is discovered.</param>
81-
/// <param name="broadcastTimeout"><see cref="ONVIF_BROADCAST_TIMEOUT"/>.</param>
82-
/// <param name="broadcastPort">Broadcast port - 0 to let the OS choose any free port.</param>
83-
/// <param name="deviceType">Device type we are searching for.</param>
84-
/// <returns>A list of discovered devices with make and model.</returns>
85-
public static async Task<IList<OnvifDiscoveryResult>> DiscoverAsyncWithMake(string ipAddress, Action<string> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
86-
{
87-
return await DiscoverAllAsync(ipAddress, r => onDeviceDiscovered?.Invoke(r), broadcastTimeout, broadcastPort, deviceType);
51+
return await DiscoverAllAsync(ipAddress, onDeviceDiscovered, broadcastTimeout, broadcastPort, deviceType);
8852
}
8953

9054
/// <summary>
@@ -95,7 +59,7 @@ public static async Task<IList<OnvifDiscoveryResult>> DiscoverAsyncWithMake(stri
9559
/// <param name="broadcastPort"></param>
9660
/// <param name="deviceType"></param>
9761
/// <returns>A list of discovered devices with make and model.</returns>
98-
internal static async Task<IList<OnvifDiscoveryResult>> DiscoverAllAsync(Action<string> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
62+
internal static async Task<IList<OnvifDiscoveryResult>> DiscoverAllAsync(Action<OnvifDiscoveryResult> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
9963
{
10064
NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces();
10165
List<Task<IList<OnvifDiscoveryResult>>> discoveryTasks = new List<Task<IList<OnvifDiscoveryResult>>>();
@@ -142,7 +106,7 @@ internal static async Task<IList<OnvifDiscoveryResult>> DiscoverAllAsync(Action<
142106

143107
await Task.WhenAll(discoveryTasks);
144108

145-
return discoveryTasks.Where(x => x.IsCompleted && !x.IsFaulted && !x.IsCanceled).SelectMany(x => x.Result).GroupBy(r => r.Endpoint).Select(g => g.First()).ToList();
109+
return discoveryTasks.Where(x => x.IsCompleted && !x.IsFaulted && !x.IsCanceled).SelectMany(x => x.Result).GroupBy(r => r.Addresses.FirstOrDefault()).Select(g => g.First()).ToList();
146110
}
147111

148112
/// <summary>
@@ -154,7 +118,7 @@ internal static async Task<IList<OnvifDiscoveryResult>> DiscoverAllAsync(Action<
154118
/// <param name="broadcastPort"></param>
155119
/// <param name="deviceType"></param>
156120
/// <returns>A list of discovered devices with make and model.</returns>
157-
internal static async Task<IList<OnvifDiscoveryResult>> DiscoverAllAsync(string ipAddress, Action<string> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
121+
internal static async Task<IList<OnvifDiscoveryResult>> DiscoverAllAsync(string ipAddress, Action<OnvifDiscoveryResult> onDeviceDiscovered = null, int broadcastTimeout = ONVIF_BROADCAST_TIMEOUT, int broadcastPort = 0, string deviceType = "NetworkVideoTransmitter")
158122
{
159123
if (ipAddress == null)
160124
throw new ArgumentNullException(nameof(ipAddress));
@@ -198,28 +162,26 @@ void ReceiveCallback(IAsyncResult ar)
198162
byte[] receiveBytes = client.EndReceive(ar, ref remote);
199163
string response = Encoding.UTF8.GetString(receiveBytes);
200164

201-
var endpoint = ReadOnvifEndpoint(response);
202-
var (make, model) = ReadOnvifMakeModelFromScopes(response);
165+
var parsed = ParseDiscoveryResponse(response);
203166

204-
if (!string.IsNullOrEmpty(endpoint))
167+
if (parsed.Addresses != null && parsed.Addresses.Length > 0)
205168
{
206169
lock (results)
207170
{
208-
if (endpoints.Add(endpoint))
171+
if (endpoints.Add(parsed.Addresses.First()))
209172
{
210-
var device = new OnvifDiscoveryResult { Endpoint = endpoint, Make = make, Model = model };
211-
results.Add(device);
212-
onDeviceDiscovered?.Invoke(device.Endpoint);
173+
results.Add(parsed);
174+
onDeviceDiscovered?.Invoke(parsed);
213175
}
214176
}
215177
}
216-
// Continue receiving
178+
// continue receiving
217179
if (!cts.IsCancellationRequested)
218180
client.BeginReceive(ReceiveCallback, null);
219181
}
220182
catch
221183
{
222-
// Ignore exceptions on shutdown
184+
// ignore exceptions on shutdown
223185
}
224186
}
225187

@@ -231,7 +193,7 @@ void ReceiveCallback(IAsyncResult ar)
231193
await Task.Delay(broadcastTimeout, cts.Token);
232194
cts.Cancel();
233195

234-
// Return a snapshot of the results
196+
// return a snapshot of the results
235197
lock (results)
236198
{
237199
return results.ToList();
@@ -240,75 +202,71 @@ void ReceiveCallback(IAsyncResult ar)
240202
}
241203
finally
242204
{
205+
cts.Dispose();
243206
_discoverySlim.Release();
244207
}
245208
}
246209

247-
248-
249-
private static string ReadOnvifEndpoint(string message)
210+
private static OnvifDiscoveryResult ParseDiscoveryResponse(string response)
250211
{
251-
using (var textReader = new StringReader(message))
212+
using (var textReader = new StringReader(response))
252213
{
253214
var document = new XPathDocument(textReader);
254215
var navigator = document.CreateNavigator();
255216

217+
OnvifDiscoveryResult result = new OnvifDiscoveryResult();
218+
result.Raw = response;
219+
256220
// local-name is used to ignore the namespace
221+
222+
// parse the XAddrs
257223
var node = navigator.SelectSingleNode("//*[local-name()='XAddrs']/text()");
258224
if (node != null)
259225
{
260226
string[] addresses = node.Value.Split(' ');
261-
return addresses.First();
227+
result.Addresses = addresses;
262228
}
263-
else
264-
{
265-
return null;
266-
}
267-
}
268-
}
269-
270-
private static (string Make, string Model) ReadOnvifMakeModelFromScopes(string message)
271-
{
272-
using (var textReader = new StringReader(message))
273-
{
274-
var document = new XPathDocument(textReader);
275-
var navigator = document.CreateNavigator();
276229

230+
// parse Scopes
277231
var scopesNode = navigator.SelectSingleNode("//*[local-name()='Scopes']/text()");
278232
if (scopesNode != null)
279233
{
280-
string scopes = scopesNode.Value;
281-
string make = null, model = null;
282-
foreach (var scope in scopes.Split(' '))
234+
string allScopes = scopesNode.Value;
235+
236+
string[] scopes = allScopes.Split(' ');
237+
result.Scopes = scopes;
238+
239+
try
283240
{
284-
if (scope.StartsWith("onvif://www.onvif.org/hardware/", StringComparison.OrdinalIgnoreCase))
285-
model = scope.Substring("onvif://www.onvif.org/hardware/".Length);
286-
if (scope.StartsWith("onvif://www.onvif.org/Manufacturer/", StringComparison.OrdinalIgnoreCase))
287-
make = scope.Substring("onvif://www.onvif.org/Manufacturer/".Length);
288-
if (scope.StartsWith("onvif://www.onvif.org/name/", StringComparison.OrdinalIgnoreCase))
241+
foreach (var scope in scopes)
289242
{
290-
var nameValue = scope.Substring("onvif://www.onvif.org/name/".Length);
291-
// Only use this if Manufacturer is missing
292-
if (make == null && !string.IsNullOrEmpty(nameValue))
293-
{
294-
var parts = nameValue.Split(new[] { "%20" }, StringSplitOptions.None);
295-
if (parts.Length > 0)
296-
{
297-
make = parts[0];
298-
// If model is still null, try to use the rest as model
299-
if (model == null && parts.Length > 1)
300-
model = string.Join(" ", parts.Skip(1));
301-
}
302-
}
303-
// If model is still null, fallback to the whole name value
304-
if (model == null && !string.IsNullOrEmpty(nameValue))
305-
model = Uri.UnescapeDataString(nameValue);
243+
if (scope.StartsWith(SharpOnvifCommon.Discovery.Scopes.MAC, StringComparison.OrdinalIgnoreCase))
244+
result.MAC = Uri.UnescapeDataString(scope.Substring(SharpOnvifCommon.Discovery.Scopes.MAC.Length));
245+
246+
if (scope.StartsWith(SharpOnvifCommon.Discovery.Scopes.Manufacturer, StringComparison.OrdinalIgnoreCase))
247+
result.Manufacturer = Uri.UnescapeDataString(scope.Substring(SharpOnvifCommon.Discovery.Scopes.Manufacturer.Length));
248+
249+
if (scope.StartsWith(SharpOnvifCommon.Discovery.Scopes.Hardware, StringComparison.OrdinalIgnoreCase))
250+
result.Hardware = Uri.UnescapeDataString(scope.Substring(SharpOnvifCommon.Discovery.Scopes.Hardware.Length));
251+
252+
if (scope.StartsWith(SharpOnvifCommon.Discovery.Scopes.Name, StringComparison.OrdinalIgnoreCase))
253+
result.Name = Uri.UnescapeDataString(scope.Substring(SharpOnvifCommon.Discovery.Scopes.Name.Length));
254+
255+
if (scope.StartsWith(SharpOnvifCommon.Discovery.Scopes.City, StringComparison.OrdinalIgnoreCase))
256+
result.City = Uri.UnescapeDataString(scope.Substring(SharpOnvifCommon.Discovery.Scopes.City.Length));
257+
258+
if (scope.StartsWith(SharpOnvifCommon.Discovery.Scopes.Country, StringComparison.OrdinalIgnoreCase))
259+
result.Country = Uri.UnescapeDataString(scope.Substring(SharpOnvifCommon.Discovery.Scopes.Country.Length));
306260
}
307261
}
308-
return (make, model);
262+
catch(Exception ex)
263+
{
264+
Debug.WriteLine($"{nameof(OnvifDiscoveryClient)}.{nameof(ParseDiscoveryResponse)} failed to parse scopes:\r\n{ex.Message}\r\nContinuing...");
265+
}
309266
}
267+
268+
return result;
310269
}
311-
return (null, null);
312270
}
313271
}
314272
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace SharpOnvifClient
2+
{
3+
/// <summary>
4+
/// Discovered device.
5+
/// </summary>
6+
public class OnvifDiscoveryResult
7+
{
8+
/// <summary>
9+
/// Raw SOAP message for advanced processing.
10+
/// </summary>
11+
public string Raw { get; set; }
12+
13+
/// <summary>
14+
/// Onvif addresses.
15+
/// </summary>
16+
public string[] Addresses { get; set; }
17+
18+
/// <summary>
19+
/// Onvif scopes.
20+
/// </summary>
21+
public string[] Scopes { get; set; }
22+
23+
/// <summary>
24+
/// Location - city.
25+
/// </summary>
26+
public string City { get; set; }
27+
28+
/// <summary>
29+
/// Location - country.
30+
/// </summary>
31+
public string Country { get; set; }
32+
33+
/// <summary>
34+
/// Hardware.
35+
/// </summary>
36+
public string Hardware { get; set; }
37+
38+
/// <summary>
39+
/// MAC address.
40+
/// </summary>
41+
public string MAC { get; set; }
42+
43+
/// <summary>
44+
/// Device manufacturer.
45+
/// </summary>
46+
public string Manufacturer { get; set; }
47+
48+
/// <summary>
49+
/// Device name.
50+
/// </summary>
51+
public string Name { get; set; }
52+
}
53+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace SharpOnvifCommon.Discovery
2+
{
3+
public static class Scopes
4+
{
5+
public const string MAC = "onvif://www.onvif.org/MAC/";
6+
public const string Manufacturer = "onvif://www.onvif.org/manufacturer/";
7+
public const string Hardware = "onvif://www.onvif.org/hardware/";
8+
public const string Name = "onvif://www.onvif.org/name/";
9+
public const string City = "onvif://www.onvif.org/location/city/";
10+
public const string Country = "onvif://www.onvif.org/location/country/";
11+
}
12+
}

0 commit comments

Comments
 (0)