Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 55 additions & 28 deletions SteamLinkVRCFTModule/SteamLinkVRCFTModule/OSCHandler.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using VRCFaceTracking;
using VRCFaceTracking.Core.OSC;
using VRCFaceTracking.Core.Params.Expressions;
using VRCFaceTracking.OSC;
using System.Buffers.Binary;
using System.Globalization;

namespace SteamLinkVRCFTModule
{
Expand Down Expand Up @@ -170,17 +160,19 @@ private static readonly
{"/sl/xrfb/facew/TongueOut", new List<UnifiedExpressions>{TongueOut} },
};


public bool initialized = false;

private Socket _receiver;
private bool _loop = true;
private readonly Thread _thread;
private bool _communicating = false;
private readonly Thread? _thread;
private readonly ILogger _logger;
private readonly int _resolvedPort;
private const int DEFAULT_PORT = 9015;
private const int TIMEOUT_MS = 10_000;
private const int INITIAL_TIMEOUT_MS = 180_000;

public OSCHandler(ILogger iLogger, int? port = null)
public OSCHandler(CancellationToken ct, ILogger iLogger, int? port = null)
{
_logger = iLogger;
if (_receiver != null)
Expand All @@ -191,29 +183,52 @@ public OSCHandler(ILogger iLogger, int? port = null)

_receiver = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
_resolvedPort = port ?? DEFAULT_PORT;
_receiver.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), _resolvedPort));
_receiver.ReceiveTimeout = TIMEOUT_MS;
try
{
_receiver.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), _resolvedPort));
}
catch (Exception ex) {
_logger.LogError(ex.ToString());
_logger.LogError($"OSCHandler failed to bind to port {_resolvedPort}");
return;
}
_receiver.ReceiveTimeout = TIMEOUT_MS;

_loop = true;
_thread = new Thread(new ThreadStart(ListenLoop));
_thread.Start();
// 1 second delay to hopefully fix any race conditions on thread initalization
_thread.Start();

// this is stupid but I'm stupid
Task t = Task.Run(() => {
_logger.LogInformation($"Waiting {INITIAL_TIMEOUT_MS / 1000.0} seconds for message from SteamLink on port {_resolvedPort}");
while (!_communicating)
{
Task.Delay(100).Wait();
}
_logger.LogInformation($"Received data from SteamLink on port {_resolvedPort}!");
});
bool result = t.Wait(INITIAL_TIMEOUT_MS, ct);
if (!result)
{
_logger.LogError($"Initialization Timeout. Failed to Receive Message from SteamLink on port {_resolvedPort} within {INITIAL_TIMEOUT_MS / 1000.0} seconds.");
// leave Module to initiate teardown
}

initialized = result;
// 1 second delay to hopefully fix any race conditions on thread initialization
//TODO remove this and actually fix the issue
Thread.Sleep(1000);
//Thread.Sleep(1000);
}

