corrade-vassal – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | vero | 1 | /* |
2 | * Copyright (c) 2006-2014, openmetaverse.org |
||
3 | * All rights reserved. |
||
4 | * |
||
5 | * - Redistribution and use in source and binary forms, with or without |
||
6 | * modification, are permitted provided that the following conditions are met: |
||
7 | * |
||
8 | * - Redistributions of source code must retain the above copyright notice, this |
||
9 | * list of conditions and the following disclaimer. |
||
10 | * - Neither the name of the openmetaverse.org nor the names |
||
11 | * of its contributors may be used to endorse or promote products derived from |
||
12 | * this software without specific prior written permission. |
||
13 | * |
||
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
||
15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
||
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE |
||
18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
||
19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
||
20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
||
21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
||
22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||
23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
||
24 | * POSSIBILITY OF SUCH DAMAGE. |
||
25 | */ |
||
26 | //#define DEBUG_VOICE |
||
27 | |||
28 | using System; |
||
29 | using System.Collections.Generic; |
||
30 | using System.Linq; |
||
31 | using System.Text; |
||
32 | using System.IO; |
||
33 | using System.Threading; |
||
34 | |||
35 | using OpenMetaverse; |
||
36 | using OpenMetaverse.StructuredData; |
||
37 | |||
38 | namespace OpenMetaverse.Voice |
||
39 | { |
||
40 | public partial class VoiceGateway : IDisposable |
||
41 | { |
||
42 | // These states should be in increasing order of 'completeness' |
||
43 | // so that the (int) values can drive a progress bar. |
||
44 | public enum ConnectionState |
||
45 | { |
||
46 | None = 0, |
||
47 | Provisioned, |
||
48 | DaemonStarted, |
||
49 | DaemonConnected, |
||
50 | ConnectorConnected, |
||
51 | AccountLogin, |
||
52 | RegionCapAvailable, |
||
53 | SessionRunning |
||
54 | } |
||
55 | |||
56 | internal string sipServer = ""; |
||
57 | private string acctServer = "https://www.bhr.vivox.com/api2/"; |
||
58 | private string connectionHandle; |
||
59 | private string accountHandle; |
||
60 | private string sessionHandle; |
||
61 | |||
62 | // Parameters to Vivox daemon |
||
63 | private string slvoicePath = ""; |
||
64 | private string slvoiceArgs = "-ll 5"; |
||
65 | private string daemonNode = "127.0.0.1"; |
||
66 | private int daemonPort = 37331; |
||
67 | |||
68 | private string voiceUser; |
||
69 | private string voicePassword; |
||
70 | private string spatialUri; |
||
71 | private string spatialCredentials; |
||
72 | |||
73 | // Session management |
||
74 | private Dictionary<string, VoiceSession> sessions; |
||
75 | private VoiceSession spatialSession; |
||
76 | private Uri currentParcelCap; |
||
77 | private Uri nextParcelCap; |
||
78 | private string regionName; |
||
79 | |||
80 | // Position update thread |
||
81 | private Thread posThread; |
||
82 | private ManualResetEvent posRestart; |
||
83 | public GridClient Client; |
||
84 | private VoicePosition position; |
||
85 | private Vector3d oldPosition; |
||
86 | private Vector3d oldAt; |
||
87 | |||
88 | // Audio interfaces |
||
89 | private List<string> inputDevices; |
||
90 | /// <summary> |
||
91 | /// List of audio input devices |
||
92 | /// </summary> |
||
93 | public List<string> CaptureDevices { get { return inputDevices; } } |
||
94 | private List<string> outputDevices; |
||
95 | /// <summary> |
||
96 | /// List of audio output devices |
||
97 | /// </summary> |
||
98 | public List<string> PlaybackDevices { get { return outputDevices; } } |
||
99 | private string currentCaptureDevice; |
||
100 | private string currentPlaybackDevice; |
||
101 | private bool testing = false; |
||
102 | |||
103 | public event EventHandler OnSessionCreate; |
||
104 | public event EventHandler OnSessionRemove; |
||
105 | public delegate void VoiceConnectionChangeCallback(ConnectionState state); |
||
106 | public event VoiceConnectionChangeCallback OnVoiceConnectionChange; |
||
107 | public delegate void VoiceMicTestCallback(float level); |
||
108 | public event VoiceMicTestCallback OnVoiceMicTest; |
||
109 | |||
110 | public VoiceGateway(GridClient c) |
||
111 | { |
||
112 | Random rand = new Random(); |
||
113 | daemonPort = rand.Next(34000, 44000); |
||
114 | |||
115 | Client = c; |
||
116 | |||
117 | sessions = new Dictionary<string, VoiceSession>(); |
||
118 | position = new VoicePosition(); |
||
119 | position.UpOrientation = new Vector3d(0.0, 1.0, 0.0); |
||
120 | position.Velocity = new Vector3d(0.0, 0.0, 0.0); |
||
121 | oldPosition = new Vector3d(0, 0, 0); |
||
122 | oldAt = new Vector3d(1, 0, 0); |
||
123 | |||
124 | slvoiceArgs = " -ll -1"; // Min logging |
||
125 | slvoiceArgs += " -i 127.0.0.1:" + daemonPort.ToString(); |
||
126 | // slvoiceArgs += " -lf " + control.instance.ClientDir; |
||
127 | } |
||
128 | |||
129 | /// <summary> |
||
130 | /// Start up the Voice service. |
||
131 | /// </summary> |
||
132 | public void Start() |
||
133 | { |
||
134 | // Start the background thread |
||
135 | if (posThread != null && posThread.IsAlive) |
||
136 | posThread.Abort(); |
||
137 | posThread = new Thread(new ThreadStart(PositionThreadBody)); |
||
138 | posThread.Name = "VoicePositionUpdate"; |
||
139 | posThread.IsBackground = true; |
||
140 | posRestart = new ManualResetEvent(false); |
||
141 | posThread.Start(); |
||
142 | |||
143 | Client.Network.EventQueueRunning += new EventHandler<EventQueueRunningEventArgs>(Network_EventQueueRunning); |
||
144 | |||
145 | // Connection events |
||
146 | OnDaemonRunning += |
||
147 | new VoiceGateway.DaemonRunningCallback(connector_OnDaemonRunning); |
||
148 | OnDaemonCouldntRun += |
||
149 | new VoiceGateway.DaemonCouldntRunCallback(connector_OnDaemonCouldntRun); |
||
150 | OnConnectorCreateResponse += |
||
151 | new EventHandler<VoiceGateway.VoiceConnectorEventArgs>(connector_OnConnectorCreateResponse); |
||
152 | OnDaemonConnected += |
||
153 | new DaemonConnectedCallback(connector_OnDaemonConnected); |
||
154 | OnDaemonCouldntConnect += |
||
155 | new DaemonCouldntConnectCallback(connector_OnDaemonCouldntConnect); |
||
156 | OnAuxAudioPropertiesEvent += |
||
157 | new EventHandler<AudioPropertiesEventArgs>(connector_OnAuxAudioPropertiesEvent); |
||
158 | |||
159 | // Session events |
||
160 | OnSessionStateChangeEvent += |
||
161 | new EventHandler<SessionStateChangeEventArgs>(connector_OnSessionStateChangeEvent); |
||
162 | OnSessionAddedEvent += |
||
163 | new EventHandler<SessionAddedEventArgs>(connector_OnSessionAddedEvent); |
||
164 | |||
165 | // Session Participants events |
||
166 | OnSessionParticipantUpdatedEvent += |
||
167 | new EventHandler<ParticipantUpdatedEventArgs>(connector_OnSessionParticipantUpdatedEvent); |
||
168 | OnSessionParticipantAddedEvent += |
||
169 | new EventHandler<ParticipantAddedEventArgs>(connector_OnSessionParticipantAddedEvent); |
||
170 | |||
171 | // Device events |
||
172 | OnAuxGetCaptureDevicesResponse += |
||
173 | new EventHandler<VoiceDevicesEventArgs>(connector_OnAuxGetCaptureDevicesResponse); |
||
174 | OnAuxGetRenderDevicesResponse += |
||
175 | new EventHandler<VoiceDevicesEventArgs>(connector_OnAuxGetRenderDevicesResponse); |
||
176 | |||
177 | // Generic status response |
||
178 | OnVoiceResponse += new EventHandler<VoiceResponseEventArgs>(connector_OnVoiceResponse); |
||
179 | |||
180 | // Account events |
||
181 | OnAccountLoginResponse += |
||
182 | new EventHandler<VoiceAccountEventArgs>(connector_OnAccountLoginResponse); |
||
183 | |||
184 | Logger.Log("Voice initialized", Helpers.LogLevel.Info); |
||
185 | |||
186 | // If voice provisioning capability is already available, |
||
187 | // proceed with voice startup. Otherwise the EventQueueRunning |
||
188 | // event will do it. |
||
189 | System.Uri vCap = |
||
190 | Client.Network.CurrentSim.Caps.CapabilityURI("ProvisionVoiceAccountRequest"); |
||
191 | if (vCap != null) |
||
192 | RequestVoiceProvision(vCap); |
||
193 | |||
194 | } |
||
195 | |||
196 | /// <summary> |
||
197 | /// Handle miscellaneous request status |
||
198 | /// </summary> |
||
199 | /// <param name="sender"></param> |
||
200 | /// <param name="e"></param> |
||
201 | /// ///<remarks>If something goes wrong, we log it.</remarks> |
||
202 | void connector_OnVoiceResponse(object sender, VoiceGateway.VoiceResponseEventArgs e) |
||
203 | { |
||
204 | if (e.StatusCode == 0) |
||
205 | return; |
||
206 | |||
207 | Logger.Log(e.Message + " on " + sender as string, Helpers.LogLevel.Error); |
||
208 | } |
||
209 | |||
210 | public void Stop() |
||
211 | { |
||
212 | Client.Network.EventQueueRunning -= new EventHandler<EventQueueRunningEventArgs>(Network_EventQueueRunning); |
||
213 | |||
214 | // Connection events |
||
215 | OnDaemonRunning -= |
||
216 | new VoiceGateway.DaemonRunningCallback(connector_OnDaemonRunning); |
||
217 | OnDaemonCouldntRun -= |
||
218 | new VoiceGateway.DaemonCouldntRunCallback(connector_OnDaemonCouldntRun); |
||
219 | OnConnectorCreateResponse -= |
||
220 | new EventHandler<VoiceGateway.VoiceConnectorEventArgs>(connector_OnConnectorCreateResponse); |
||
221 | OnDaemonConnected -= |
||
222 | new VoiceGateway.DaemonConnectedCallback(connector_OnDaemonConnected); |
||
223 | OnDaemonCouldntConnect -= |
||
224 | new VoiceGateway.DaemonCouldntConnectCallback(connector_OnDaemonCouldntConnect); |
||
225 | OnAuxAudioPropertiesEvent -= |
||
226 | new EventHandler<AudioPropertiesEventArgs>(connector_OnAuxAudioPropertiesEvent); |
||
227 | |||
228 | // Session events |
||
229 | OnSessionStateChangeEvent -= |
||
230 | new EventHandler<SessionStateChangeEventArgs>(connector_OnSessionStateChangeEvent); |
||
231 | OnSessionAddedEvent -= |
||
232 | new EventHandler<SessionAddedEventArgs>(connector_OnSessionAddedEvent); |
||
233 | |||
234 | // Session Participants events |
||
235 | OnSessionParticipantUpdatedEvent -= |
||
236 | new EventHandler<ParticipantUpdatedEventArgs>(connector_OnSessionParticipantUpdatedEvent); |
||
237 | OnSessionParticipantAddedEvent -= |
||
238 | new EventHandler<ParticipantAddedEventArgs>(connector_OnSessionParticipantAddedEvent); |
||
239 | OnSessionParticipantRemovedEvent -= |
||
240 | new EventHandler<ParticipantRemovedEventArgs>(connector_OnSessionParticipantRemovedEvent); |
||
241 | |||
242 | // Tuning events |
||
243 | OnAuxGetCaptureDevicesResponse -= |
||
244 | new EventHandler<VoiceGateway.VoiceDevicesEventArgs>(connector_OnAuxGetCaptureDevicesResponse); |
||
245 | OnAuxGetRenderDevicesResponse -= |
||
246 | new EventHandler<VoiceGateway.VoiceDevicesEventArgs>(connector_OnAuxGetRenderDevicesResponse); |
||
247 | |||
248 | // Account events |
||
249 | OnAccountLoginResponse -= |
||
250 | new EventHandler<VoiceGateway.VoiceAccountEventArgs>(connector_OnAccountLoginResponse); |
||
251 | |||
252 | // Stop the background thread |
||
253 | if (posThread != null) |
||
254 | { |
||
255 | PosUpdating(false); |
||
256 | |||
257 | if (posThread.IsAlive) |
||
258 | posThread.Abort(); |
||
259 | posThread = null; |
||
260 | } |
||
261 | |||
262 | // Close all sessions |
||
263 | foreach (VoiceSession s in sessions.Values) |
||
264 | { |
||
265 | if (OnSessionRemove != null) |
||
266 | OnSessionRemove(s, EventArgs.Empty); |
||
267 | s.Close(); |
||
268 | } |
||
269 | |||
270 | // Clear out lots of state so in case of restart we begin at the beginning. |
||
271 | currentParcelCap = null; |
||
272 | sessions.Clear(); |
||
273 | accountHandle = null; |
||
274 | voiceUser = null; |
||
275 | voicePassword = null; |
||
276 | |||
277 | SessionTerminate(sessionHandle); |
||
278 | sessionHandle = null; |
||
279 | AccountLogout(accountHandle); |
||
280 | accountHandle = null; |
||
281 | ConnectorInitiateShutdown(connectionHandle); |
||
282 | connectionHandle = null; |
||
283 | StopDaemon(); |
||
284 | } |
||
285 | |||
286 | /// <summary> |
||
287 | /// Cleanup oject resources |
||
288 | /// </summary> |
||
289 | public void Dispose() |
||
290 | { |
||
291 | Stop(); |
||
292 | } |
||
293 | |||
294 | internal string GetVoiceDaemonPath() |
||
295 | { |
||
296 | string myDir = |
||
297 | Path.GetDirectoryName( |
||
298 | (System.Reflection.Assembly.GetEntryAssembly() ?? typeof (VoiceGateway).Assembly).Location); |
||
299 | |||
300 | if (Environment.OSVersion.Platform != PlatformID.MacOSX && |
||
301 | Environment.OSVersion.Platform != PlatformID.Unix) |
||
302 | { |
||
303 | string localDaemon = Path.Combine(myDir, Path.Combine("voice", "SLVoice.exe")); |
||
304 | |||
305 | if (File.Exists(localDaemon)) |
||
306 | return localDaemon; |
||
307 | |||
308 | string progFiles; |
||
309 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ProgramFiles(x86)"))) |
||
310 | { |
||
311 | progFiles = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); |
||
312 | } |
||
313 | else |
||
314 | { |
||
315 | progFiles = Environment.GetEnvironmentVariable("ProgramFiles"); |
||
316 | } |
||
317 | |||
318 | if (System.IO.File.Exists(Path.Combine(progFiles, @"SecondLife" + Path.DirectorySeparatorChar + @"SLVoice.exe"))) |
||
319 | { |
||
320 | return Path.Combine(progFiles, @"SecondLife" + Path.DirectorySeparatorChar + @"SLVoice.exe"); |
||
321 | } |
||
322 | |||
323 | return Path.Combine(myDir, @"SLVoice.exe"); |
||
324 | |||
325 | } |
||
326 | else |
||
327 | { |
||
328 | string localDaemon = Path.Combine(myDir, Path.Combine("voice", "SLVoice")); |
||
329 | |||
330 | if (File.Exists(localDaemon)) |
||
331 | return localDaemon; |
||
332 | |||
333 | return Path.Combine(myDir,"SLVoice"); |
||
334 | } |
||
335 | } |
||
336 | |||
337 | void RequestVoiceProvision(System.Uri cap) |
||
338 | { |
||
339 | OpenMetaverse.Http.CapsClient capClient = |
||
340 | new OpenMetaverse.Http.CapsClient(cap); |
||
341 | capClient.OnComplete += |
||
342 | new OpenMetaverse.Http.CapsClient.CompleteCallback(cClient_OnComplete); |
||
343 | OSD postData = new OSD(); |
||
344 | |||
345 | // STEP 0 |
||
346 | Logger.Log("Requesting voice capability", Helpers.LogLevel.Info); |
||
347 | capClient.BeginGetResponse(postData, OSDFormat.Xml, 10000); |
||
348 | } |
||
349 | |||
350 | /// <summary> |
||
351 | /// Request voice cap when changing regions |
||
352 | /// </summary> |
||
353 | void Network_EventQueueRunning(object sender, EventQueueRunningEventArgs e) |
||
354 | { |
||
355 | // We only care about the sim we are in. |
||
356 | if (e.Simulator != Client.Network.CurrentSim) |
||
357 | return; |
||
358 | |||
359 | // Did we provision voice login info? |
||
360 | if (string.IsNullOrEmpty(voiceUser)) |
||
361 | { |
||
362 | // The startup steps are |
||
363 | // 0. Get voice account info |
||
364 | // 1. Start Daemon |
||
365 | // 2. Create TCP connection |
||
366 | // 3. Create Connector |
||
367 | // 4. Account login |
||
368 | // 5. Create session |
||
369 | |||
370 | // Get the voice provisioning data |
||
371 | System.Uri vCap = |
||
372 | Client.Network.CurrentSim.Caps.CapabilityURI("ProvisionVoiceAccountRequest"); |
||
373 | |||
374 | // Do we have voice capability? |
||
375 | if (vCap == null) |
||
376 | { |
||
377 | Logger.Log("Null voice capability after event queue running", Helpers.LogLevel.Warning); |
||
378 | } |
||
379 | else |
||
380 | { |
||
381 | RequestVoiceProvision(vCap); |
||
382 | } |
||
383 | |||
384 | return; |
||
385 | } |
||
386 | else |
||
387 | { |
||
388 | // Change voice session for this region. |
||
389 | ParcelChanged(); |
||
390 | } |
||
391 | } |
||
392 | |||
393 | |||
394 | #region Participants |
||
395 | |||
396 | void connector_OnSessionParticipantUpdatedEvent(object sender, ParticipantUpdatedEventArgs e) |
||
397 | { |
||
398 | VoiceSession s = FindSession(e.SessionHandle, false); |
||
399 | if (s == null) return; |
||
400 | s.ParticipantUpdate(e.URI, e.IsMuted, e.IsSpeaking, e.Volume, e.Energy); |
||
401 | } |
||
402 | |||
403 | public string SIPFromUUID(UUID id) |
||
404 | { |
||
405 | return "sip:" + |
||
406 | nameFromID(id) + |
||
407 | "@" + |
||
408 | sipServer; |
||
409 | } |
||
410 | |||
411 | private static string nameFromID(UUID id) |
||
412 | { |
||
413 | string result = null; |
||
414 | |||
415 | if (id == UUID.Zero) |
||
416 | return result; |
||
417 | |||
418 | // Prepending this apparently prevents conflicts with reserved names inside the vivox and diamondware code. |
||
419 | result = "x"; |
||
420 | |||
421 | // Base64 encode and replace the pieces of base64 that are less compatible |
||
422 | // with e-mail local-parts. |
||
423 | // See RFC-4648 "Base 64 Encoding with URL and Filename Safe Alphabet" |
||
424 | byte[] encbuff = id.GetBytes(); |
||
425 | result += Convert.ToBase64String(encbuff); |
||
426 | result = result.Replace('+', '-'); |
||
427 | result = result.Replace('/', '_'); |
||
428 | |||
429 | return result; |
||
430 | } |
||
431 | |||
432 | void connector_OnSessionParticipantAddedEvent(object sender, ParticipantAddedEventArgs e) |
||
433 | { |
||
434 | VoiceSession s = FindSession(e.SessionHandle, false); |
||
435 | if (s == null) |
||
436 | { |
||
437 | Logger.Log("Orphan participant", Helpers.LogLevel.Error); |
||
438 | return; |
||
439 | } |
||
440 | s.AddParticipant(e.URI); |
||
441 | } |
||
442 | |||
443 | void connector_OnSessionParticipantRemovedEvent(object sender, ParticipantRemovedEventArgs e) |
||
444 | { |
||
445 | VoiceSession s = FindSession(e.SessionHandle, false); |
||
446 | if (s == null) return; |
||
447 | s.RemoveParticipant(e.URI); |
||
448 | } |
||
449 | #endregion |
||
450 | |||
451 | #region Sessions |
||
452 | void connector_OnSessionAddedEvent(object sender, SessionAddedEventArgs e) |
||
453 | { |
||
454 | sessionHandle = e.SessionHandle; |
||
455 | |||
456 | // Create our session context. |
||
457 | VoiceSession s = FindSession(sessionHandle, true); |
||
458 | s.RegionName = regionName; |
||
459 | |||
460 | spatialSession = s; |
||
461 | |||
462 | // Tell any user-facing code. |
||
463 | if (OnSessionCreate != null) |
||
464 | OnSessionCreate(s, null); |
||
465 | |||
466 | Logger.Log("Added voice session in " + regionName, Helpers.LogLevel.Info); |
||
467 | } |
||
468 | |||
469 | /// <summary> |
||
470 | /// Handle a change in session state |
||
471 | /// </summary> |
||
472 | void connector_OnSessionStateChangeEvent(object sender, SessionStateChangeEventArgs e) |
||
473 | { |
||
474 | VoiceSession s; |
||
475 | |||
476 | switch (e.State) |
||
477 | { |
||
478 | case VoiceGateway.SessionState.Connected: |
||
479 | s = FindSession(e.SessionHandle, true); |
||
480 | sessionHandle = e.SessionHandle; |
||
481 | s.RegionName = regionName; |
||
482 | spatialSession = s; |
||
483 | |||
484 | Logger.Log("Voice connected in " + regionName, Helpers.LogLevel.Info); |
||
485 | // Tell any user-facing code. |
||
486 | if (OnSessionCreate != null) |
||
487 | OnSessionCreate(s, null); |
||
488 | break; |
||
489 | |||
490 | case VoiceGateway.SessionState.Disconnected: |
||
491 | s = FindSession(sessionHandle, false); |
||
492 | sessions.Remove(sessionHandle); |
||
493 | |||
494 | if (s != null) |
||
495 | { |
||
496 | Logger.Log("Voice disconnected in " + s.RegionName, Helpers.LogLevel.Info); |
||
497 | |||
498 | // Inform interested parties |
||
499 | if (OnSessionRemove != null) |
||
500 | OnSessionRemove(s, null); |
||
501 | |||
502 | if (s == spatialSession) |
||
503 | spatialSession = null; |
||
504 | } |
||
505 | |||
506 | // The previous session is now ended. Check for a new one and |
||
507 | // start it going. |
||
508 | if (nextParcelCap != null) |
||
509 | { |
||
510 | currentParcelCap = nextParcelCap; |
||
511 | nextParcelCap = null; |
||
512 | RequestParcelInfo(currentParcelCap); |
||
513 | } |
||
514 | break; |
||
515 | } |
||
516 | |||
517 | |||
518 | } |
||
519 | |||
520 | /// <summary> |
||
521 | /// Close a voice session |
||
522 | /// </summary> |
||
523 | /// <param name="sessionHandle"></param> |
||
524 | internal void CloseSession(string sessionHandle) |
||
525 | { |
||
526 | if (!sessions.ContainsKey(sessionHandle)) |
||
527 | return; |
||
528 | |||
529 | PosUpdating(false); |
||
530 | ReportConnectionState(ConnectionState.AccountLogin); |
||
531 | |||
532 | // Clean up spatial pointers. |
||
533 | VoiceSession s = sessions[sessionHandle]; |
||
534 | if (s.IsSpatial) |
||
535 | { |
||
536 | spatialSession = null; |
||
537 | currentParcelCap = null; |
||
538 | } |
||
539 | |||
540 | // Remove this session from the master session list |
||
541 | sessions.Remove(sessionHandle); |
||
542 | |||
543 | // Let any user-facing code clean up. |
||
544 | if (OnSessionRemove != null) |
||
545 | OnSessionRemove(s, null); |
||
546 | |||
547 | // Tell SLVoice to clean it up as well. |
||
548 | SessionTerminate(sessionHandle); |
||
549 | } |
||
550 | |||
551 | /// <summary> |
||
552 | /// Locate a Session context from its handle |
||
553 | /// </summary> |
||
554 | /// <remarks>Creates the session context if it does not exist.</remarks> |
||
555 | VoiceSession FindSession(string sessionHandle, bool make) |
||
556 | { |
||
557 | if (sessions.ContainsKey(sessionHandle)) |
||
558 | return sessions[sessionHandle]; |
||
559 | |||
560 | if (!make) return null; |
||
561 | |||
562 | // Create a new session and add it to the sessions list. |
||
563 | VoiceSession s = new VoiceSession(this, sessionHandle); |
||
564 | |||
565 | // Turn on position updating for spatial sessions |
||
566 | // (For now, only spatial sessions are supported) |
||
567 | if (s.IsSpatial) |
||
568 | PosUpdating(true); |
||
569 | |||
570 | // Register the session by its handle |
||
571 | sessions.Add(sessionHandle, s); |
||
572 | return s; |
||
573 | } |
||
574 | |||
575 | #endregion |
||
576 | |||
577 | #region MinorResponses |
||
578 | |||
579 | void connector_OnAuxAudioPropertiesEvent(object sender, AudioPropertiesEventArgs e) |
||
580 | { |
||
581 | if (OnVoiceMicTest != null) |
||
582 | OnVoiceMicTest(e.MicEnergy); |
||
583 | } |
||
584 | |||
585 | #endregion |
||
586 | |||
587 | private void ReportConnectionState(ConnectionState s) |
||
588 | { |
||
589 | if (OnVoiceConnectionChange == null) return; |
||
590 | |||
591 | OnVoiceConnectionChange(s); |
||
592 | } |
||
593 | |||
594 | /// <summary> |
||
595 | /// Handle completion of main voice cap request. |
||
596 | /// </summary> |
||
597 | /// <param name="client"></param> |
||
598 | /// <param name="result"></param> |
||
599 | /// <param name="error"></param> |
||
600 | void cClient_OnComplete(OpenMetaverse.Http.CapsClient client, |
||
601 | OpenMetaverse.StructuredData.OSD result, |
||
602 | Exception error) |
||
603 | { |
||
604 | if (error != null) |
||
605 | { |
||
606 | Logger.Log("Voice cap error " + error.Message, Helpers.LogLevel.Error); |
||
607 | return; |
||
608 | } |
||
609 | |||
610 | Logger.Log("Voice provisioned", Helpers.LogLevel.Info); |
||
611 | ReportConnectionState(ConnectionState.Provisioned); |
||
612 | |||
613 | OpenMetaverse.StructuredData.OSDMap pMap = result as OpenMetaverse.StructuredData.OSDMap; |
||
614 | |||
615 | // We can get back 4 interesting values: |
||
616 | // voice_sip_uri_hostname |
||
617 | // voice_account_server_name (actually a full URI) |
||
618 | // username |
||
619 | // password |
||
620 | if (pMap.ContainsKey("voice_sip_uri_hostname")) |
||
621 | sipServer = pMap["voice_sip_uri_hostname"].AsString(); |
||
622 | if (pMap.ContainsKey("voice_account_server_name")) |
||
623 | acctServer = pMap["voice_account_server_name"].AsString(); |
||
624 | voiceUser = pMap["username"].AsString(); |
||
625 | voicePassword = pMap["password"].AsString(); |
||
626 | |||
627 | // Start the SLVoice daemon |
||
628 | slvoicePath = GetVoiceDaemonPath(); |
||
629 | |||
630 | // Test if the executable exists |
||
631 | if (!System.IO.File.Exists(slvoicePath)) |
||
632 | { |
||
633 | Logger.Log("SLVoice is missing", Helpers.LogLevel.Error); |
||
634 | return; |
||
635 | } |
||
636 | |||
637 | // STEP 1 |
||
638 | StartDaemon(slvoicePath, slvoiceArgs); |
||
639 | } |
||
640 | |||
641 | #region Daemon |
||
642 | void connector_OnDaemonCouldntConnect() |
||
643 | { |
||
644 | Logger.Log("No voice daemon connect", Helpers.LogLevel.Error); |
||
645 | } |
||
646 | |||
647 | void connector_OnDaemonCouldntRun() |
||
648 | { |
||
649 | Logger.Log("Daemon not started", Helpers.LogLevel.Error); |
||
650 | } |
||
651 | |||
652 | /// <summary> |
||
653 | /// Daemon has started so connect to it. |
||
654 | /// </summary> |
||
655 | void connector_OnDaemonRunning() |
||
656 | { |
||
657 | OnDaemonRunning -= |
||
658 | new VoiceGateway.DaemonRunningCallback(connector_OnDaemonRunning); |
||
659 | |||
660 | Logger.Log("Daemon started", Helpers.LogLevel.Info); |
||
661 | ReportConnectionState(ConnectionState.DaemonStarted); |
||
662 | |||
663 | // STEP 2 |
||
664 | ConnectToDaemon(daemonNode, daemonPort); |
||
665 | |||
666 | } |
||
667 | |||
668 | /// <summary> |
||
669 | /// The daemon TCP connection is open. |
||
670 | /// </summary> |
||
671 | void connector_OnDaemonConnected() |
||
672 | { |
||
673 | Logger.Log("Daemon connected", Helpers.LogLevel.Info); |
||
674 | ReportConnectionState(ConnectionState.DaemonConnected); |
||
675 | |||
676 | // The connector is what does the logging. |
||
677 | VoiceGateway.VoiceLoggingSettings vLog = |
||
678 | new VoiceGateway.VoiceLoggingSettings(); |
||
679 | |||
680 | #if DEBUG_VOICE |
||
681 | vLog.Enabled = true; |
||
682 | vLog.FileNamePrefix = "OpenmetaverseVoice"; |
||
683 | vLog.FileNameSuffix = ".log"; |
||
684 | vLog.LogLevel = 4; |
||
685 | #endif |
||
686 | // STEP 3 |
||
687 | int reqId = ConnectorCreate( |
||
688 | "V2 SDK", // Magic value keeps SLVoice happy |
||
689 | acctServer, // Account manager server |
||
690 | 30000, 30099, // port range |
||
691 | vLog); |
||
692 | if (reqId < 0) |
||
693 | { |
||
694 | Logger.Log("No voice connector request", Helpers.LogLevel.Error); |
||
695 | } |
||
696 | } |
||
697 | |||
698 | /// <summary> |
||
699 | /// Handle creation of the Connector. |
||
700 | /// </summary> |
||
701 | void connector_OnConnectorCreateResponse( |
||
702 | object sender, |
||
703 | VoiceGateway.VoiceConnectorEventArgs e) |
||
704 | { |
||
705 | Logger.Log("Voice daemon protocol started " + e.Message, Helpers.LogLevel.Info); |
||
706 | |||
707 | connectionHandle = e.Handle; |
||
708 | |||
709 | if (e.StatusCode != 0) |
||
710 | return; |
||
711 | |||
712 | // STEP 4 |
||
713 | AccountLogin( |
||
714 | connectionHandle, |
||
715 | voiceUser, |
||
716 | voicePassword, |
||
717 | "VerifyAnswer", // This can also be "AutoAnswer" |
||
718 | "", // Default account management server URI |
||
719 | 10, // Throttle state changes |
||
720 | true); // Enable buddies and presence |
||
721 | } |
||
722 | #endregion |
||
723 | |||
724 | void connector_OnAccountLoginResponse( |
||
725 | object sender, |
||
726 | VoiceGateway.VoiceAccountEventArgs e) |
||
727 | { |
||
728 | Logger.Log("Account Login " + e.Message, Helpers.LogLevel.Info); |
||
729 | accountHandle = e.AccountHandle; |
||
730 | ReportConnectionState(ConnectionState.AccountLogin); |
||
731 | ParcelChanged(); |
||
732 | } |
||
733 | |||
734 | #region Audio devices |
||
735 | /// <summary> |
||
736 | /// Handle response to audio output device query |
||
737 | /// </summary> |
||
738 | void connector_OnAuxGetRenderDevicesResponse( |
||
739 | object sender, |
||
740 | VoiceGateway.VoiceDevicesEventArgs e) |
||
741 | { |
||
742 | outputDevices = e.Devices; |
||
743 | currentPlaybackDevice = e.CurrentDevice; |
||
744 | } |
||
745 | |||
746 | /// <summary> |
||
747 | /// Handle response to audio input device query |
||
748 | /// </summary> |
||
749 | void connector_OnAuxGetCaptureDevicesResponse( |
||
750 | object sender, |
||
751 | VoiceGateway.VoiceDevicesEventArgs e) |
||
752 | { |
||
753 | inputDevices = e.Devices; |
||
754 | currentCaptureDevice = e.CurrentDevice; |
||
755 | } |
||
756 | |||
757 | public string CurrentCaptureDevice |
||
758 | { |
||
759 | get { return currentCaptureDevice; } |
||
760 | set |
||
761 | { |
||
762 | currentCaptureDevice = value; |
||
763 | AuxSetCaptureDevice(value); |
||
764 | } |
||
765 | } |
||
766 | public string PlaybackDevice |
||
767 | { |
||
768 | get { return currentPlaybackDevice; } |
||
769 | set |
||
770 | { |
||
771 | currentPlaybackDevice = value; |
||
772 | AuxSetRenderDevice(value); |
||
773 | } |
||
774 | } |
||
775 | |||
776 | public int MicLevel |
||
777 | { |
||
778 | set |
||
779 | { |
||
780 | ConnectorSetLocalMicVolume(connectionHandle, value); |
||
781 | } |
||
782 | } |
||
783 | public int SpkrLevel |
||
784 | { |
||
785 | set |
||
786 | { |
||
787 | ConnectorSetLocalSpeakerVolume(connectionHandle, value); |
||
788 | } |
||
789 | } |
||
790 | |||
791 | public bool MicMute |
||
792 | { |
||
793 | set |
||
794 | { |
||
795 | ConnectorMuteLocalMic(connectionHandle, value); |
||
796 | } |
||
797 | } |
||
798 | |||
799 | public bool SpkrMute |
||
800 | { |
||
801 | set |
||
802 | { |
||
803 | ConnectorMuteLocalSpeaker(connectionHandle, value); |
||
804 | } |
||
805 | } |
||
806 | |||
807 | /// <summary> |
||
808 | /// Set audio test mode |
||
809 | /// </summary> |
||
810 | public bool TestMode |
||
811 | { |
||
812 | get { return testing; } |
||
813 | set |
||
814 | { |
||
815 | testing = value; |
||
816 | if (testing) |
||
817 | { |
||
818 | if (spatialSession != null) |
||
819 | { |
||
820 | spatialSession.Close(); |
||
821 | spatialSession = null; |
||
822 | } |
||
823 | AuxCaptureAudioStart(0); |
||
824 | } |
||
825 | else |
||
826 | { |
||
827 | AuxCaptureAudioStop(); |
||
828 | ParcelChanged(); |
||
829 | } |
||
830 | } |
||
831 | } |
||
832 | #endregion |
||
833 | |||
834 | |||
835 | |||
836 | |||
837 | /// <summary> |
||
838 | /// Set voice channel for new parcel |
||
839 | /// </summary> |
||
840 | /// |
||
841 | internal void ParcelChanged() |
||
842 | { |
||
843 | // Get the capability for this parcel. |
||
844 | Caps c = Client.Network.CurrentSim.Caps; |
||
845 | System.Uri pCap = c.CapabilityURI("ParcelVoiceInfoRequest"); |
||
846 | |||
847 | if (pCap == null) |
||
848 | { |
||
849 | Logger.Log("Null voice capability", Helpers.LogLevel.Error); |
||
850 | return; |
||
851 | } |
||
852 | |||
853 | // Parcel has changed. If we were already in a spatial session, we have to close it first. |
||
854 | if (spatialSession != null) |
||
855 | { |
||
856 | nextParcelCap = pCap; |
||
857 | CloseSession(spatialSession.Handle); |
||
858 | } |
||
859 | |||
860 | // Not already in a session, so can start the new one. |
||
861 | RequestParcelInfo(pCap); |
||
862 | } |
||
863 | |||
864 | private OpenMetaverse.Http.CapsClient parcelCap; |
||
865 | |||
866 | /// <summary> |
||
867 | /// Request info from a parcel capability Uri. |
||
868 | /// </summary> |
||
869 | /// <param name="cap"></param> |
||
870 | |||
871 | void RequestParcelInfo(Uri cap) |
||
872 | { |
||
873 | Logger.Log("Requesting region voice info", Helpers.LogLevel.Info); |
||
874 | |||
875 | parcelCap = new OpenMetaverse.Http.CapsClient(cap); |
||
876 | parcelCap.OnComplete += |
||
877 | new OpenMetaverse.Http.CapsClient.CompleteCallback(pCap_OnComplete); |
||
878 | OSD postData = new OSD(); |
||
879 | |||
880 | currentParcelCap = cap; |
||
881 | parcelCap.BeginGetResponse(postData, OSDFormat.Xml, 10000); |
||
882 | } |
||
883 | |||
884 | /// <summary> |
||
885 | /// Receive parcel voice cap |
||
886 | /// </summary> |
||
887 | /// <param name="client"></param> |
||
888 | /// <param name="result"></param> |
||
889 | /// <param name="error"></param> |
||
890 | void pCap_OnComplete(OpenMetaverse.Http.CapsClient client, |
||
891 | OpenMetaverse.StructuredData.OSD result, |
||
892 | Exception error) |
||
893 | { |
||
894 | parcelCap.OnComplete -= |
||
895 | new OpenMetaverse.Http.CapsClient.CompleteCallback(pCap_OnComplete); |
||
896 | parcelCap = null; |
||
897 | |||
898 | if (error != null) |
||
899 | { |
||
900 | Logger.Log("Region voice cap " + error.Message, Helpers.LogLevel.Error); |
||
901 | return; |
||
902 | } |
||
903 | |||
904 | OpenMetaverse.StructuredData.OSDMap pMap = result as OpenMetaverse.StructuredData.OSDMap; |
||
905 | |||
906 | regionName = pMap["region_name"].AsString(); |
||
907 | ReportConnectionState(ConnectionState.RegionCapAvailable); |
||
908 | |||
909 | if (pMap.ContainsKey("voice_credentials")) |
||
910 | { |
||
911 | OpenMetaverse.StructuredData.OSDMap cred = |
||
912 | pMap["voice_credentials"] as OpenMetaverse.StructuredData.OSDMap; |
||
913 | |||
914 | if (cred.ContainsKey("channel_uri")) |
||
915 | spatialUri = cred["channel_uri"].AsString(); |
||
916 | if (cred.ContainsKey("channel_credentials")) |
||
917 | spatialCredentials = cred["channel_credentials"].AsString(); |
||
918 | } |
||
919 | |||
920 | if (spatialUri == null || spatialUri == "") |
||
921 | { |
||
922 | // "No voice chat allowed here"); |
||
923 | return; |
||
924 | } |
||
925 | |||
926 | Logger.Log("Voice connecting for region " + regionName, Helpers.LogLevel.Info); |
||
927 | |||
928 | // STEP 5 |
||
929 | int reqId = SessionCreate( |
||
930 | accountHandle, |
||
931 | spatialUri, // uri |
||
932 | "", // Channel name seems to be always null |
||
933 | spatialCredentials, // spatialCredentials, // session password |
||
934 | true, // Join Audio |
||
935 | false, // Join Text |
||
936 | ""); |
||
937 | if (reqId < 0) |
||
938 | { |
||
939 | Logger.Log("Voice Session ReqID " + reqId.ToString(), Helpers.LogLevel.Error); |
||
940 | } |
||
941 | } |
||
942 | |||
943 | #region Location Update |
||
944 | /// <summary> |
||
945 | /// Tell Vivox where we are standing |
||
946 | /// </summary> |
||
947 | /// <remarks>This has to be called when we move or turn.</remarks> |
||
948 | internal void UpdatePosition(AgentManager self) |
||
949 | { |
||
950 | // Get position in Global coordinates |
||
951 | Vector3d OMVpos = new Vector3d(self.GlobalPosition); |
||
952 | |||
953 | // Do not send trivial updates. |
||
954 | if (OMVpos.ApproxEquals(oldPosition, 1.0)) |
||
955 | return; |
||
956 | |||
957 | oldPosition = OMVpos; |
||
958 | |||
959 | // Convert to the coordinate space that Vivox uses |
||
960 | // OMV X is East, Y is North, Z is up |
||
961 | // VVX X is East, Y is up, Z is South |
||
962 | position.Position = new Vector3d(OMVpos.X, OMVpos.Z, -OMVpos.Y); |
||
963 | |||
964 | // TODO Rotate these two vectors |
||
965 | |||
966 | // Get azimuth from the facing Quaternion. |
||
967 | // By definition, facing.W = Cos( angle/2 ) |
||
968 | double angle = 2.0 * Math.Acos(self.Movement.BodyRotation.W); |
||
969 | |||
970 | position.LeftOrientation = new Vector3d(-1.0, 0.0, 0.0); |
||
971 | position.AtOrientation = new Vector3d((float)Math.Acos(angle), 0.0, -(float)Math.Asin(angle)); |
||
972 | |||
973 | SessionSet3DPosition( |
||
974 | sessionHandle, |
||
975 | position, |
||
976 | position); |
||
977 | } |
||
978 | |||
979 | /// <summary> |
||
980 | /// Start and stop updating out position. |
||
981 | /// </summary> |
||
982 | /// <param name="go"></param> |
||
983 | internal void PosUpdating(bool go) |
||
984 | { |
||
985 | if (go) |
||
986 | posRestart.Set(); |
||
987 | else |
||
988 | posRestart.Reset(); |
||
989 | } |
||
990 | |||
991 | private void PositionThreadBody() |
||
992 | { |
||
993 | while (true) |
||
994 | { |
||
995 | posRestart.WaitOne(); |
||
996 | Thread.Sleep(1500); |
||
997 | UpdatePosition(Client.Self); |
||
998 | } |
||
999 | } |
||
1000 | #endregion |
||
1001 | |||
1002 | } |
||
1003 | } |