MultiFunPlayer v1.23.1 - Multi axis funscript player - Now with SLR Interactive support

Uh… I’m a bit of a newb when it comes to installing things from GitHub. I know how to perform a git pull request in a folder and I sort of know how to install a python environment and activate it, but it seems MultiFunPlayer is written in C++? I have installed the prerequisites (.NET 6.0 x64 Desktop Runtime) and (Visual C++ 2019 x64). I have also downloaded the MultiFunPlayer zip-file from GitHub and unzipped it.

That’s where I run out of steam. How do I actually launch or install the app? My intuition tells me I should just double-click the file “MultiFunPlayer.sln”, but windows just asks me what app I would like to use to open the file, so… help?

Edit: Turns out I downloaded the source code, where instead I should have downloaded the file MultiFunPlayer-1.23.1-SelfContained.6.0.402.zip from another directory on GitHub. It’s up and running now - thanks Khrull for helping me on Discord!

@aanon

I added a simple plugin/script system: nightly.link | Repository Yoooi0/MultiFunPlayer | Run #3352949699
You should be able to create custom stash integration.
Plugin menu is accessed via a button on top of the window, it only shows detected scripts in Plugins folder.

Currently exposed “api” methods: MultiFunPlayer/IPlugin.cs at c12e8d98c5b7c02c245bedbef01db6395448c615 · Yoooi0/MultiFunPlayer · GitHub
Everything subject to change.

Example websocket plugin (save as websocket.cs to Plugins folder).
It receives float/double values and sets them on L0 axis.

#r "name:System.Net.HttpListener"

using MultiFunPlayer.Plugin;
using MultiFunPlayer.Common;
using System.IO;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Demo;

public class WebsocketPlugin : AsyncPluginBase
{
    public override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        using var listener = new HttpListener();
        listener.Start();
        listener.Prefixes.Add($"http://127.0.0.1:6789/ws/");

        while (!cancellationToken.IsCancellationRequested)
        {
            var context = await listener.GetContextAsync().WithCancellation(cancellationToken);
            if (!context.Request.IsWebSocketRequest)
            {
                context.Response.StatusCode = 400;
                context.Response.Close();
                continue;
            }

            var webSocketContext = await context.AcceptWebSocketAsync(subProtocol: null).WithCancellation(cancellationToken);
            var webSocket = webSocketContext.WebSocket;
            while (!cancellationToken.IsCancellationRequested && webSocket.State == WebSocketState.Open)
            {
                var result = await ReceiveStringAsync(webSocket, cancellationToken);
                Logger.Info("Received data: \"{0}\"", result);

                InvokeAction("Axis::Value::Set", DeviceAxis.Parse("L0"), double.Parse(result));
            }
        }
    }

    private async Task<string> ReceiveStringAsync(WebSocket socket, CancellationToken cancellationToken)
    {
        var buffer = new byte[1024];

        using var stream = new MemoryStream();
        var result = default(WebSocketReceiveResult);
        do
        {
            result = await socket.ReceiveAsync(buffer, cancellationToken);
            if (result.MessageType == WebSocketMessageType.Close)
                return null;

            stream.Write(buffer, 0, result.Count);
        }
        while (!result.EndOfMessage);

        stream.Seek(0, SeekOrigin.Begin);

        using var reader = new StreamReader(stream, Encoding.UTF8);
        return reader.ReadToEnd();
    }
}
2 Likes

Thank you, this is great! Your plugin compliation system is really well thought out and will be huge in giving the community powerful tools to make their own designs.

I implemented this initial Axis::Value::Set approach in a Chrome extension reading from Stash’s funscripts and it does work… However, it ends up losing some of the things that make MFP great. There’s no interpolation for example, and the heatmap obviously doesn’t work.

The good news: with the recent changes made in your GitHub after this post to expose more event methods, I was able to reimplement my vision using the internal events in MFP via a plugin implementing a general-purpose RPC over WebSocket to successfully send a funscript, duration, seek events, and play/pause information into MFP and it works flawlessly! Just click play in Stash and it loads the script / starts playing.

Next steps are:

  • MFP to Stash seek & play/pause (for example, when clicking on the heatmap in MFP)
  • Multi-axis framework (for when Stash adds support)

As a quick note @Yoooi, I think you’ll need this change to support multiple assembly references in a script as I did to support Json & WebSockets together… Or maybe I misunderstand your regex and I could have written it all on one line? Not certain, but this worked out well:

