1 | // -----------------------------------------------------------------------
|
---|
2 | // <copyright file="DefaultUserStateManager.cs" company="Microsoft">
|
---|
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
|
---|
4 | // </copyright>
|
---|
5 | // -----------------------------------------------------------------------
|
---|
6 |
|
---|
7 | namespace Microsoft.Samples.Kinect.Webserver.Sensor
|
---|
8 | {
|
---|
9 | using System;
|
---|
10 | using System.Collections.Generic;
|
---|
11 | using System.Linq;
|
---|
12 |
|
---|
13 | using Microsoft.Kinect;
|
---|
14 | using Microsoft.Kinect.Toolkit;
|
---|
15 | using Microsoft.Kinect.Toolkit.Interaction;
|
---|
16 | using Microsoft.Samples.Kinect.Webserver.Sensor.Serialization;
|
---|
17 |
|
---|
18 | /// <summary>
|
---|
19 | /// Default implementation of <see cref="IUserStateManager"/> interface.
|
---|
20 | /// </summary>
|
---|
21 | public class DefaultUserStateManager : IUserStateManager
|
---|
22 | {
|
---|
23 | /// <summary>
|
---|
24 | /// Name of state representing a tracked user.
|
---|
25 | /// </summary>
|
---|
26 | public const string TrackedStateName = "tracked";
|
---|
27 |
|
---|
28 | /// <summary>
|
---|
29 | /// Name of state representing an engaged user.
|
---|
30 | /// </summary>
|
---|
31 | public const string EngagedStateName = "engaged";
|
---|
32 |
|
---|
33 | /// <summary>
|
---|
34 | /// Category of all events originating from this class.
|
---|
35 | /// </summary>
|
---|
36 | public const string EventCategory = "userState";
|
---|
37 |
|
---|
38 | /// <summary>
|
---|
39 | /// Event type for primary user changed event.
|
---|
40 | /// </summary>
|
---|
41 | public const string PrimaryUserChangedEventType = "primaryUserChanged";
|
---|
42 |
|
---|
43 | /// <summary>
|
---|
44 | /// Event type for user state changed event.
|
---|
45 | /// </summary>
|
---|
46 | public const string UserStatesChangedEventType = "userStatesChanged";
|
---|
47 |
|
---|
48 | /// <summary>
|
---|
49 | /// Length (in milliseconds) of period of inactivity required
|
---|
50 | /// before users become candidates for tracking.
|
---|
51 | /// </summary>
|
---|
52 | internal const long MinimumInactivityBeforeTrackingMilliseconds = 500;
|
---|
53 |
|
---|
54 | /// <summary>
|
---|
55 | /// Object used to keep track of user activity.
|
---|
56 | /// </summary>
|
---|
57 | private readonly UserActivityMeter activityMeter = new UserActivityMeter();
|
---|
58 |
|
---|
59 | /// <summary>
|
---|
60 | /// Object used to synchronize modifications to engagement state.
|
---|
61 | /// </summary>
|
---|
62 | private readonly object lockObject = new object();
|
---|
63 |
|
---|
64 | /// <summary>
|
---|
65 | /// Helper object used to keep track of previous set of tracked user ids while avoiding
|
---|
66 | /// object allocation while processing each frame.
|
---|
67 | /// </summary>
|
---|
68 | private HashSet<int> previousTrackedUserTrackingIds = new HashSet<int>();
|
---|
69 |
|
---|
70 | /// <summary>
|
---|
71 | /// Map between user tracking IDs and user states exposed to clients.
|
---|
72 | /// </summary>
|
---|
73 | private Dictionary<int, string> publicUserStates = new Dictionary<int, string>();
|
---|
74 |
|
---|
75 | /// <summary>
|
---|
76 | /// Dictionary used to accumulate mappings between user tracking IDs and user states
|
---|
77 | /// before they're ready to be exposed to clients.
|
---|
78 | /// </summary>
|
---|
79 | private Dictionary<int, string> userStatesAccumulator = new Dictionary<int, string>();
|
---|
80 |
|
---|
81 | /// <summary>
|
---|
82 | /// Initializes a new instance of the <see cref="DefaultUserStateManager"/> class.
|
---|
83 | /// </summary>
|
---|
84 | public DefaultUserStateManager()
|
---|
85 | {
|
---|
86 | this.TrackedUserTrackingIds = new HashSet<int>();
|
---|
87 | }
|
---|
88 |
|
---|
89 | /// <summary>
|
---|
90 | /// Event triggered whenever user state changes.
|
---|
91 | /// </summary>
|
---|
92 | public event EventHandler<UserStateChangedEventArgs> UserStateChanged;
|
---|
93 |
|
---|
94 | /// <summary>
|
---|
95 | /// Dictionary mapping user tracking Ids to names used for states corresponding to
|
---|
96 | /// those users.
|
---|
97 | /// </summary>
|
---|
98 | public IDictionary<int, string> UserStates
|
---|
99 | {
|
---|
100 | get
|
---|
101 | {
|
---|
102 | return this.publicUserStates;
|
---|
103 | }
|
---|
104 | }
|
---|
105 |
|
---|
106 | /// <summary>
|
---|
107 | /// Tracking ID corresponding to primary user.
|
---|
108 | /// </summary>
|
---|
109 | /// <remarks>
|
---|
110 | /// May be an invalid tracking id to represent that no user is currently primary.
|
---|
111 | /// </remarks>
|
---|
112 | public int PrimaryUserTrackingId { get; set; }
|
---|
113 |
|
---|
114 | /// <summary>
|
---|
115 | /// Tracking ID corresponding to engaged user.
|
---|
116 | /// </summary>
|
---|
117 | /// <remarks>
|
---|
118 | /// May be an invalid tracking id to represent that no user is currently engaged.
|
---|
119 | /// </remarks>
|
---|
120 | private int EngagedUserTrackingId { get; set; }
|
---|
121 |
|
---|
122 | /// <summary>
|
---|
123 | /// Set of tracking Ids corresponding to users currently considered to be tracked.
|
---|
124 | /// </summary>
|
---|
125 | private HashSet<int> TrackedUserTrackingIds { get; set; }
|
---|
126 |
|
---|
127 | /// <summary>
|
---|
128 | /// Resets all state to the initial state, with no users remembered as engaged or tracked.
|
---|
129 | /// </summary>
|
---|
130 | public void Reset()
|
---|
131 | {
|
---|
132 | using (var callbackLock = new CallbackLock(this.lockObject))
|
---|
133 | {
|
---|
134 | this.activityMeter.Clear();
|
---|
135 | this.TrackedUserTrackingIds.Clear();
|
---|
136 | this.EngagedUserTrackingId = SharedConstants.InvalidUserTrackingId;
|
---|
137 | this.SetPrimaryUserTrackingId(SharedConstants.InvalidUserTrackingId, callbackLock);
|
---|
138 | this.UpdateUserStates(callbackLock);
|
---|
139 | }
|
---|
140 | }
|
---|
141 |
|
---|
142 | /// <summary>
|
---|
143 | /// Determines which users should be tracked in the future, based on selection
|
---|
144 | /// metrics and engagement state.
|
---|
145 | /// </summary>
|
---|
146 | /// <param name="frameSkeletons">
|
---|
147 | /// Array of skeletons from which the appropriate user tracking Ids will be selected.
|
---|
148 | /// </param>
|
---|
149 | /// <param name="timestamp">
|
---|
150 | /// Timestamp from skeleton frame.
|
---|
151 | /// </param>
|
---|
152 | /// <param name="chosenTrackingIds">
|
---|
153 | /// Array that will contain the tracking Ids of users to track, sorted from most
|
---|
154 | /// important to least important user to track.
|
---|
155 | /// </param>
|
---|
156 | public void ChooseTrackedUsers(Skeleton[] frameSkeletons, long timestamp, int[] chosenTrackingIds)
|
---|
157 | {
|
---|
158 | if (frameSkeletons == null)
|
---|
159 | {
|
---|
160 | throw new ArgumentNullException("frameSkeletons");
|
---|
161 | }
|
---|
162 |
|
---|
163 | if (chosenTrackingIds == null)
|
---|
164 | {
|
---|
165 | throw new ArgumentNullException("chosenTrackingIds");
|
---|
166 | }
|
---|
167 |
|
---|
168 | var availableSkeletons = new List<Skeleton>(
|
---|
169 | from skeleton in frameSkeletons
|
---|
170 | where
|
---|
171 | (skeleton.TrackingId != SharedConstants.InvalidUserTrackingId)
|
---|
172 | &&
|
---|
173 | ((skeleton.TrackingState == SkeletonTrackingState.Tracked)
|
---|
174 | || (skeleton.TrackingState == SkeletonTrackingState.PositionOnly))
|
---|
175 | select skeleton);
|
---|
176 | var trackingCandidateSkeletons = new List<Skeleton>();
|
---|
177 |
|
---|
178 | // Update user activity metrics
|
---|
179 | this.activityMeter.Update(availableSkeletons, timestamp);
|
---|
180 |
|
---|
181 | foreach (var skeleton in availableSkeletons)
|
---|
182 | {
|
---|
183 | UserActivityRecord record;
|
---|
184 | if (this.activityMeter.TryGetActivityRecord(skeleton.TrackingId, out record))
|
---|
185 | {
|
---|
186 | // The tracked skeletons become candidate skeletons for tracking if we have an activity record for them.
|
---|
187 | trackingCandidateSkeletons.Add(skeleton);
|
---|
188 | }
|
---|
189 | }
|
---|
190 |
|
---|
191 | // sort the currently tracked skeletons according to our tracking choice criteria
|
---|
192 | trackingCandidateSkeletons.Sort((left, right) => this.ComputeTrackingMetric(right).CompareTo(this.ComputeTrackingMetric(left)));
|
---|
193 |
|
---|
194 | for (int i = 0; i < chosenTrackingIds.Length; ++i)
|
---|
195 | {
|
---|
196 | chosenTrackingIds[i] = (i < trackingCandidateSkeletons.Count) ? trackingCandidateSkeletons[i].TrackingId : SharedConstants.InvalidUserTrackingId;
|
---|
197 | }
|
---|
198 | }
|
---|
199 |
|
---|
200 | /// <summary>
|
---|
201 | /// Called whenever the set of tracked users has changed.
|
---|
202 | /// </summary>
|
---|
203 | /// <param name="trackedUserInfo">
|
---|
204 | /// User information from which we'll update the set of tracked users and the primary user.
|
---|
205 | /// </param>
|
---|
206 | /// <param name="timestamp">
|
---|
207 | /// Interaction frame timestamp corresponding to given user information.
|
---|
208 | /// </param>
|
---|
209 | public void UpdateUserInformation(IEnumerable<UserInfo> trackedUserInfo, long timestamp)
|
---|
210 | {
|
---|
211 | bool foundEngagedUser = false;
|
---|
212 | int firstTrackedUser = SharedConstants.InvalidUserTrackingId;
|
---|
213 |
|
---|
214 | using (var callbackLock = new CallbackLock(this.lockObject))
|
---|
215 | {
|
---|
216 | this.previousTrackedUserTrackingIds.Clear();
|
---|
217 | var nextTrackedIds = this.previousTrackedUserTrackingIds;
|
---|
218 | this.previousTrackedUserTrackingIds = this.TrackedUserTrackingIds;
|
---|
219 | this.TrackedUserTrackingIds = nextTrackedIds;
|
---|
220 |
|
---|
221 | var trackedUserInfoArray = trackedUserInfo as UserInfo[] ?? trackedUserInfo.ToArray();
|
---|
222 |
|
---|
223 | foreach (var userInfo in trackedUserInfoArray)
|
---|
224 | {
|
---|
225 | if (userInfo.SkeletonTrackingId == SharedConstants.InvalidUserTrackingId)
|
---|
226 | {
|
---|
227 | continue;
|
---|
228 | }
|
---|
229 |
|
---|
230 | if (this.EngagedUserTrackingId == userInfo.SkeletonTrackingId)
|
---|
231 | {
|
---|
232 | this.TrackedUserTrackingIds.Add(userInfo.SkeletonTrackingId);
|
---|
233 |
|
---|
234 | foundEngagedUser = true;
|
---|
235 | }
|
---|
236 | else if (HasTrackedHands(userInfo)
|
---|
237 | && (this.previousTrackedUserTrackingIds.Contains(userInfo.SkeletonTrackingId)
|
---|
238 | || this.IsInactive(userInfo, timestamp)))
|
---|
239 | {
|
---|
240 | // Keep track of the non-engaged users we find that have at least one
|
---|
241 | // tracked hand pointer and also either (1) were previously tracked or
|
---|
242 | // (2) are not moving too much
|
---|
243 | this.TrackedUserTrackingIds.Add(userInfo.SkeletonTrackingId);
|
---|
244 |
|
---|
245 | if (firstTrackedUser == SharedConstants.InvalidUserTrackingId)
|
---|
246 | {
|
---|
247 | // Consider the first non-engaged, stationary user as a candidate for engagement
|
---|
248 | firstTrackedUser = userInfo.SkeletonTrackingId;
|
---|
249 | }
|
---|
250 | }
|
---|
251 | }
|
---|
252 |
|
---|
253 | // If engaged user was not found in list of candidate users, engaged user has become invalid.
|
---|
254 | if (!foundEngagedUser)
|
---|
255 | {
|
---|
256 | this.EngagedUserTrackingId = SharedConstants.InvalidUserTrackingId;
|
---|
257 | }
|
---|
258 |
|
---|
259 | // Decide who should be the primary user, if anyone
|
---|
260 | this.UpdatePrimaryUser(trackedUserInfoArray, callbackLock);
|
---|
261 |
|
---|
262 | // If there's a primary user, it is the preferred candidate for engagement.
|
---|
263 | // Otherwise, the first tracked user seen is the preferred candidate.
|
---|
264 | int candidateUserTrackingId = (this.PrimaryUserTrackingId != SharedConstants.InvalidUserTrackingId)
|
---|
265 | ? this.PrimaryUserTrackingId
|
---|
266 | : firstTrackedUser;
|
---|
267 |
|
---|
268 | // If there is a valid candidate user that is not already the engaged user
|
---|
269 | if ((candidateUserTrackingId != SharedConstants.InvalidUserTrackingId)
|
---|
270 | && (candidateUserTrackingId != this.EngagedUserTrackingId))
|
---|
271 | {
|
---|
272 | // If there is currently no engaged user, or if candidate user is the
|
---|
273 | // primary user controlling interactions while the currently engaged user
|
---|
274 | // is not interacting
|
---|
275 | if ((this.EngagedUserTrackingId == SharedConstants.InvalidUserTrackingId)
|
---|
276 | || (candidateUserTrackingId == this.PrimaryUserTrackingId))
|
---|
277 | {
|
---|
278 | this.PromoteCandidateToEngaged(candidateUserTrackingId);
|
---|
279 | }
|
---|
280 | }
|
---|
281 |
|
---|
282 | // Update user states as the very last action, to include results from updates
|
---|
283 | // performed so far
|
---|
284 | this.UpdateUserStates(callbackLock);
|
---|
285 | }
|
---|
286 | }
|
---|
287 |
|
---|
288 | /// <summary>
|
---|
289 | /// Promote candidate user to be the engaged user.
|
---|
290 | /// </summary>
|
---|
291 | /// <param name="candidateTrackingId">
|
---|
292 | /// Tracking Id of user to be promoted to engaged user.
|
---|
293 | /// If tracking Id does not match the Id of one of the currently tracked users,
|
---|
294 | /// no action is taken.
|
---|
295 | /// </param>
|
---|
296 | /// <returns>
|
---|
297 | /// True if specified candidate could be confirmed as the new engaged user,
|
---|
298 | /// false otherwise.
|
---|
299 | /// </returns>
|
---|
300 | public bool PromoteCandidateToEngaged(int candidateTrackingId)
|
---|
301 | {
|
---|
302 | bool isConfirmed = false;
|
---|
303 |
|
---|
304 | if ((candidateTrackingId != SharedConstants.InvalidUserTrackingId) && this.TrackedUserTrackingIds.Contains(candidateTrackingId))
|
---|
305 | {
|
---|
306 | using (var callbackLock = new CallbackLock(this.lockObject))
|
---|
307 | {
|
---|
308 | this.EngagedUserTrackingId = candidateTrackingId;
|
---|
309 | this.UpdateUserStates(callbackLock);
|
---|
310 | }
|
---|
311 |
|
---|
312 | isConfirmed = true;
|
---|
313 | }
|
---|
314 |
|
---|
315 | return isConfirmed;
|
---|
316 | }
|
---|
317 |
|
---|
318 | /// <summary>
|
---|
319 | /// Tries to get the last position observed for the specified user tracking Id.
|
---|
320 | /// </summary>
|
---|
321 | /// <param name="trackingId">
|
---|
322 | /// User tracking Id for which we're finding the last position observed.
|
---|
323 | /// </param>
|
---|
324 | /// <returns>
|
---|
325 | /// Skeleton point, if last position is being tracked for specified
|
---|
326 | /// tracking Id, null otherwise.
|
---|
327 | /// </returns>
|
---|
328 | public SkeletonPoint? TryGetLastPositionForId(int trackingId)
|
---|
329 | {
|
---|
330 | if (SharedConstants.InvalidUserTrackingId == trackingId)
|
---|
331 | {
|
---|
332 | return null;
|
---|
333 | }
|
---|
334 |
|
---|
335 | UserActivityRecord record;
|
---|
336 | if (this.activityMeter.TryGetActivityRecord(trackingId, out record))
|
---|
337 | {
|
---|
338 | return record.LastPosition;
|
---|
339 | }
|
---|
340 |
|
---|
341 | return null;
|
---|
342 | }
|
---|
343 |
|
---|
344 | /// <summary>
|
---|
345 | /// Get a JSON friendly array of user-tracking-id-to-state mapping entries
|
---|
346 | /// representing the specified user state map.
|
---|
347 | /// </summary>
|
---|
348 | /// <param name="userStates">
|
---|
349 | /// Dictionary mapping user tracking ids to user state names.
|
---|
350 | /// </param>
|
---|
351 | /// <returns>
|
---|
352 | /// Array of <see cref="StateMappingEntry"/> objects.
|
---|
353 | /// </returns>
|
---|
354 | internal static StateMappingEntry[] GetStateMappingEntryArray(IDictionary<int, string> userStates)
|
---|
355 | {
|
---|
356 | var mappingEntries = new StateMappingEntry[userStates.Count];
|
---|
357 | int entryIndex = 0;
|
---|
358 | foreach (var userStateEntry in userStates)
|
---|
359 | {
|
---|
360 | mappingEntries[entryIndex] = new StateMappingEntry { id = userStateEntry.Key, userState = userStateEntry.Value };
|
---|
361 | ++entryIndex;
|
---|
362 | }
|
---|
363 |
|
---|
364 | return mappingEntries;
|
---|
365 | }
|
---|
366 |
|
---|
367 | internal void SetPrimaryUserTrackingId(int newId, CallbackLock callbackLock)
|
---|
368 | {
|
---|
369 | int oldId = this.PrimaryUserTrackingId;
|
---|
370 | this.PrimaryUserTrackingId = newId;
|
---|
371 |
|
---|
372 | if (oldId != newId)
|
---|
373 | {
|
---|
374 | callbackLock.LockExit +=
|
---|
375 | () =>
|
---|
376 | this.SendUserStateChanged(
|
---|
377 | new UserTrackingIdChangedEventMessage
|
---|
378 | {
|
---|
379 | category = EventCategory,
|
---|
380 | eventType = PrimaryUserChangedEventType,
|
---|
381 | oldValue = oldId,
|
---|
382 | newValue = newId
|
---|
383 | });
|
---|
384 | }
|
---|
385 | }
|
---|
386 |
|
---|
387 | /// <summary>
|
---|
388 | /// Determine if any of the specified user's hands is tracked.
|
---|
389 | /// </summary>
|
---|
390 | /// <param name="userInfo">
|
---|
391 | /// User information from which to determine hand tracking status.
|
---|
392 | /// </param>
|
---|
393 | /// <returns>
|
---|
394 | /// True if user has at least one tracked hand pointer. False otherwise.
|
---|
395 | /// </returns>
|
---|
396 | private static bool HasTrackedHands(UserInfo userInfo)
|
---|
397 | {
|
---|
398 | return userInfo.HandPointers.Any(handPointer => handPointer.IsTracked);
|
---|
399 | }
|
---|
400 |
|
---|
401 | /// <summary>
|
---|
402 | /// Update the primary user being tracked.
|
---|
403 | /// </summary>
|
---|
404 | /// <param name="candidateUserInfo">
|
---|
405 | /// User information collection from which we will choose a primary user.
|
---|
406 | /// </param>
|
---|
407 | /// <param name="callbackLock">
|
---|
408 | /// Lock used to delay all events until after we exit lock section.
|
---|
409 | /// </param>
|
---|
410 | private void UpdatePrimaryUser(IEnumerable<UserInfo> candidateUserInfo, CallbackLock callbackLock)
|
---|
411 | {
|
---|
412 | int firstPrimaryUserCandidate = SharedConstants.InvalidUserTrackingId;
|
---|
413 | bool currentPrimaryUserStillPrimary = false;
|
---|
414 | bool engagedUserIsPrimary = false;
|
---|
415 |
|
---|
416 | var trackingIdsAvailable = new HashSet<int>();
|
---|
417 |
|
---|
418 | foreach (var userInfo in candidateUserInfo)
|
---|
419 | {
|
---|
420 | if (userInfo.SkeletonTrackingId == SharedConstants.InvalidUserTrackingId)
|
---|
421 | {
|
---|
422 | continue;
|
---|
423 | }
|
---|
424 |
|
---|
425 | trackingIdsAvailable.Add(userInfo.SkeletonTrackingId);
|
---|
426 |
|
---|
427 | foreach (var handPointer in userInfo.HandPointers)
|
---|
428 | {
|
---|
429 | if (handPointer.IsPrimaryForUser)
|
---|
430 | {
|
---|
431 | if (this.PrimaryUserTrackingId == userInfo.SkeletonTrackingId)
|
---|
432 | {
|
---|
433 | // If the current primary user still has an active hand, we should continue to consider them the primary user.
|
---|
434 | currentPrimaryUserStillPrimary = true;
|
---|
435 | }
|
---|
436 | else if (SharedConstants.InvalidUserTrackingId == firstPrimaryUserCandidate)
|
---|
437 | {
|
---|
438 | // Else if this is the first user with an active hand, they are the alternative candidate for primary user.
|
---|
439 | firstPrimaryUserCandidate = userInfo.SkeletonTrackingId;
|
---|
440 | }
|
---|
441 |
|
---|
442 | if (this.EngagedUserTrackingId == userInfo.SkeletonTrackingId)
|
---|
443 | {
|
---|
444 | engagedUserIsPrimary = true;
|
---|
445 | }
|
---|
446 | }
|
---|
447 | }
|
---|
448 | }
|
---|
449 |
|
---|
450 | // If engaged user has a primary hand, always pick that user as primary user.
|
---|
451 | // If current primary user still has a primary hand, let them remain primary.
|
---|
452 | // Otherwise default to first primary user candidate seen.
|
---|
453 | int primaryUserTrackingId = engagedUserIsPrimary
|
---|
454 | ? this.EngagedUserTrackingId
|
---|
455 | : (currentPrimaryUserStillPrimary ? this.PrimaryUserTrackingId : firstPrimaryUserCandidate);
|
---|
456 | this.SetPrimaryUserTrackingId(primaryUserTrackingId, callbackLock);
|
---|
457 | }
|
---|
458 |
|
---|
459 | /// <summary>
|
---|
460 | /// Calculate how valuable it will be to keep tracking the specified skeleton.
|
---|
461 | /// </summary>
|
---|
462 | /// <param name="skeleton">
|
---|
463 | /// Skeleton that is one of several candidates for tracking.
|
---|
464 | /// </param>
|
---|
465 | /// <returns>
|
---|
466 | /// A non-negative metric that estimates how valuable it is to keep tracking
|
---|
467 | /// the specified skeleton. The higher the value, the more valuable the skeleton
|
---|
468 | /// is estimated to be.
|
---|
469 | /// </returns>
|
---|
470 | private double ComputeTrackingMetric(Skeleton skeleton)
|
---|
471 | {
|
---|
472 | const double MaxCameraDistance = 4.0;
|
---|
473 |
|
---|
474 | // Give preference to engaged users, then to tracked users, then to users
|
---|
475 | // near the center of the Kinect Sensor's field of view that are also
|
---|
476 | // closer (distance) to the KinectSensor and not moving around too much.
|
---|
477 | const double EngagedWeight = 100.0;
|
---|
478 | const double TrackedWeight = 50.0;
|
---|
479 | const double AngleFromCenterWeight = 1.30;
|
---|
480 | const double DistanceFromCameraWeight = 1.15;
|
---|
481 | const double BodyMovementWeight = 0.05;
|
---|
482 |
|
---|
483 | double engagedMetric = (skeleton.TrackingId == this.EngagedUserTrackingId) ? 1.0 : 0.0;
|
---|
484 | double trackedMetric = this.TrackedUserTrackingIds.Contains(skeleton.TrackingId) ? 1.0 : 0.0;
|
---|
485 | double angleFromCenterMetric = (skeleton.Position.Z > 0.0) ? (1.0 - Math.Abs(2 * Math.Atan(skeleton.Position.X / skeleton.Position.Z) / Math.PI)) : 0.0;
|
---|
486 | double distanceFromCameraMetric = (MaxCameraDistance - skeleton.Position.Z) / MaxCameraDistance;
|
---|
487 | UserActivityRecord activityRecord;
|
---|
488 | double bodyMovementMetric = this.activityMeter.TryGetActivityRecord(skeleton.TrackingId, out activityRecord)
|
---|
489 | ? 1.0 - activityRecord.ActivityLevel
|
---|
490 | : 0.0;
|
---|
491 | return (EngagedWeight * engagedMetric) +
|
---|
492 | (TrackedWeight * trackedMetric) +
|
---|
493 | (AngleFromCenterWeight * angleFromCenterMetric) +
|
---|
494 | (DistanceFromCameraWeight * distanceFromCameraMetric) +
|
---|
495 | (BodyMovementWeight * bodyMovementMetric);
|
---|
496 | }
|
---|
497 |
|
---|
498 | /// <summary>
|
---|
499 | /// Determine if the specified user information represents a user that has been
|
---|
500 | /// relatively inactive for at least a minimum period of time required for tracking.
|
---|
501 | /// </summary>
|
---|
502 | /// <param name="userInfo">
|
---|
503 | /// User information from which to determine inactivity.
|
---|
504 | /// </param>
|
---|
505 | /// <param name="timestamp">
|
---|
506 | /// Current timestamp used to determine how long user has been inactive.
|
---|
507 | /// </param>
|
---|
508 | /// <returns>
|
---|
509 | /// True if user is present in scene and has been inactive for a minimum threshold
|
---|
510 | /// period of time.
|
---|
511 | /// </returns>
|
---|
512 | private bool IsInactive(UserInfo userInfo, long timestamp)
|
---|
513 | {
|
---|
514 | UserActivityRecord record;
|
---|
515 | return this.activityMeter.TryGetActivityRecord(userInfo.SkeletonTrackingId, out record) && !record.IsActive
|
---|
516 | && (record.StateTransitionTimestamp + MinimumInactivityBeforeTrackingMilliseconds <= timestamp);
|
---|
517 | }
|
---|
518 |
|
---|
519 | /// <summary>
|
---|
520 | /// Determines if user states have changed.
|
---|
521 | /// </summary>
|
---|
522 | /// <returns>
|
---|
523 | /// true if accumulated user states are different from the ones currently visible
|
---|
524 | /// to clients.
|
---|
525 | /// </returns>
|
---|
526 | private bool HaveUserStatesChanged()
|
---|
527 | {
|
---|
528 | if (this.publicUserStates.Count != this.userStatesAccumulator.Count)
|
---|
529 | {
|
---|
530 | return true;
|
---|
531 | }
|
---|
532 |
|
---|
533 | foreach (var stateEntry in this.publicUserStates)
|
---|
534 | {
|
---|
535 | string accumulatorState;
|
---|
536 | if (!this.userStatesAccumulator.TryGetValue(stateEntry.Key, out accumulatorState))
|
---|
537 | {
|
---|
538 | // Key is absent from accumulator but present in current state map
|
---|
539 | return true;
|
---|
540 | }
|
---|
541 |
|
---|
542 | if (!stateEntry.Value.Equals(accumulatorState))
|
---|
543 | {
|
---|
544 | // state names are present in both maps, but they're different
|
---|
545 | return true;
|
---|
546 | }
|
---|
547 | }
|
---|
548 |
|
---|
549 | return false;
|
---|
550 | }
|
---|
551 |
|
---|
552 | /// <summary>
|
---|
553 | /// Update user states exposed to clients, if necessary.
|
---|
554 | /// </summary>
|
---|
555 | /// <param name="callbackLock">
|
---|
556 | /// Lock used to delay all events until after we exit lock section.
|
---|
557 | /// </param>
|
---|
558 | private void UpdateUserStates(CallbackLock callbackLock)
|
---|
559 | {
|
---|
560 | this.userStatesAccumulator.Clear();
|
---|
561 |
|
---|
562 | // Add states for tracked users
|
---|
563 | foreach (var trackingId in this.TrackedUserTrackingIds)
|
---|
564 | {
|
---|
565 | this.userStatesAccumulator.Add(trackingId, TrackedStateName);
|
---|
566 | }
|
---|
567 |
|
---|
568 | if (this.EngagedUserTrackingId != SharedConstants.InvalidUserTrackingId)
|
---|
569 | {
|
---|
570 | // Engaged state supersedes all other states
|
---|
571 | this.userStatesAccumulator[this.EngagedUserTrackingId] = EngagedStateName;
|
---|
572 | }
|
---|
573 |
|
---|
574 | if (this.HaveUserStatesChanged())
|
---|
575 | {
|
---|
576 | var temporaryMap = this.publicUserStates;
|
---|
577 | this.publicUserStates = this.userStatesAccumulator;
|
---|
578 | this.userStatesAccumulator = temporaryMap;
|
---|
579 |
|
---|
580 | var userStatesToSend = GetStateMappingEntryArray(this.publicUserStates);
|
---|
581 |
|
---|
582 | callbackLock.LockExit +=
|
---|
583 | () =>
|
---|
584 | this.SendUserStateChanged(
|
---|
585 | new UserStatesChangedEventMessage
|
---|
586 | {
|
---|
587 | category = EventCategory,
|
---|
588 | eventType = UserStatesChangedEventType,
|
---|
589 | userStates = userStatesToSend
|
---|
590 | });
|
---|
591 | }
|
---|
592 | }
|
---|
593 |
|
---|
594 | /// <summary>
|
---|
595 | /// Send UserStateChanged event if there are any subscribers.
|
---|
596 | /// </summary>
|
---|
597 | /// <param name="message">
|
---|
598 | /// Message to send.
|
---|
599 | /// </param>
|
---|
600 | private void SendUserStateChanged(EventMessage message)
|
---|
601 | {
|
---|
602 | if (this.UserStateChanged != null)
|
---|
603 | {
|
---|
604 | this.UserStateChanged(this, new UserStateChangedEventArgs(message));
|
---|
605 | }
|
---|
606 | }
|
---|
607 | }
|
---|
608 | }
|
---|