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.
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
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
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.
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!
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.
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();
}
}
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.
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.
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?
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.
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.
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.