CJ_Clippy 9a708fce1d
Some checks failed
ci / test (push) Failing after 2m41s
fp/our CI/CD / build (push) Successful in 36s
add vibeui player
2025-12-03 03:59:28 -08:00

306 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const MenuButton = videojs.getComponent('MenuButton');
const VideoJsMenuItem = videojs.getComponent('MenuItem');
/* ------------------------- Menu Components ------------------------- */
class ActuatorMenuButton extends MenuButton {
constructor(player, { actuator, funscripts = [], onAssign } = {}) {
super(player, {
title: actuator?.name ?? 'Actuator',
name: 'ActuatorMenuButton',
actuator,
funscripts,
onAssign,
});
}
createItems() {
const { actuator, funscripts, onAssign } = this.options_ || {};
if (!actuator || !funscripts?.length) return [];
return funscripts.map(funscript =>
new FunscriptMenuItem(this.player_, {
actuator,
funscript,
onAssign,
})
);
}
}
class FunscriptMenuItem extends VideoJsMenuItem {
constructor(player, { actuator, funscript, onAssign } = {}) {
super(player, { label: funscript.name, selectable: true });
this.actuator = actuator;
this.funscript = funscript;
this.onAssign = onAssign;
// Pre-select if this actuator already has this funscript assigned
this.selected(this.actuator.assignedFunscript?.url === this.funscript.url);
this.on('click', () => {
if (!this.actuator || !this.onAssign) return;
// Use centralized assignment method
this.onAssign?.(this, this.actuator, this.funscript);
});
}
}
/* ------------------------- Helper ------------------------- */
function pickInitialFunscript(actuatorType, availableFunscripts) {
actuatorType = actuatorType.toLowerCase();
if (actuatorType.includes('vibrator')) {
return availableFunscripts.find(f => f.name.includes('vibrate')) ?? availableFunscripts[0];
}
if (actuatorType.includes('thrust')) {
return availableFunscripts.find(f => f.name.includes('thrust')) ?? availableFunscripts[0];
}
return availableFunscripts[0];
}
/* ------------------------- Main Plugin ------------------------- */
class FunscriptPlayer {
constructor(player, { debug = false, funscripts, ...options } = {}) {
this.player = player;
this.debug = debug;
this.funscriptCache = new Map(); // key: url, value: parsed funscript
this.funscripts = (funscripts || []).map(f => {
if (typeof f === 'string') {
const url = f;
const name = url.split('/').pop().split('?')[0];
return { name, url };
}
return f;
});
this.options = options;
this.menuButton = null;
this.devices = [];
player.on('toyConnected', () => this.refreshMenu());
player.on('toyDisconnected', () => this.refreshMenu());
player.on('pause', () => this.stopAllDevices());
}
async init() {
const connector = new window.buttplug.ButtplugBrowserWebsocketClientConnector("ws://localhost:12345");
this.client = new window.buttplug.ButtplugClient(this.options?.buttplugClientName || "Funscripts VideoJS Client");
this.client.addListener("deviceadded", device => {
this.addDevice(device);
this.debug && console.log("Device added:", device.name, device);
this.player.trigger('toyConnected');
});
this.client.addListener("deviceremoved", device => {
this.removeDevice(device);
this.debug && console.log("Device removed:", device.name);
this.player.trigger('toyDisconnected');
});
try {
await this.client.connect(connector);
await this.client.startScanning();
this.initVideoListeners();
this.debug && console.log("[buttplug] Connected and scanning");
} catch (err) {
console.error("[buttplug] Connection error:", err);
}
}
async loadFunscript(funscript) {
try {
const response = await fetch(funscript.url);
if (!response.ok) throw new Error(`Failed to load ${funscript.name}`);
return await response.json();
} catch (err) {
console.error("[funscripts] Error loading funscript:", funscript.name, err);
return null;
}
}
getFunscriptValueAtTime(funscriptData, timeMs) {
if (!funscriptData?.actions?.length) return null;
let lastAction = null;
for (const action of funscriptData.actions) {
if (action.at <= timeMs) {
lastAction = action;
} else {
break;
}
}
return lastAction ? lastAction.pos / 100 : null; // normalize 01
}
initVideoListeners() {
this.player.on('pause', () => {
this.debug && console.log('Video paused. Stopping devices...');
this.stopAllDevices();
});
this.player.on('timeupdate', () => {
if (this.player.paused() || this.player.ended()) return;
const currentTime = this.player.currentTime() * 1000;
this.devices.forEach(device => {
device.actuators.forEach(actuator => {
const fun = actuator.assignedFunscript;
if (!fun) return;
const funscriptData = this.funscriptCache.get(fun.url);
if (!funscriptData) return;
const position = this.getFunscriptValueAtTime(funscriptData, currentTime);
this.debug && console.log(`${fun.name} position=${position}`);
if (position !== null) {
this.sendToDevice(device, actuator, position);
}
});
});
});
}
async stopAllDevices() {
if (!this.client || !this.client.devices) return;
for (const [deviceIndex, device] of this.client.devices.entries()) {
const scalarCmds = device.messageAttributes?.ScalarCmd || [];
for (const cmd of scalarCmds) {
try {
await this.sendToDevice(device, {
index: cmd.Index,
name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}`
}, 0);
} catch (err) {
console.warn(`[buttplug] Failed to stop ${device.name}`, err);
}
}
}
}
async sendToDevice(device, actuator, position) {
try {
this.client.devices[device.index].vibrate(position);
if (this.debug) {
console.log(`[buttplug] Sent to ${actuator.name}:`, position);
}
} catch (err) {
console.error("[buttplug] Error sending command:", err);
}
}
/* ------------------------- Device Tracking ------------------------- */
addDevice(device) {
const scalarCmds = device._deviceInfo?.DeviceMessages?.ScalarCmd || [];
const actuators = scalarCmds.map((cmd, i) => {
const assignedFunscript = pickInitialFunscript(cmd.ActuatorType, this.funscripts);
if (assignedFunscript && !this.funscriptCache.has(assignedFunscript.url)) {
this.loadFunscript(assignedFunscript).then(data => {
if (data) this.funscriptCache.set(assignedFunscript.url, data);
});
}
return {
name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}`,
type: cmd.ActuatorType,
index: cmd.Index,
assignedFunscript,
deviceIndex: this.devices.length, // track device index
};
});
this.devices.push({
name: device._deviceInfo.DeviceName,
index: this.devices.length,
actuators,
});
}
removeDevice(device) {
this.devices = this.devices.filter(d => d.name !== device.name);
}
/* ------------------------- Central Assignment ------------------------- */
async assignFunscript(deviceIndex, actuatorIndex, funscript) {
const actuator = this.devices[deviceIndex]?.actuators[actuatorIndex];
if (!actuator) return;
actuator.assignedFunscript = funscript;
if (!this.funscriptCache.has(funscript.url)) {
const data = await this.loadFunscript(funscript);
if (data) this.funscriptCache.set(funscript.url, data);
}
this.debug && console.log(`Assigned ${funscript.name} to ${actuator.name}`);
}
/* ------------------------- Menu Management ------------------------- */
createMenuButtons() {
if (this.menuButtons?.length) {
this.menuButtons.forEach(btn => this.player.controlBar.removeChild(btn));
}
this.menuButtons = [];
this.devices.forEach(device => {
device.actuators.forEach(actuator => {
const button = new ActuatorMenuButton(this.player, {
actuator,
funscripts: this.funscripts,
onAssign: async (item, act, funscript) => {
await this.assignFunscript(device.index, act.index, funscript);
const menuItems = item.parentComponent_.children()
.filter(c => c instanceof FunscriptMenuItem);
menuItems.forEach(i => {
i.selected(i.funscript.url === funscript.url);
});
}
});
const placementIndex = Math.max(
0,
this.options?.placementIndex ?? (this.player.controlBar.children().length - 2)
);
this.player.controlBar.addChild(button, { componentClass: 'funscriptSelector' }, placementIndex);
setTimeout(() => {
button.addClass('vjs-funscript-selector');
button.removeClass('vjs-hidden');
}, 0);
this.menuButtons.push(button);
});
});
}
refreshMenu() {
this.createMenuButtons();
}
}
/* ------------------------- Register Plugin ------------------------- */
videojs.registerPlugin("funscriptPlayer", async function (options) {
try {
const plugin = new FunscriptPlayer(this, options);
await plugin.init();
return plugin;
} catch (err) {
console.error("Funscripts plugin failed to initialize:", err);
}
});