private void ListenLoop()
{
private void ListenLoop()
{
var buffer = new byte[8192];
while (_loop)
{
try
{
if (_receiver.IsBound)
{



var length = _receiver.Receive(buffer);
List<OSCM> msgList = new List<OSCM>();
if (OSCParser.IsBundle(ref buffer))
Expand Down Expand Up @@ -252,37 +267,45 @@ private void ListenLoop()
if (oscMessage.Values.Count < 1) continue;
if (oscMessage.Address == "/sl/eyeTrackedGazePoint")
{
if (!_communicating) _communicating = true;
for (int i = 0; i < 3; i++)
{
eyeTrackData[i] = (float)oscMessage.Values[i];
}
continue;
}
if (oscMessage.Address == ("/sl/xrfb/facew/EyesClosedL"))
{
{
if (!_communicating) _communicating = true;
eyelids[0] = (float)oscMessage.Values[0];
continue;

}
if (oscMessage.Address == "/sl/xrfb/facew/EyesClosedR")
{
{
if (!_communicating) _communicating = true;
eyelids[1] = (float)oscMessage.Values[0];
continue;
}

if (mapOSCDirectXRFBUnifiedExpressions.ContainsKey(oscMessage.Address))
{
{
if (!_communicating) _communicating = true;
foreach (UnifiedExpressions unifiedExpression in mapOSCDirectXRFBUnifiedExpressions[oscMessage.Address])
{
//This may not be strictly safe but should be good enough for our use case
ueData[unifiedExpression] = (float)oscMessage.Values[0]; }
ueData[unifiedExpression] = (float)oscMessage.Values[0];
}
}
}
}
else
{
// attempt to rebind?
_receiver.Close();
_receiver.Dispose();
_communicating = false;

_receiver = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
_receiver.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), _resolvedPort));
_receiver.ReceiveTimeout = TIMEOUT_MS;
Expand All @@ -299,7 +322,11 @@ public void Teardown()
_loop = false;
_receiver.Close();
_receiver.Dispose();
_thread.Join();
if (_thread != null)
{
_thread.Join();
}
_communicating = false;
}

}
Expand Down
42 changes: 30 additions & 12 deletions SteamLinkVRCFTModule/SteamLinkVRCFTModule/SteamLinkVRCFTModule.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
using Microsoft.Extensions.Logging;
using SteamLinkVRCFTModule;
using System.Net.Sockets;
using VRCFaceTracking;
using VRCFaceTracking;
using VRCFaceTracking.Core.Params.Expressions;
using static VRCFaceTracking.Core.Params.Expressions.UnifiedExpressions;

namespace SteamLinkVRCFTModule
{
public class SteamLinkVRCFTModule : ExtTrackingModule
{
private OSCHandler OSCHandler;
private OSCHandler? OSCHandler;
private const int DEFAULT_PORT = 9015;
private bool _ownsEyes = false;
private bool _ownsExpressions = false;
private CancellationTokenSource? _cts;

public override (bool SupportsEye, bool SupportsExpression) Supported => (true, true);

Expand All @@ -21,10 +21,19 @@ public override (bool eyeSuccess, bool expressionSuccess) Initialize(bool eyeAva
var stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("SteamLinkVRCFTModule.Assets.steamlink.png");
ModuleInformation.StaticImages = stream != null ? new List<Stream> { stream } : ModuleInformation.StaticImages;

_cts = new CancellationTokenSource();

//TODO better error handling on fail? isInit for OSC Handler?
OSCHandler = new OSCHandler(Logger, DEFAULT_PORT);
OSCHandler = new OSCHandler(_cts.Token, Logger, DEFAULT_PORT);
if (!OSCHandler.initialized)
{
// make sure to teardown anything started before returning as uninitialized
Teardown();
return (false, false);
}

return (true, true);
(_ownsEyes, _ownsExpressions) = (eyeAvailable, expressionAvailable);
return (_ownsEyes, _ownsExpressions);
}

private static float CalculateEyeOpenness(float fEyeClosedWeight, float fEyeTightener)
Expand All @@ -48,8 +57,8 @@ private void UpdateEyeTracking()
if (float.IsNaN(fNmAngleY))
{
fNmAngleY = 0.0f;
}

}
UnifiedTracking.Data.Eye.Left.Gaze.x = fAngleX;
UnifiedTracking.Data.Eye.Left.Gaze.y = fAngleY;

Expand Down Expand Up @@ -94,12 +103,21 @@ private void UpdateFaceTracking()
public override void Update()
{
Thread.Sleep(10);
UpdateEyeTracking();
UpdateFaceTracking();
if (_ownsEyes)
{
UpdateEyeTracking();
}
if (_ownsExpressions)
{
UpdateFaceTracking();
}
}

public override void Teardown()
{
OSCHandler.Teardown();
if (_cts != null) _cts.Cancel();
if (OSCHandler != null) OSCHandler.Teardown();
if (_cts != null) _cts.Dispose();
}
}
}
28 changes: 14 additions & 14 deletions SteamLinkVRCFTModule/res/module.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"ModuleId": "2a8c8080-2a76-46af-bf76-1da7c0127ef8",
"LastUpdated": "2024-08-27T00:56:23.724132",
"Version": "1.0.7",
"Downloads": 0,
"Ratings": 0,
"Rating": 0,
"AuthorName": "Ykeara",
"ModuleName": "SteamLink VRCFT Module",
"ModuleDescription": "VRCFT eye and face tracking module for steamlink on Meta Quest devices.",
"UsageInstructions": "Run on VRCFT Launch (uninstall if you need another module)",
"DownloadUrl": "https://github.com/ykeara/LinkFT/releases/latest/download/LinkFTModule.zip",
"ModulePageUrl": "https://github.com/ykeara/LinkFT",
"DllFileName": "SteamLinkVRCFTModule.dll"
{
"ModuleId": "2a8c8080-2a76-46af-bf76-1da7c0127ef8",
"LastUpdated": "2025-03-01T09:27:03.219452",
"Version": "1.0.8",
"Downloads": 0,
"Ratings": 0,
"Rating": 0,
"AuthorName": "Ykeara",
"ModuleName": "SteamLink VRCFT Module",
"ModuleDescription": "VRCFT eye and face tracking module for SteamLink on Meta Quest devices.",
"UsageInstructions": "Run on VRCFT Launch (uninstall if you need another module)",
"DownloadUrl": "https://github.com/ykeara/LinkFT/releases/latest/download/LinkFTModule.zip",
"ModulePageUrl": "https://github.com/ykeara/LinkFT",
"DllFileName": "SteamLinkVRCFTModule.dll"
}