MultiFunPlayer v1.32.0 - Multi axis funscript player - Now with SLR script streaming

Yeah, since MFP wouldn’t be able to easily connect to an arbitrary webapp, do it the other way around where the website reaches out to MFP. That way this could work with Stash or just about any other website.

Stash is a self hosted web app. It can hold and serve funscripts on its own, so a URL like http://192.0.2.1:9999/scene/1234/stream is the video, while http://192.0.2.1:9999/scene/1234/funscript serves a funscript for that scene. Right now, Stash only works with The Handy, but integrating MFP would open it up to all the outputs that MFP supports.

The idea would be for Stash and other web sites to integrate client side code that connects to a websocket listener inside MFP and sends a funscript URL to load (or maybe just the content of the funscript?) over that websocket and periodic bidirectional time/pause state. That way MFP can work without any local files, just using the data inside Stash.

If you have a better approach for webapp sync, I’d be interested to hear it.

1 Like

So how would you implement the stash part, like a plugin or edit the server code directly?

If it is self hosted then why websocket? Im assuming because browsers can only use websockets and not tcp/udp, but couldnt you just send data directly from the server instead of the webpage? This way you could just emulate deovr api with tcp (without the funscript part).

But I think it might be better to create an api server in stash if that does not exist yet, and MFP would connect to that. This way other players could do the same.

If we go the MFP api way, any suggestions on what would you like the api to look like?

It sounds like he wants buttplug integration in stash, and then have MFP present itself as a sex toy to buttplug.

I don’t think it’s a good idea to reinvent the buttplug API. Application builders want to implement a single API to support many toys. We don’t want to end up in a situation where application builders need to implement the handy api, the MFP api, the lush api…

The stash developers are open to direct implementation in the client JS sent to browsers, or a browser plugin could also work initially for rapid iteration.

The server can theoretically serve multiple simultaneous users, which introduces a problem of its own. Which stream do you send to MFP? That makes server side implementation complicated.
From browser JS you can’t open a websocket server and accept connections from MFP, so that’s out too.

I’m suggesting that MFP create a standard for universal video/device sync exchange from browsers. If MFP served a websocket that Stash’s JS could connect to, with a simple JSON API to send back and forth “please load this funscript URL” alongside bidirectional time sync events and play/pause events, that’s easy to work with. That would also work with any other website and not just stash. Then, browsers could just support that one single protocol and use MFP as the translator between all the others including limits, etc.

Yea, the api would just either have to include a way to determine users/sessions in event data (and send events for all users), or have a way to filter/subscribe to events based on user/session.
I really dont think it would be that complicated of an api.

This way all future apps will be handled with one api in the stash server.
Otherwise you will be adding new api per funscript player to stash client code. Even tho im not a developer of stash this feels weird for me. If there is code to be added to stash why not just write the general api that can be used by all players instead?

Yea, I think I could add something like that, even tho I think the api should be in stash, it might be useful to somebody one day. Would have to support tcp/websocket/namedpipe clients.

Things that the stash client would have to support:

  • Send media play/pause events
  • Send media path/url when playing a video, null/empty when stopped playing
  • Send media duration
  • Send media playback rate
  • Send media position at most every 1s
  • Receive play/pause request
  • Receive media seek request
  • Receive media play file/url request

All those can be supported in the client js?

He suggests a way for other apps to connect to MFP tcp/websocket server and send media/funscript events via api specified by MFP.
Currently all other supported players in MFP work the other way around where MFP connects to the server in media player and implements their api.

MultiFunPlayer v1.23.0:

Download: Release MultiFunPlayer v1.23.0 · Yoooi0/MultiFunPlayer · GitHub
Patreon build: https://www.patreon.com/posts/73725137

  • Add send only changed values toggle (#82), optimizes amount of data sent each update tick, defaults to enabled for every output but UDP
  • Add offload update tick elapsed time toggle (#86), tries to improve smoothness by offloading elapsed time calculation to device firmware, defaults to enabled
    2022-10-19_17-56-04
  • Merge all settings into one popup
    2022-10-19_17-58-37
  • Add device settings (#78)
  • Add advanced serial settings (#85)
    2022-10-19_17-57-49
  • Add missing motion provider shortcuts (#89)
  • Add ability to show/hide media sources
    firefox_2022-10-19_17-58-20
  • Split Network output to TCP and UDP output (#87)
    MultiFunPlayer_2022-10-19_17-59-20
  • Fix selected serial port not saving after unplugging the device
  • Fix crash when starting with always on top enabled (#83)
  • Fix NPE when creating buttplug output with log level set to Off
  • Update packages

If you like what I’m doing, please consider supporting me on Patreon

5 Likes

Always looking forward to a new update for this player. Best full featured player around. Thank you for the great work on this!

1 Like

MultiFunPlayer v1.23.1:

Download: Release MultiFunPlayer v1.23.1 · Yoooi0/MultiFunPlayer · GitHub
Patreon build: https://www.patreon.com/posts/73725137

v1.23.1:

  • Fix seek and play/pause requests not working

v1.23.0:

  • Add send only changed values toggle (#82), optimizes amount of data sent each update tick, defaults to enabled for every output but UDP
  • Add offload update tick elapsed time toggle (#86), tries to improve smoothness by offloading elapsed time calculation to device firmware, defaults to enabled
    2022-10-19_17-56-04
  • Merge all settings into one popup
    2022-10-19_17-58-37
  • Add device settings (#78)
  • Add advanced serial settings (#85)
    2022-10-19_17-57-49
  • Add missing motion provider shortcuts (#89)
  • Add ability to show/hide media sources
    firefox_2022-10-19_17-58-20
  • Split Network output to TCP and UDP output (#87)
    MultiFunPlayer_2022-10-19_17-59-20
  • Fix selected serial port not saving after unplugging the device
  • Fix crash when starting with always on top enabled (#83)
  • Fix NPE when creating buttplug output with log level set to Off
  • Update packages

If you like what I’m doing, please consider supporting me on Patreon

1 Like

Have you thought about adding Plex support? They also have a VR app, albeit a very cheesy one.

Yea, I thought about it before, added as todo on github.
Technically it is kinda supported right now since both plex and emby use mpv internally and you can connect MFP to the internal mpv. Tho the reported file is just a stream url so MFP cant match scripts to it without path modifiers.

1 Like

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/MultiFunPlayer/Plugin/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.