Skip to content

Latest commit

 

History

History
321 lines (250 loc) · 18.4 KB

README.md

File metadata and controls

321 lines (250 loc) · 18.4 KB

Funscript Player – your video, your devices, perfectly in sync. A Funscript player for The Handy and Satisfyer – with your Stash library or plain video folders.

Install  Download ZIP

Version Docker amd64 + arm64 Node.js The Handy Satisfyer Stash License: AGPL-3.0 OpenSSF Scorecard

Animation: the player plays a scene, The Handy strokes and the Satisfyer pulses – both from the same Funscript

One choice for every device · Satisfyer with any script · Natural picture through upscaling

See it – the player as it looks today

The player in demo mode: video plays, The Handy and Satisfyer follow the script  Library with script heatmaps  Devices: The Handy and Satisfyer

The player on a PC: video, script intensity, devices and the up-next list

Screenshots with neutral test videos and virtual devices (built-in demo mode, /demo).

Who is it for? Whatever you own Just a Handy · Just a Satisfyer · A Stash library

In three steps – connect, pick, play 1 Connect · 2 Pick a scene · 3 Play

✨ All features

Funscript Player plays your videos in the browser and drives The Handy and Satisfyer devices in sync with the Funscript – at the same time, from the same script. It runs on your own server in Docker, reads your Stash library or plain video folders, and works on phone, tablet, PC and in VR (HereSphere).

  • One script, every device – pick the intensity once for Handy and Satisfyer: Auto, Original, Soft or Intense, per scene, with a mini heatmap. Several scripts per video (clip.soft.funscript …) switchable while playing.
  • Satisfyer with any script – the stroke script becomes vibration live. No vibration script needed; several Satisfyers at once, each with its own strength.
  • Sync that stays in time – Handy cloud with clock sync; with an Application ID (API v3) even at 0.5×–2×.
  • Library – Stash scenes and folder videos with heatmap, intensity filter, 🔥 highlights, saved Stash filters, 2D-only mode, ratings and favourites back to Stash.
  • Like an app – install it on the home screen, swipe controls in fullscreen, picture-in-picture, mini player.
  • Your own scripts – control the devices by hand, ⏺ record along the video, replace just a section. Originals are never overwritten.
  • Natural picture – small videos are upscaled on the server (Intel Quick Sync, otherwise CPU).
  • VR – HereSphere API for VR headsets.
  • 📺 On the TV – stream to a Chromecast (Chrome) or AirPlay (Safari); the devices follow the TV.
  • Emergency stop – Esc and “Stop everything” always; optional STOP buttons, stop on screen lock, auto-stop.

Installation – Docker in five minutes

🚀 Quick start

Two ways – both end up with the same player:

  • Simple (2 minutes): one small compose file, no ZIP, no .env – login, Stash and devices are set up in the player itself. → QUICKSTART.md
  • Normal: ZIP with docker-compose.yml and .env for all options (reverse proxy, Intel graphics, own user …):

Grab funscript-player-vX.Y.Z.zip from the latest release (or clone this repo), then:

unzip funscript-player-vX.Y.Z.zip && cd funscript-player
cp .env.example .env
nano .env                       # login, Stash address, video folder
docker compose up -d
docker logs Funscript-Player    # startup summary with self-check, ends with “✓ ready”

