source: other-projects/playing-in-the-street/summer-2013/trunk/Playing-in-the-Street-WPF/Microsoft.Samples.Kinect.Webserver/KinectWebserver.cs@ 28897

Last change on this file since 28897 was 28897, checked in by davidb, 10 years ago

GUI front-end to server base plus web page content

File size: 15.9 KB
Line 
1// -----------------------------------------------------------------------
2// <copyright file="KinectWebserver.cs" company="Microsoft">
3// Copyright (c) Microsoft Corporation. All rights reserved.
4// </copyright>
5// -----------------------------------------------------------------------
6
7namespace Microsoft.Samples.Kinect.Webserver
8{
9 using System;
10 using System.Collections.Generic;
11 using System.Collections.ObjectModel;
12 using System.Globalization;
13 using System.Linq;
14
15 using Microsoft.Kinect.Toolkit;
16 using Microsoft.Samples.Kinect.Webserver.Sensor;
17
18 /// <summary>
19 /// Server used to expose Kinect state and events to web clients.
20 /// </summary>
21 /// <remarks>
22 /// <para>
23 /// Instances of this class are not thread-safe, and are expected to be called from
24 /// a single thread, e.g.: a UI thread.
25 /// </para>
26 /// <para>
27 /// By default listens for connections at origin http://localhost:8181 and does not
28 /// serve files until owner configures FileServerRootDirectory property.
29 /// </para>
30 /// </remarks>
31 public sealed class KinectWebserver
32 {
33 /// <summary>
34 /// Base URI path (within expected origin) for Kinect endpoints.
35 /// </summary>
36 private const string KinectEndpointBasePath = "/Kinect";
37
38 /// <summary>
39 /// Base URI path (within expected origin) for file server endpoints.
40 /// </summary>
41 private const string DefaultFileEndpointBasePath = "/files";
42
43 /// <summary>
44 /// Default HTTP URI origin to listen on.
45 /// </summary>
46 private static readonly Uri DefaultUriOrigin = new Uri("http://localhost:8181");
47
48 /// <summary>
49 /// Mapping between URI base path names to be used by clients to refer to data from a
50 /// specific Kinect sensor and their corresponding KinectSensorChooser objects.
51 /// </summary>
52 private readonly Dictionary<string, KinectSensorChooser> sensorChooserMap = new Dictionary<string, KinectSensorChooser>();
53
54 /// <summary>
55 /// Origin Uris that are allowed to access data owned by this server.
56 /// </summary>
57 private readonly List<Uri> allowedOrigins = new List<Uri>();
58
59 /// <summary>
60 /// Collection of stream handler factories to be used to process kinect data and deliver
61 /// data streams ready for web consumption.
62 /// </summary>
63 private readonly Collection<ISensorStreamHandlerFactory> streamHandlerFactories;
64
65 /// <summary>
66 /// Snapshot of mapping between URI base path names to be used by clients to refer to
67 /// data from a specific Kinect sensor and their corresponding KinectSensorChooser
68 /// objects, taken every time we create a new listener.
69 /// </summary>
70 private Dictionary<string, KinectSensorChooser> sensorChooserMapSnapshot = new Dictionary<string, KinectSensorChooser>();
71
72 /// <summary>
73 /// Snapshot of origin Uris that are allowed to access data owned by this server,
74 /// taken every time we create a new listener.
75 /// </summary>
76 private List<Uri> allowedOriginsSnapshot = new List<Uri>();
77
78 /// <summary>
79 /// Snapshot of collection of stream handler factories to be used to process kinect data and deliver
80 /// data streams ready for web consumption, taken every time we create a new listener.
81 /// </summary>
82 private Collection<ISensorStreamHandlerFactory> streamHandlerFactoriesSnapshot = new Collection<ISensorStreamHandlerFactory>();
83
84 /// <summary>
85 /// Uri specifying origin associated with web server.
86 /// </summary>
87 private Uri originUri = DefaultUriOrigin;
88
89 /// <summary>
90 /// True if origin URI has changed since last time we started listening for
91 /// connections.
92 /// </summary>
93 private bool hasOriginChanged;
94
95 /// <summary>
96 /// URI base path name to be used by clients to refer to HTTP endpoint that serves
97 /// static files.
98 /// </summary>
99 private string fileServerBasePath = DefaultFileEndpointBasePath;
100
101 /// <summary>
102 /// Root directory in local file system from which static files are served.
103 /// </summary>
104 /// <remarks>
105 /// If null, static files will not be served.
106 /// </remarks>
107 private string fileServerRootDirectory;
108
109 /// <summary>
110 /// True if static file server settings have changed since last time we started
111 /// listening for connections.
112 /// </summary>
113 private bool hasFileServerChanged;
114
115 /// <summary>
116 /// Listener that handles HTTP requests in a dedicated thread.
117 /// </summary>
118 private ThreadHostedHttpListener listener;
119
120 /// <summary>
121 /// Initializes a new instance of the <see cref="KinectWebserver"/> class.
122 /// </summary>
123 public KinectWebserver()
124 {
125 this.streamHandlerFactories = KinectRequestHandlerFactory.CreateDefaultStreamHandlerFactories();
126 }
127
128 /// <summary>
129 /// Event used to signal that the server has started listening for connections.
130 /// </summary>
131 public event EventHandler<EventArgs> Started;
132
133 /// <summary>
134 /// Event used to signal that the server has stopped listening for connections.
135 /// </summary>
136 public event EventHandler<EventArgs> Stopped;
137
138 /// <summary>
139 /// Mapping between URI base path names to be used by clients to refer to data from a
140 /// specific Kinect sensor and their corresponding KinectSensorChooser objects.
141 /// </summary>
142 /// <remarks>
143 /// Changes made to this map are only reflected in the server request-handling
144 /// behavior after a call to <see cref="Start"/> method after the modification
145 /// has taken place.
146 /// </remarks>
147 public Dictionary<string, KinectSensorChooser> SensorChooserMap
148 {
149 get
150 {
151 return this.sensorChooserMap;
152 }
153 }
154
155 /// <summary>
156 /// Origin Uris that are allowed to access data served by this listener, in addition
157 /// to owned origin Uri.
158 /// </summary>
159 /// <remarks>
160 /// Changes made to this list are only reflected in the server request-handling
161 /// behavior after a call to <see cref="Start"/> method after the modification
162 /// has taken place.
163 /// </remarks>
164 public ICollection<Uri> AccessControlAllowedOrigins
165 {
166 get
167 {
168 return this.allowedOrigins;
169 }
170 }
171
172 /// <summary>
173 /// Set of stream handler factories to be used to process kinect data and deliver
174 /// data streams ready for web consumption.
175 /// </summary>
176 public ICollection<ISensorStreamHandlerFactory> SensorStreamHandlerFactories
177 {
178 get
179 {
180 return this.streamHandlerFactories;
181 }
182 }
183
184 /// <summary>
185 /// URI base path name to be used by clients to refer to HTTP endpoint that serves
186 /// static files.
187 /// </summary>
188 /// <remarks>
189 /// <para>
190 /// Must not be a null or empty string.
191 /// </para>
192 /// <para>
193 /// Valid examples: "file", "files", "content", "resources", etc.
194 /// </para>
195 /// <para>
196 /// Changes made to this map are only reflected in the server request-handling
197 /// behavior after a call to <see cref="Start"/> method after the modification
198 /// has taken place.
199 /// </para>
200 /// </remarks>
201 public string FileServerBasePath
202 {
203 get
204 {
205 return this.fileServerBasePath;
206 }
207
208 set
209 {
210 if (string.IsNullOrEmpty(value))
211 {
212 throw new ArgumentException(@"FileServerBasePath value must not be null or empty");
213 }
214
215 this.fileServerBasePath = value;
216 this.hasFileServerChanged = true;
217 }
218 }
219
220 /// <summary>
221 /// Origin URI where web server is listening for requests.
222 /// </summary>
223 public Uri OriginUri
224 {
225 get
226 {
227 return this.originUri;
228 }
229
230 set
231 {
232 if (value == null)
233 {
234 throw new ArgumentNullException("value");
235 }
236
237 this.originUri = value;
238 this.hasOriginChanged = true;
239 }
240 }
241
242 /// <summary>
243 /// Root directory in local file system from which static files are served.
244 /// </summary>
245 /// <remarks>
246 /// <para>
247 /// If null, static files will not be served.
248 /// </para>
249 /// <para>
250 /// Changes made to this map are only reflected in the server request-handling
251 /// behavior after a call to <see cref="Start"/> method after the modification
252 /// has taken place.
253 /// </para>
254 /// </remarks>
255 public string FileServerRootDirectory
256 {
257 get
258 {
259 return this.fileServerRootDirectory;
260 }
261
262 set
263 {
264 this.fileServerRootDirectory = value;
265 this.hasFileServerChanged = true;
266 }
267 }
268
269 /// <summary>
270 /// Start listening for requests.
271 /// </summary>
272 public void Start()
273 {
274 if (this.SensorChooserMap.Count <= 0)
275 {
276 if (this.listener != null)
277 {
278 this.listener.Stop();
279 }
280
281 return;
282 }
283
284 bool snapshotMatch = true;
285
286 // Check if the sensor chooser map, the allowed origins list or the collection
287 // of stream handler factories have changed since last snapshot was taken.
288 // If any of them has changed we need to create a new ThreadHostedHttpListener,
289 // since the listener handler factory map is immutable data per instance.
290 if (!SensorMapEquals(this.sensorChooserMap, this.sensorChooserMapSnapshot))
291 {
292 snapshotMatch = false;
293 this.sensorChooserMapSnapshot = new Dictionary<string, KinectSensorChooser>(this.sensorChooserMap);
294 }
295
296 if (!this.allowedOrigins.SequenceEqual(this.allowedOriginsSnapshot))
297 {
298 snapshotMatch = false;
299 this.allowedOriginsSnapshot = new List<Uri>(this.allowedOrigins);
300 }
301
302 if (!this.streamHandlerFactoriesSnapshot.SequenceEqual(this.streamHandlerFactories))
303 {
304 snapshotMatch = false;
305 this.streamHandlerFactoriesSnapshot = new Collection<ISensorStreamHandlerFactory>(this.streamHandlerFactories);
306 }
307
308 if (!snapshotMatch || this.hasFileServerChanged || this.hasOriginChanged || (this.listener == null))
309 {
310 if (this.listener != null)
311 {
312 // we need to stop the current listener before creating a new one with
313 // diferent parameters
314 this.listener.Stop(true);
315 this.listener.Started -= this.ListenerOnStarted;
316 this.listener.Stopped -= this.ListenerOnStopped;
317 this.listener = null;
318 }
319
320 var factoryMap = new Dictionary<string, IHttpRequestHandlerFactory>();
321 foreach (var entry in this.sensorChooserMapSnapshot)
322 {
323 factoryMap.Add(
324 string.Format(CultureInfo.InvariantCulture, "{0}/{1}", KinectEndpointBasePath, entry.Key),
325 new KinectRequestHandlerFactory(entry.Value, this.streamHandlerFactoriesSnapshot));
326 }
327
328 if (!string.IsNullOrEmpty(this.fileServerRootDirectory))
329 {
330 factoryMap.Add(
331 this.fileServerBasePath,
332 new FileRequestHandlerFactory(this.fileServerRootDirectory));
333 }
334
335 var origins = new[] { this.OriginUri };
336 this.listener = new ThreadHostedHttpListener(origins, this.allowedOriginsSnapshot, factoryMap);
337 this.listener.Started += this.ListenerOnStarted;
338 this.listener.Stopped += this.ListenerOnStopped;
339 this.listener.Start();
340
341 this.hasFileServerChanged = false;
342 this.hasOriginChanged = false;
343 }
344 else if (!this.listener.IsListening)
345 {
346 this.listener.Start();
347 }
348 }
349
350 /// <summary>
351 /// Stop listening for requests.
352 /// </summary>
353 public void Stop()
354 {
355 if (this.listener == null)
356 {
357 return;
358 }
359
360 this.listener.Stop();
361 }
362
363 /// <summary>
364 /// Determine if two sensor chooser maps are equivalent.
365 /// </summary>
366 /// <param name="left">
367 /// Reference dictionary in comparison.
368 /// </param>
369 /// <param name="right">
370 /// Other dictionary to compare against
371 /// </param>
372 /// <returns>
373 /// True if left and right dictionaries are equivalent. False otherwise.
374 /// </returns>
375 private static bool SensorMapEquals(Dictionary<string, KinectSensorChooser> left, Dictionary<string, KinectSensorChooser> right)
376 {
377 if (left == right)
378 {
379 return true;
380 }
381
382 if ((left == null) || (right == null))
383 {
384 return false;
385 }
386
387 if (left.Count != right.Count)
388 {
389 return false;
390 }
391
392 foreach (var entry in left)
393 {
394 KinectSensorChooser rightValue;
395 if (!right.TryGetValue(entry.Key, out rightValue))
396 {
397 return false;
398 }
399
400 if (entry.Value == null)
401 {
402 if (rightValue != null)
403 {
404 return false;
405 }
406
407 continue;
408 }
409
410 if (!entry.Value.Equals(rightValue))
411 {
412 return false;
413 }
414 }
415
416 return true;
417 }
418
419 /// <summary>
420 /// Handler for ThreadHostedHttpListener.Started event.
421 /// </summary>
422 /// <param name="sender">
423 /// Object that sent the event.
424 /// </param>
425 /// <param name="args">
426 /// Event arguments.
427 /// </param>
428 private void ListenerOnStarted(object sender, EventArgs args)
429 {
430 if (this.Started != null)
431 {
432 this.Started(this, args);
433 }
434 }
435
436 /// <summary>
437 /// Handler for ThreadHostedHttpListener.Stopped event.
438 /// </summary>
439 /// <param name="sender">
440 /// Object that sent the event.
441 /// </param>
442 /// <param name="args">
443 /// Event arguments.
444 /// </param>
445 private void ListenerOnStopped(object sender, EventArgs args)
446 {
447 if (this.Stopped != null)
448 {
449 this.Stopped(this, args);
450 }
451 }
452 }
453}
Note: See TracBrowser for help on using the repository browser.