MFP needs to start mpv for scripts to work. Opening the video directly via mpv.exe was just a test.
But I don’t know why you cant drag drop files into the mpv window opened by MFP, did you change the arguments list?
Can I see it?
I managed to fix it by deleting all the other versions of MFP. Everything works now, thank you!
Oh btw is it possible to have video’s looped in MPV?
it can be achieved by pressing L but that repeats only the current file,
Sure, the file attached implements pause script, manual mode and edging mode at the moment. Return to base doesn’t seem to register for me maybe Yoooi knows why. You need the latest version 1.32.0+ as this uses the new plugin API.
What it does at the moment
- Everything is mapped to L0 as I have a FUNOSR1 and no other axis
- Edging mode toggles between an edging and normal range
- Pause script bypasses all motion (motion provider and script)
- Manual mode toggels the motion providor on and off
To use this you’d have to map the buttons to the script actions like so:
Keep in mind that currently you have to press the button twice. I’m hoping Yoooi will reduce this to once as it’s an implementation choice but we’ll see.
The html part is integrated with my Electron application that uses Vue 3 and isn’t easily sharable. however you can easily reproduce something that communicates with the plugin using some basic html and javascript and then just host it on your localhost.
For refrence this is how my plugin ui looks like (speed and delay for manual mode aren’t implemented yet).
You can communicate with the plugin like so, for exampe something like this for is-edging
await fetch(“localhost/set-is-edging”, {
method: “POST”,
headers: {
“Content-Type”: “application/json”
},
body: JSON.stringify({ isEdging: true })
});
RemoteControlPlugin.cs.txt (18.4 KB)
Here’s my one, with a proper HTTP (tho I guess I gonna copypaste routes from your code), and a no-working (I think) enable/disable GUI taken from example
I’ve already used it to play a couple of games, it worked somehow.
ServerPlugin.cs.currently-broken.txt (10.0 KB)
ServerPlugin.xaml.txt (3.0 KB)
code
//#r "System.Net.HttpListener"
using System.Net;
using MultiFunPlayer.MediaSource.ViewModels;
using MultiFunPlayer.MediaSource.Views;
using MultiFunPlayer.Script;
using MultiFunPlayer.Script.Repository.ViewModels;
using Windows.ApplicationModel.Calls;
using FileInfo = System.IO.FileInfo;
[DisplayName("ServerPlugin DisplayName")]
public class ServerPlugin : PluginBase
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private Task _task;
private CancellationTokenSource _cancellationSource;
private ConnectionStatus _status;
private string _text;
public ConnectionStatus Status
{
get => _status;
set => SetAndNotify(ref _status, value);
}
public string Text
{
get => _text;
set => SetAndNotify(ref _text, value);
}
protected override void OnInitialize() { }
protected override void OnDispose()
{
_cancellationSource?.Cancel();
_cancellationSource?.Dispose();
_cancellationSource = null;
_task = null;
}
public void OnConnectClick()
{
if (_task == null)
{
_cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken);
_task = Task.Run(async () => await RunAsync(_cancellationSource.Token));
}
else
{
OnDispose();
}
}
private HttpListener server;
private async Task RunAsync(CancellationToken token)
{
try
{
Status = ConnectionStatus.Connecting;
server = new HttpListener();
server.Start();
server.Prefixes.Add("http://127.0.0.1:5000/");
server.Prefixes.Add("http://localhost:5000/");
server.Prefixes.Add("http://127.0.0.1:5050/");
server.Prefixes.Add("http://localhost:5050/");
await Task.Delay(500);
Status = ConnectionStatus.Connected;
while (!token.IsCancellationRequested)
{
var context = await server.GetContextAsync();
// There is no response so we close the stream
context.Response.StatusCode = 204;
context.Response.OutputStream.Close();
if (context.Request.Url.LocalPath == "/favicon.ico") continue;
ProcessGeneralRequest(context);
}
}
catch (OperationCanceledException) { }
catch (Exception e)
{
Logger.Error(e);
}
finally
{
Status = ConnectionStatus.Disconnecting;
server.Stop();
await Task.Delay(500);
Status = ConnectionStatus.Disconnected;
Text = null;
}
}
private bool isFiller = false;
private async void ProcessGeneralRequest(HttpListenerContext context)
{
context.Response.OutputStream.Close();
Logger.Info($"url: {context.Request.Url.AbsoluteUri}");
Text = $"url: {context.Request.Url.AbsoluteUri.Substring(22)}";
// var filename = context.Request.QueryString["filename"];
// var chapter = context.Request.QueryString["chapter"];
// var speed = Double.Parse(context.Request.QueryString["speed"] ?? "1.0");
// var A = Double.Parse(context.Request.QueryString["A"] ?? "-1.0");
// var B = Double.Parse(context.Request.QueryString["B"] ?? "-1.0");
// Text += $"\nFilename: {filename}\nChapter: {chapter}\nSpeed: {speed}\nA: {A}\nB: {B}";
// // sleep because Internal::Playlist::PlayByName resets AB
// await Task.Delay(100);
}
private async void ProcessEdiRequest(HttpListenerContext context)
{
context.Response.OutputStream.Close();
Logger.Info($"url: {context.Request.Url.AbsoluteUri}");
if (context.Request.Url.LocalPath != "/game/gallery")
{
return;
}
Text = $"url: {context.Request.Url.AbsoluteUri.Substring(22)}\nLocalPath: {context.Request.Url.LocalPath}";
var code = context.Request.QueryString["code"];
Text += $"\nCode: {code} ({code.Length})";
if (code == "")
{
code = "filler1";
// InvokeAction("Media::PlayPause::Set", false, true);
// InvokeAction("Media::Loop::Set", 10, 11, true);
// return;
}
var csv = System.IO.File.ReadAllLines("./ClassLibrary1/funscript/marielle/Definitions.csv")
.Select(x => x.Split(',')).ToList();
// csv.ForEach(x => Logger.Info($"{x[0]} | {x[1]} | {x[2]} | {x[3]}"));
var line = csv.FirstOrDefault(x => x[0] == code, null);
if (line == null)
{
Logger.Info($"Not found {code}");
return;
}
Logger.Info($"Found {line[0]} | {line[1]} | {line[2]} | {line[3]}");
var file = line[1];
var start = double.Parse(line[2]) * 0.001;
var end = double.Parse(line[3]) * 0.001;
InvokeAction("Internal::Playlist::PlayByName", $"{file}.funscript", true);
InvokeAction("Media::PlayPause::Set", true, true);
InvokeAction("Media::Position::Time::Set", start, true);
// sleep because Internal::Playlist::PlayByName resets AB
await Task.Delay(100);
InvokeAction("Media::Loop::Set", start, end, true);
InvokeAction("Media::Position::Time::Set", start + 0.100, true);
Logger.Info($"Playing {file} from {start} to {end}");
// InvokeAction("Media::Loop::Set", start, end, true);
// InvokeAction("Media::Loop::Start::Set", start, true);
// InvokeAction("Media::Loop::End::Set", end, true);
}
private void ProcessSetariaRequest(HttpListenerContext context)
{
Text = $"url: {context.Request.Url.AbsoluteUri.Substring(22)}\n{String.Join(",\n",
context.Request.QueryString.AllKeys.Select(x => $"{x}={context.Request.QueryString[x]}")
)}";
context.Response.OutputStream.Close();
}
private void ProcessRequest(HttpListenerContext context)
{
if (context.Request.Url.LocalPath == "/favicon.ico")
{
context.Response.OutputStream.Close();
return;
}
Text = $"url: {context.Request.Url.AbsoluteUri}";
// process
var result = new Dictionary<DeviceAxis, IScriptResource>();
var scriptName = context.Request.Url.LocalPath.Replace("/Edi/Play/", "");
var scriptName1 = scriptName.Split("_")[0];
// DeviceAxisUtils.GetBaseNameWithExtension(context.Request.Url.LocalPath);
var fi = new FileInfo($"./ClassLibrary1/funscript/{scriptName}.funscript");
var fr = FunscriptReader.Default;
var readerResult = fr.FromFileInfo(fi);
Logger.Info($"Found IsSuccess:{readerResult.IsSuccess} multiaxis:{readerResult.IsMultiAxis} count:{readerResult.Resources?.Count}");
Text = $"{Text}\n{fi.Exists} {fi}\n{scriptName} / {scriptName1}";
if (readerResult.IsSuccess)
{
result.Merge(readerResult.Resources);
PublishMessage(new ChangeScriptMessage(result));
PublishMessage(new MediaPathChangedMessage($"./ClassLibrary1/funscript/{scriptName}.funscript"));
// Media::Chapter::SeekToByName
InvokeAction("Internal::Playlist::PlayByName", $"{scriptName}.funscript");
InvokeAction("Media::Chapter::SeekToByName", scriptName1);
InvokeAction("Media::Loop::Set::FromCurrentChapter");
InvokeAction("Media::PlayPause::Set", true);
// InvokeAction()
}
}
private void Play(
string filename,
string chapter = null,
double speed = 1.0
)
{
var fi = new FileInfo($"./ClassLibrary1/funscript/{filename}.funscript");
var readerResult = FunscriptReader.Default.FromFileInfo(fi);
Logger.Info($"Found IsSuccess:{readerResult.IsSuccess} multiaxis:{readerResult.IsMultiAxis} count:{readerResult.Resources?.Count}");
if (!readerResult.IsSuccess) return;
var res = readerResult.Resource;
var chap = res.Chapters.FirstOrDefault(x => x.Name == chapter);
if (chap == null) return;
readerResult.Resources.Merge(DeviceAxis.All.Except(readerResult.Resources.Keys).ToDictionary(a => a, _ => default(IScriptResource)));
PublishMessage(new ChangeScriptMessage(readerResult.Resources));
InvokeAction("Media::Chapter::SeekToByName", chapter, invokeDirectly: true);
InvokeAction("Media::Loop::Set::FromCurrentChapter", invokeDirectly: true);
InvokeAction("Media::PlayPause::Set", invokeDirectly: true);
}
// public void LoadFunscript()
// {
// var result = new Dictionary<DeviceAxis, IScriptResource>();
// if (LoadAdditionalScripts)
// {
// var scriptName = DeviceAxisUtils.GetBaseNameWithExtension(item.Name);
// result.Merge(localRepository.SearchForScripts(scriptName, Path.GetDirectoryName(item.FullName), DeviceAxis.All));
// }
// else
// {
// var readerResult = FunscriptReader.Default.FromFileInfo(item.AsFileInfo());
// if (readerResult.IsSuccess)
// {
// if (readerResult.IsMultiAxis)
// result.Merge(readerResult.Resources);
// else
// result.Merge(DeviceAxisUtils.FindAxesMatchingName(item.Name, true).ToDictionary(a => a, _ => readerResult.Resource));
// }
// }
// if (result.Count == 0)
// {
// ResetState();
// return;
// }
// SetDuration(result.Values.Max(s => s.Keyframes[^1].Position));
// SetPosition(0, forceSeek: true);
// result.Merge(DeviceAxis.All.Except(result.Keys).ToDictionary(a => a, _ => default(IScriptResource)));
// PublishMessage(new ChangeScriptMessage(result));
// }
}
Yeah importing using System.Net; didn’t work for me in the previous version of mfp for some reason so I worked around it. However, everything is working fine and dandy on my side so no need for any code but thanks
You need to use //#r "System.Net.HttpListener"
for not-included deps (that was somewhere i changelogs I guess?)
Tho I’m not sure if it works in the build with System dlls
I’ve posted code mostly to post code so it doesn’t just lays on my PC and someone can read and comment on it, and your code post finally made me do this
The plugin base class exposes CancellationToken
property which gets cancelled before the call to OnDispose
. You should use it (or your own) on all your tasks/threads so that they are properly cleaned up when the plugin is unloaded.
Your Action queue
task will never finish, meaning the plugin assembly will always be loaded, so on each recompile you will have a memory leak.
Also I dont think you need the whole action queue, if you want thread safety just add a lock. But if it works it works.
Its not an implementation choice, thats how it behaves in DeoVR, you saw the logs, there are no events for when you press/release the button, only events for when a feature is enabled/disabled.
You can add two shortcuts one for press (enable feature), one for release (disable feature).
Ahh, I’m mostly a javascript dev, C# is like my fifth language, thanks for the feedback!
Actually no, maybe this was a bit of miscommunication. You get a log line for every button press from DeoVR (I’ve confirmed this). However the action is only triggered once every two clicks. So when it’s disabled initially in DeoVR and you then start the plugin it would be:
first click enabled (no action triggered but a log line is shown)
click two disabled (action is triggered and a log line is shown).
So about the action queue. I had to add this since doing it directly like below doesn’t work:
RegisterAction(“RemoteControlPlugin::ReturnToBase”, () =>
{
InvokeAction<DeviceAxis, double>(“Serial/0::Axis::Range::Minimum::Set”, DeviceAxis.Parse(“L0”), min);
});
The action queue makes sure it is running in the same thread and then it does work. Maybe there is a better way but that’s why it’s there.
You may try adding a random timeout as I did to avoid script change resetting my AB loop
Or try the InvokeActionAsync maybe
Some additional feedback then.
If you register an action you have to unregistered it while disposing otherwise you will have stuck actions in the MFP UI (I might make it so the plugin base unregistered the actions for you just in case). This will also prevent the plugin from fully unloading.
When you call EnqueueAction
with an async lambda, you are actually passing a Task not Action, but EnqueueAction
accepts an Action
so the Task is converted. Then inside ProcessActionQueue
when you invoke the actions with action();
the task is not awaited so it is executed instantly (it will not wait for SendHttpPostOverTcp
to finish).
This means that multiple calls to EnqueueAction
with async lambdas will be executed at the same time, and thus its not thread safe.
You would have to make EnqueueAction
accept Func<Task>
as action, and inside ProcessActionQueue
you would do await action();
If you add a Button Press
and Button Release
shortcut with the same action it will be triggered each click. Unless you are describing some bug.
Oh, yea thats an issue. This will push a new action into the internal queue and wait for it to finish, but since you are inside another action that is already getting processed it causes a dead lock.
Don’t know if there is an easy way to fix this.
For now you can invoke the action directly bypassing the internal queue by adding , invokeDirectly: true
as the last argument to InvokeAction
Your queue does not run each action on the same thread, because you call ProcessActionQueue
from two different tasks that can run on two different threads. Also you are creating Task actions (async lambdas) that are passed to EnqueueAction and they can also run on different threads.
Async/await programming is not that straight forward if you have threads/tasks/UI mixed together.
Thanks again for the detailed write up!
I’ll take a look at cleaning everything up properly and the invoke immediately did indeed fix it so I can get rid of the queue. So thanks for that.
The only thing that doesn’t work like you said is the toggle. I really think this is a bug then:
Blockquote If you add a
Button Press
andButton Release
shortcut with the same action it will be triggered each click. Unless you are describing some bug.
Adding a press and release will just trigger the action twice for the same log line (if the logline triggers an action which it doesn’t always do). I really think that if your intention is to trigger the listeners on each log line, then there is a bug.
To illustrate this I’ve started up MFP from scratch and kept all the DeoVRInputProcessor logs and the triggered action logs. There are 2 things of note
- You can see that the actions are only triggered after two DeoVR messages are received. So only when is down (enabled) is false will the action trigger. Every other log line.
- Even though the plugin is running and I’ve intentionally waited a few seconds and made sure DeoVR showed connected. The first commands are ignored all together.
Logs
2024-12-30 20:27:20.4287|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:22.0605|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command “{“ReturnToBase”:false,“ManualMode”:false,“EdgingMode”:false,“ScriptPaused”:true}”
2024-12-30 20:27:22.0605|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:33.6998|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command “{“Time”:“2024-12-30T19:27:35.942822Z”,“IsHolding”:false,“IsDown”:false,“Type”:3}”
2024-12-30 20:27:33.6998|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:35.0535|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command “{“Time”:“2024-12-30T19:27:37.299761Z”,“IsHolding”:false,“IsDown”:true,“Type”:3}”
2024-12-30 20:27:35.0535|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:35.9107|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command “{“Time”:“2024-12-30T19:27:38.154497Z”,“IsHolding”:false,“IsDown”:false,“Type”:3}”
2024-12-30 20:27:35.9107|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:36.1072|TRACE|MultiFunPlayer.Shortcut.ShortcutActionRunner|Invoking “RemoteControlPlugin::EdgingMode” action [Configuration: “”, Gesture: SimpleInputGestureData { }]
2024-12-30 20:27:36.1427|TRACE|MultiFunPlayer.Shortcut.ShortcutActionRunner|Invoking “Debug::Log” action [Configuration: “Info, >>>>> Edging mode action triggered”, Gesture: SimpleInputGestureData { }]
2024-12-30 20:27:36.1427|INFO|MultiFunPlayer|>>>>> Edging mode action triggered
2024-12-30 20:27:36.9913|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command “{“Time”:“2024-12-30T19:27:39.2346Z”,“IsHolding”:false,“IsDown”:true,“Type”:3}”
2024-12-30 20:27:36.9913|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:37.9106|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command “{“Time”:“2024-12-30T19:27:40.143938Z”,“IsHolding”:false,“IsDown”:false,“Type”:3}”
2024-12-30 20:27:37.9106|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:38.1117|TRACE|MultiFunPlayer.Shortcut.ShortcutActionRunner|Invoking “RemoteControlPlugin::EdgingMode” action [Configuration: “”, Gesture: SimpleInputGestureData { }]
2024-12-30 20:27:38.1117|TRACE|MultiFunPlayer.Shortcut.ShortcutActionRunner|Invoking “Debug::Log” action [Configuration: “Info, >>>>> Edging mode action triggered”, Gesture: SimpleInputGestureData { }]
2024-12-30 20:27:38.1117|INFO|MultiFunPlayer|>>>>> Edging mode action triggered
2024-12-30 20:27:38.4732|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command “{“Time”:“2024-12-30T19:27:40.710938Z”,“IsHolding”:false,“IsDown”:true,“Type”:3}”
2024-12-30 20:27:38.4732|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:39.5451|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command “{“Time”:“2024-12-30T19:27:41.788289Z”,“IsHolding”:false,“IsDown”:false,“Type”:3}”
2024-12-30 20:27:39.5451|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
2024-12-30 20:27:39.7452|TRACE|MultiFunPlayer.Shortcut.ShortcutActionRunner|Invoking “RemoteControlPlugin::EdgingMode” action [Configuration: “”, Gesture: SimpleInputGestureData { }]
2024-12-30 20:27:39.7452|TRACE|MultiFunPlayer.Shortcut.ShortcutActionRunner|Invoking “Debug::Log” action [Configuration: “Info, >>>>> Edging mode action triggered”, Gesture: SimpleInputGestureData { }]
2024-12-30 20:27:39.7452|INFO|MultiFunPlayer|>>>>> Edging mode action triggered
Sorry I still dont understand.
Received command "{"ReturnToBase":false,"ManualMode":false,"EdgingMode":false,"ScriptPaused":true}"
Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
Button Press
Received command "{"ReturnToBase":false,"ManualMode":false,"EdgingMode":false,"ScriptPaused":false}"
Button Release
Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
Received command "{"ReturnToBase":false,"ManualMode":false,"EdgingMode":false,"ScriptPaused":true}"
Button Press
Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
Received command "{"ReturnToBase":false,"ManualMode":false,"EdgingMode":false,"ScriptPaused":false}"
Button Release
Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
Received command "{"ReturnToBase":false,"ManualMode":false,"EdgingMode":false,"ScriptPaused":true}"
Button Press
Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
Received command "{"ReturnToBase":false,"ManualMode":false,"EdgingMode":false,"ScriptPaused":false}"
Button Release
Waiting for incoming TCP connection [Endpoint: 0.0.0.0:38882]
edit:
The whole “api” for those 4 buttons is the most convoluted shit I’ve seen.
It seems like the “[Hold]” versions of haptics shortcuts are not being sent on “v3” api (which MFP uses), they were sent only on “v2”, also the ReturnToBase
button only is sent on the false state, the true state is not being sent because of some magic if checks.
Haha yeah noticed some strange things too. Most notably the ReturnToBase command sends
Received command "{"ReturnToBase":false,"ManualMode":false,"EdgingMode":false,"ScriptPaused":false}"
while the rest does this
Received command “{“Time”:“2024-12-30T19:27:35.942822Z”,“IsHolding”:false,“IsDown”:false,“Type”:3}”
That’s why I misuse the pause script button for return to base at the moment. As in my logic return to base works like a switch but with SLR it works differently I suppose. Haven’t used my handy in a while and never used that feature on the Handy so I don’t know how it is supposed to work. Could check for you if you don’t have one yourself.
Okay, so the button press was configured as a button click (my bad). After changing that to button press the behavior is indeed a lot better. There is still some weird behavior though: The first few commands are always missed and sometimes I notice a haptic command not resulting in a button trigger. It also dropped out for a while after I put the headset down and put it back on again but I’m guessing that it just lost connection there for a while. It works a lot better now though so that’s nice.
Annotated logs
--> First two aren't registering
2024-12-31 10:16:02.0249|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:16:03.170535Z","IsHolding":false,"IsDown":true,"Type":3}"
2024-12-31 10:16:04.0607|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:16:05.205943Z","IsHolding":false,"IsDown":false,"Type":3}"
2024-12-31 10:16:05.1422|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:16:06.283747Z","IsHolding":false,"IsDown":true,"Type":3}"
2024-12-31 10:16:05.1422|INFO|MultiFunPlayer|Button press
2024-12-31 10:16:06.4434|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:16:07.30625Z","IsHolding":false,"IsDown":false,"Type":3}"
2024-12-31 10:16:06.4434|INFO|MultiFunPlayer|Button release
2024-12-31 10:16:07.0139|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:16:07.795076Z","IsHolding":false,"IsDown":true,"Type":3}"
2024-12-31 10:16:07.0139|INFO|MultiFunPlayer|Button press
--> This one doesn't trigger the action
2024-12-31 10:16:07.6388|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:16:08.694596Z","IsHolding":false,"IsDown":true,"Type":3}"
2024-12-31 10:16:07.7867|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:16:08.939403Z","IsHolding":false,"IsDown":false,"Type":3}"
2024-12-31 10:16:07.7867|INFO|MultiFunPlayer|Button release
2024-12-31 10:16:08.0500|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:16:09.194332Z","IsHolding":false,"IsDown":true,"Type":3}"
2024-12-31 10:16:08.0500|INFO|MultiFunPlayer|Button press
--> Tried again later, it didn't produce any logs or actions for a while but then it did again
2024-12-31 10:24:29.5606|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:24:30.446153Z","IsHolding":false,"IsDown":true,"Type":3}"
2024-12-31 10:24:30.1583|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:24:31.313858Z","IsHolding":false,"IsDown":false,"Type":3}"
2024-12-31 10:24:30.1583|INFO|MultiFunPlayer|Button release
2024-12-31 10:24:30.8521|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:24:32.013793Z","IsHolding":false,"IsDown":true,"Type":3}"
2024-12-31 10:24:30.8521|INFO|MultiFunPlayer|Button press
2024-12-31 10:24:31.3621|DEBUG|MultiFunPlayer.Input.DeoVR.DeoVRInputProcessor|Received command "{"Time":"2024-12-31T09:24:32.51424Z","IsHolding":false,"IsDown":false,"Type":3}"
2024-12-31 10:24:31.3621|INFO|MultiFunPlayer|Button release
--> keeps working from here
Anyways, I’ve been diving deeper into MFP now that I’ve got it running smoothly, and I’m discovering all kinds of great features it brings to the table. Really a lot nicer then just the handy controls once you get it going!
I must say though, that I did find the initial setup and learning process to be pretty challenging. Have you thought about some centralized (community) documentation or a guide or something? Think that could make a huge difference for newbies. Especially with things like setting up the SLR buttons etc.
On a related note, I think it could be really valuable to include a SLR starter plugin with a built-in web server you can access from a Quest browser with the same sliders as people are used to from the SLR handy settings (similar to what I made but completely contained in the plugin). I won’t use it personally as I’ve got everything running the way I like now, but I feel like this would be a game-changer for any new SLR users.
Just some thoughts though