Open http://<server>:8080.

  • Login – required. Set AUTH_USER + AUTH_PASS in .env, or leave them empty and choose user name and password on the player's setup page (change or remove it later in ⚙️ → Security & privacy).
  • Stash – STASH_URL as seen from the container: on a shared Docker network the container name (http://Stash:9999), not localhost. No Stash? STASH=off.
  • Video folders – MEDIA=/path/to/videos: every subfolder becomes a source; video and script share the name (Name.mp4 + Name.funscript).
  • HTTPS – needed for Bluetooth and “install as app”: put any reverse proxy in front (Traefik, Caddy, nginx …). See Security & proxy.
  • Update – docker compose pull && docker compose up -d. Your .env and the funscript-data volume stay.

Everything else is optional and commented out at the bottom of .env.example.

✅ Runs on

  • Server: anything with Docker – Linux PC, NAS, Raspberry Pi 4/5, Mac with Apple silicon (images for amd64 + arm64). Intel graphics make the HQ stream faster (optional); without them it runs on the CPU.
  • Player: any current browser on phone, tablet or PC – installable as an app.
  • The Handy over Wi-Fi: every browser. Satisfyer via Bluetooth: Chrome or Edge on Android, Windows, macOS, Linux; on iPhone/iPad the Bluefy browser. Bluetooth needs HTTPS (or localhost).
  • Without browser Bluetooth: Satisfyer and Handy via Intiface Central in your home network.
  • Other strokers (e.g. Kiiroo Keon, OSR2/SR6 single axis) via Intiface – they get the same script intensity as the Handy. Not tested on real hardware yet; feedback welcome.
  • VR: HereSphere (Quest and others).

📱 Using it

Tap the status at the top → connect the Handy (connection key) and/or the Satisfyer → pick a scene → play.

More
  1. Tap the status at the top (or Devices) → connect the Handy with its connection key and/or the Satisfyer via Bluetooth.
  2. Pick a scene in the library and press play. Below the video you only see the controls of connected devices.
  3. If a script is off: shift the offset or tap along to calibrate it (saved per scene).
  4. No Funscript? Control yourself at the video: speed, stroke, touch area or your patterns (switch Direct | Patterns).
  5. 📺 at the video streams it to a Chromecast (Chrome) or via AirPlay (Safari) – the devices follow the TV.

Settings are grouped into pages: Devices, Security & privacy, Script, Library, Picture & display, VR & HereSphere, System. ⚙️ → System shows “Update available” when there is a newer release. The interface speaks English and German. Want to try what's next? PLAYER_VERSION=edge follows the latest tested state of main.

🔒 Security & proxy

Login required, passwords hashed, brute-force brake, hardened read-only container. Behind a proxy with its own login, exclude only /heresphere.

Details
  • Login page with “stay logged in” (30 days) and “log out”; basic auth still works for scripts (curl -u).
  • Login: rule order .env (AUTH_USER/AUTH_PASS) > login set up in ⚙️ > LOGIN=off > setup page. Passwords set in the interface are stored as scrypt hashes; 10 wrong attempts per minute lock out guessing. LOGIN=off only if a reverse proxy with its own login protects the player. Forgotten? LOGIN_RESET=1, restart, remove the line.
  • Reverse proxy with its own login: exclude only /heresphere (HereSphere can't do browser logins) – never /stash or /media. PLAYER_BIND=127.0.0.1 publishes the port only on the server itself, so nobody gets past the proxy.
  • HereSphere is off by default. If you switch it on, give it its own password (⚙️ → VR & HereSphere) – without one, the /heresphere exception in your proxy is an open door to your library. Its token in the URL never expires; if it leaks, set a new password (that logs out all devices).
  • The server only accepts changes from its own page (origin check), can't be embedded in other sites and serves a strict Content Security Policy.
  • The container runs as user node (never root), read-only, without Linux capabilities, with no-new-privileges and a memory cap (PLAYER_MEM, default 2g). It only writes to its data volume.
  • Intiface relay: home-network addresses and port 12345 only (INTIFACE_LAN_ONLY, INTIFACE_PORTS).
  • The Handy connection key and Application ID are stored encrypted (AES-256-GCM) in the data volume. Set DATA_SECRET in .env and a copy of the volume alone no longer reveals the key.
  • Failed logins are logged with time and address (docker logs) and counted in ⚙️ → Security & privacy; there you can also log out all devices. HereSphere devices can be logged out in ⚙️ → VR & HereSphere without a new password.

Found a vulnerability? See SECURITY.md.

Verify a download or the image
# ZIP: checksum and signature (Sigstore, bound to this repository's release workflow)
sha256sum -c funscript-player-vX.Y.Z.zip.sha256
cosign verify-blob --bundle funscript-player-vX.Y.Z.zip.sigstore.json \
  --certificate-identity-regexp '^https://github.com/ScriptHost/funscript-player/.github/workflows/release.yml@' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com funscript-player-vX.Y.Z.zip

# Image: build provenance and software bill of materials (SBOM)
docker buildx imagetools inspect ghcr.io/scripthost/funscript-player:X.Y.Z --format '{{ json .Provenance.SLSA }}'
docker buildx imagetools inspect ghcr.io/scripthost/funscript-player:X.Y.Z --format '{{ json .SBOM }}'

🛡 Privacy

The server needs no internet. Only the Handy over Wi-Fi uses the Handy cloud (handyfeeling.com) – your browser sends the connection key and the current script there; videos and titles stay home.

Details
  • The container needs no internet access. It only talks to Stash and serves the player to your browser. The only exception is the optional update check (GitHub, at most every 6 h) – UPDATE_CHECK=off switches it off.
  • The Handy over Wi-Fi goes through the Handy cloud (handyfeeling.com). Your browser – not the server – sends your connection key and the Funscript of the current video there, because the Handy plays scripts from the cloud. The script is uploaded as a CSV with the neutral name script.csv.
  • The connection key is stored on your player server so all your devices know it.
  • 📺 Streaming to the TV: in Chrome the player loads Google's Cast library from www.gstatic.com (only Chrome, only for the 📺 button). The TV loads the video from your player with a time-limited link that only allows videos.
  • Satisfyer connects via Bluetooth from the browser or through Intiface Central in your home network – no cloud. The Handy can also run via Bluetooth through Intiface, then without the Handy cloud.

📚 Stash 0.32+

Shows “HTTP 403”? Stash rejects networks it considers public. ⚙️ → Library shows the exact fix.

Details

Stash 0.32 rejects requests from networks it considers public when Stash has no login; some Docker networks count as public. Two ways out:

  • Allow the player's network in Stash's config.yml, e.g. public_whitelist: ["172.18.0.0/16"], and restart Stash. The player shows the exact line for your network in ⚙️ → Library and in docker logs.
  • Or set a user + password in Stash and put its API key into STASH_API_KEY (or ⚙️ → Library). The key never leaves the player server.

⚙️ → Library shows the connection status and has a Test connection button; the player retries every 30 s.

Stash itself under <player>/stash: the player can also show Stash's own interface – through the player and behind its login, with the API key added by the server. Everyone logged in to the player is Stash admin there, so it can be switched off in ⚙️ → Library (or for good with STASH_UI=off).

🥽 VR (HereSphere)

Switch it on in ⚙️ → VR & HereSphere, then open the player's normal address in HereSphere – it is redirected to /heresphere automatically. VR videos by default; categories for all scenes with scripts, saved Stash filters and folders. HereSphere drives the Handy itself (enter the connection key there). HERESPHERE=off switches it off for good.

💾 Data & upgrading

Everything the player stores lives in the funscript-data volume – back it up and you have backed up everything.

What is in the volume?

Settings shared by all devices (prefs.json), patterns (store.json), server switches and login hashes (settings.json), your own scripts (scripts/, userscripts.json) and the Handy upload cache.

Upgrading from 1.19 to 2.0
  • Take the new docker-compose.yml and compare your .env with the new .env.example. Video folders are now one line: MEDIA=/path (its subfolders become sources) instead of MEDIA_FILME, MEDIA_DOWNLOADS … – or keep your own extra volume lines. The ZIP and its folder are now called funscript-player (before: handy-player); you can keep your old folder.
  • Set a login (AUTH_USER/AUTH_PASS) or use the setup page on first start.
  • Behind a reverse proxy: remove the exceptions for /stash and /media, keep only /heresphere.
  • HereSphere users: reload the library in HereSphere once (addresses changed).
Upgrading from 1.19.6 or older (container and volume were renamed)

Since 1.19.7 the container is called Funscript-Player and the volume funscript-data (before: Handy-Player and <folder>_handy-data). Copy your old data over once:

docker volume ls | grep handy-data          # find the old name, e.g. handy-player_handy-data
docker stop Handy-Player && docker rm Handy-Player
docker volume create funscript-data
docker run --rm -v handy-player_handy-data:/from -v funscript-data:/to alpine cp -a /from/. /to/
docker compose pull && docker compose up -d

❓ FAQ / Troubleshooting

  • “Handy is not online” – the Handy must be in Wi-Fi mode and online (LED blue) and the connection key right.
  • The Satisfyer doesn't show up – Bluetooth in the browser needs Chrome or Edge (iPhone: Bluefy) and HTTPS or localhost. Close the Satisfyer app first (only one connection at a time).
  • Devices are off the beat – shift the offset or tap along to calibrate. A yellow status means the Handy cloud answers slowly.
  • Stash not reachable – check the line Stash: in docker logs Funscript-Player and ⚙️ → Library → Test connection. HTTP 403 → see Stash 0.32+.
  • HQ stream says “CPU” or “not available” – enable /dev/dri in docker-compose.yml (Intel graphics only) and set RENDER_GID (getent group render).
  • Locked out – LOGIN_RESET=1 in .env, docker compose up -d, log in / set up again, remove the line.

Found a bug or have an idea? Open an issue

🗺 Coming next

  • 📺 TV streaming: a converted stream for videos the Chromecast can't play (e.g. AV1), optionally upscaled
  • Your idea? Suggest it

🛠 Development

MEDIA_DIR=./testmedia DATA_DIR=./data node server.js   # → http://localhost:8080
node --test "test/*.test.js"                           # tests incl. a browser self-test, no packages needed

Technical details: DEVELOPMENT.md · Release history: CHANGELOG.md · Want to help? CONTRIBUTING.md

Trademarks & how it is made

“The Handy”, “Satisfyer”, “Stash”, “HereSphere” and “Intiface” are names of their respective owners. This is an independent hobby project – not affiliated with, endorsed or sponsored by any of them.

Built with the help of Claude (Anthropic) – much of the code is AI-written. Every release is checked by automated tests, a browser self-test, ESLint and code scanning before it goes out.

License

Copyright © 2026 Funscript Player contributors. Licensed under the GNU AGPL-3.0: use, self-host and change it freely; if you offer a modified version to others – also as a service over the network – you have to share its source code under the same license.


Releases · Changelog · Report a bug / idea