diff --git a/MultiFunPlayer/Plugin/IPluginCompiler.cs b/MultiFunPlayer/Plugin/IPluginCompiler.cs
index 552ec62..8553ffb 100644
--- a/MultiFunPlayer/Plugin/IPluginCompiler.cs
+++ b/MultiFunPlayer/Plugin/IPluginCompiler.cs
@@ -51,7 +51,7 @@ public class PluginCompilationResult : IDisposable
 public static class PluginCompiler
 {
     private static Logger Logger { get; } = LogManager.GetCurrentClassLogger();
-    private static Regex ReferenceRegex { get; } = new Regex("^#r\\s+\\\"(?<type>name|file):(?<value>.+?)\\\"", RegexOptions.Compiled);
+    private static Regex ReferenceRegex { get; } = new Regex("^#r\\s+\\\"(?<type>name|file):(?<value>.+?)\\\"", RegexOptions.Compiled | RegexOptions.Multiline);
     private static IContainer Container { get; set; }

     public static void QueueCompile(string pluginSource, Action<PluginCompilationResult> callback)

I’ll post my Chrome extension & MFP Plugin code when they’re a little more polished.

1 Like

Yea in case of scripts you want to use ScriptLoadMessage: MultiFunPlayer/PluginBase.cs at e616dcbb8fabf0422ef4b27165d817193366e2fc · Yoooi0/MultiFunPlayer · GitHub
You basically have to imitate any media source like HereSphere by publishing correct messages, which I assume you managed to do later.

I think you should be able to just connect straight to stash api without the extension right?

Yup, pushed the exact fix earlier today.
Also today I added ability for plugins to have their own settings UI, and ability to save/load settings.

I’m glad you have a use for it!
If you have any suggestions/ideas let me know.

That’d work, but the Stash server has no idea which client is watching which video, at what seek position, or at what play/pause status, so it makes sense to send that all from a Chrome extension rather than reimplement the wheel.

That’s exactly what I ended up doing, I simplified the RPC into a generalized Load which sends the funscript, duration and media path at the same time and the MFP plugin takes care of the rest. I’m sending all the correct messages over the WebSocket and translating on the Plugin side. That’s definitely the right approach for full scripts.

The Axis::Value::Set style approach will be really useful for creating interface translation layers between something like the Flash toy sync app and an OSR for example.

Thanks again! I look forward to seeing what others come up with.

Bidirectional sync is working!

Still a few edge case bugs to iron out where the chrome extension doesn’t get injected or misses the event that the URL changed and the funscript should be paused. I also had to account for buffering requiring resyncs after resume - it seems like sending a MediaPositionChanged message periodically isn’t causing any problems, @Yoooi is that behavior I should depend on or should I do something else?

:+1:

Yes you should send MediaPositionChanged periodically, ideally at most every 1 second but I have not tested more (DeoVR/HereSphere send every 1s, MPV sends every frame). Bigger time between messages will increase video/script desync.

In case of buffering you should probably send the playing changed message based on video play/pause state but also only if the position is advancing forward. Otherwise the script will be few seconds ahead and might take some time to sync.

@aanon how is stash integration going?
There was a bunch of changes to the plugin system, any issues/suggestions?

everything is connected, but i have stutters with handy no fluid motions… Any idea how i can fix this? wifi or bluetooth has this problem.

Yea, the handy support is experimental, i don’t have one so i cant fully test it.
But pretty much the handy wifi/bluetooth api is designed only for uploading the scripts to handy, and having handy play them, MultiFunPlayer does way more than that so it needs fast local api.

Sorry Off Topic :innocent:

Ik lig ffkes strijk mee uwe nickname… :joy:
Geweldig man :love_you_gesture:

1 Like

feature request. This sounds simple but might be programmatically difficult.

I would like to set a hot-key to move and park the receiver to a higher L0 position- say 90-100%. This would make ingress / egress much easier compared to the 50% default home position. Do you suppose this could be possible for future releases?

edit for clarity. So this could act as a toggle - hit a button and the receiver moves to position and stops in 3 seconds (could be configurable) - hit the button again and the receiver continues script or motion provider. Here, the receiver could home first and then continue script or motion provider, since you already have auto-home built in.

I think this should be doable with current stuff.
Use Axis::Bypass::Toggle shortcut action, have auto-home enabled on L0 with no delay and enable to auto-home inside script.
When you enable bypass the script/motion provider updates will stop and it will immediately start to auto-home.

Or you could have two shortcuts:
#1: enables bypass, sets auto-home delay to 0
#2: disables bypass and sets auto-home delay to your normal time

The only feature to add would be a customizable auto-home position.

1 Like

Will look out for this feature :+1: :grinning: Thank you for looking into this!

Oh - just re-read your reply. So I use Axis::Bypass::Toggle to pause things on demand. I dont think I would want it to home every time I hit that button. I was thinking a separate button since I would only use this at the very beginning and at the very end. Hope that makes sense.

It would be kinda ‘get out of the way’ button.

Not sure I understand, you can have as many shortcuts as you want, so you can have this one just toggle the bypass and optionally configure the auto-home to be instant.

I’m talking about Settings -> Shortcut accessed via cog button at the top of the window.

1 Like

I dont think I understood this well myself. It didnt occur to me you could duplicate shortcuts like this :blush:

Added customizable home position:
https://nightly.link/Yoooi0/MultiFunPlayer/actions/runs/3554176021

1 Like

Tested it. Works perfectly! :+1: :grinning: You are the G.O.A.T!

Also noticed that the device settings for the nightly build are all greyed out ie I cannot enable or disable axes etc

Last question - is there a way to transfer configured settings from one MFP to another?

:+1:

You cannot edit default devices.

You can move MultiFunPlayer.config.json file or just extract new version files over old files.
Tho development builds might reset/remove some settings while changes are made before final release.

1 Like

Moving MultiFunPlayer.config.json file makes life so much easier!

This is no exaggeration when I say that MFP, for my use case is pretty darn complete feature wise :+1: I can literally make OSR do anything to my preference as long as its configurable. Thank You very much!

1 Like