13
13
14
14
namespace SharpOnvifClient
15
15
{
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>
25
19
public static class OnvifDiscoveryClient
26
20
{
27
21
public const int ONVIF_BROADCAST_TIMEOUT = 4000 ; // 4s timeout
@@ -38,10 +32,9 @@ public static class OnvifDiscoveryClient
38
32
/// <param name="broadcastPort">Broadcast port - 0 to let the OS choose any free port.</param>
39
33
/// <param name="deviceType">Device type we are searching for.</param>
40
34
/// <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" )
42
36
{
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 ) ;
45
38
}
46
39
47
40
/// <summary>
@@ -53,38 +46,9 @@ public static async Task<IList<string>> DiscoverAsync(Action<string> onDeviceDis
53
46
/// <param name="broadcastPort">Broadcast port - 0 to let the OS choose any free port.</param>
54
47
/// <param name="deviceType">Device type we are searching for.</param>
55
48
/// <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" )
72
50
{
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 ) ;
88
52
}
89
53
90
54
/// <summary>
@@ -95,7 +59,7 @@ public static async Task<IList<OnvifDiscoveryResult>> DiscoverAsyncWithMake(stri
95
59
/// <param name="broadcastPort"></param>
96
60
/// <param name="deviceType"></param>
97
61
/// <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" )
99
63
{
100
64
NetworkInterface [ ] nics = NetworkInterface . GetAllNetworkInterfaces ( ) ;
101
65
List < Task < IList < OnvifDiscoveryResult > > > discoveryTasks = new List < Task < IList < OnvifDiscoveryResult > > > ( ) ;
@@ -142,7 +106,7 @@ internal static async Task<IList<OnvifDiscoveryResult>> DiscoverAllAsync(Action<
142
106
143
107
await Task . WhenAll ( discoveryTasks ) ;
144
108
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 ( ) ;
146
110
}
147
111
148
112
/// <summary>
@@ -154,7 +118,7 @@ internal static async Task<IList<OnvifDiscoveryResult>> DiscoverAllAsync(Action<
154
118
/// <param name="broadcastPort"></param>
155
119
/// <param name="deviceType"></param>
156
120
/// <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" )
158
122
{
159
123
if ( ipAddress == null )
160
124
throw new ArgumentNullException ( nameof ( ipAddress ) ) ;
@@ -198,28 +162,26 @@ void ReceiveCallback(IAsyncResult ar)
198
162
byte [ ] receiveBytes = client . EndReceive ( ar , ref remote ) ;
199
163
string response = Encoding . UTF8 . GetString ( receiveBytes ) ;
200
164
201
- var endpoint = ReadOnvifEndpoint ( response ) ;
202
- var ( make , model ) = ReadOnvifMakeModelFromScopes ( response ) ;
165
+ var parsed = ParseDiscoveryResponse ( response ) ;
203
166
204
- if ( ! string . IsNullOrEmpty ( endpoint ) )
167
+ if ( parsed . Addresses != null && parsed . Addresses . Length > 0 )
205
168
{
206
169
lock ( results )
207
170
{
208
- if ( endpoints . Add ( endpoint ) )
171
+ if ( endpoints . Add ( parsed . Addresses . First ( ) ) )
209
172
{
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 ) ;
213
175
}
214
176
}
215
177
}
216
- // Continue receiving
178
+ // continue receiving
217
179
if ( ! cts . IsCancellationRequested )
218
180
client . BeginReceive ( ReceiveCallback , null ) ;
219
181
}
220
182
catch
221
183
{
222
- // Ignore exceptions on shutdown
184
+ // ignore exceptions on shutdown
223
185
}
224
186
}
225
187
@@ -231,7 +193,7 @@ void ReceiveCallback(IAsyncResult ar)
231
193
await Task . Delay ( broadcastTimeout , cts . Token ) ;
232
194
cts . Cancel ( ) ;
233
195
234
- // Return a snapshot of the results
196
+ // return a snapshot of the results
235
197
lock ( results )
236
198
{
237
199
return results . ToList ( ) ;
@@ -240,75 +202,71 @@ void ReceiveCallback(IAsyncResult ar)
240
202
}
241
203
finally
242
204
{
205
+ cts . Dispose ( ) ;
243
206
_discoverySlim . Release ( ) ;
244
207
}
245
208
}
246
209
247
-
248
-
249
- private static string ReadOnvifEndpoint ( string message )
210
+ private static OnvifDiscoveryResult ParseDiscoveryResponse ( string response )
250
211
{
251
- using ( var textReader = new StringReader ( message ) )
212
+ using ( var textReader = new StringReader ( response ) )
252
213
{
253
214
var document = new XPathDocument ( textReader ) ;
254
215
var navigator = document . CreateNavigator ( ) ;
255
216
217
+ OnvifDiscoveryResult result = new OnvifDiscoveryResult ( ) ;
218
+ result . Raw = response ;
219
+
256
220
// local-name is used to ignore the namespace
221
+
222
+ // parse the XAddrs
257
223
var node = navigator . SelectSingleNode ( "//*[local-name()='XAddrs']/text()" ) ;
258
224
if ( node != null )
259
225
{
260
226
string [ ] addresses = node . Value . Split ( ' ' ) ;
261
- return addresses . First ( ) ;
227
+ result . Addresses = addresses ;
262
228
}
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 ( ) ;
276
229
230
+ // parse Scopes
277
231
var scopesNode = navigator . SelectSingleNode ( "//*[local-name()='Scopes']/text()" ) ;
278
232
if ( scopesNode != null )
279
233
{
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
283
240
{
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 )
289
242
{
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 ) ) ;
306
260
}
307
261
}
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 \n Continuing...") ;
265
+ }
309
266
}
267
+
268
+ return result ;
310
269
}
311
- return ( null , null ) ;
312
270
}
313
271
}
314
272
}
0 commit comments