funscript playback ui basic actuator menu list
This commit is contained in:
parent
e0e10fd926
commit
28f8b7e94e
@ -1 +1 @@
|
||||
{"last_found_secrets": [{"match": "a5cd2a1a54ccc453d07fae38adafb7b0791b4824afad85d962266e48a54be75a", "name": "Base64 Basic Authentication - commit://staged/services/tracker-helper/test/app.test.ts"}]}
|
||||
{"last_found_secrets": [{"name": "Generic High Entropy Secret - commit://staged/services/our/src/client/vod.js", "match": "3baceea0f1ce69c8007c0619c3e518cfd24135212b012f3b4d99e82fb1cced6f"}, {"name": "Generic High Entropy Secret - commit://staged/services/our/src/client/vod.js", "match": "4d24f894db33032a2666656e35e34fe3281490468e350e0be5aa8b0faabef682"}]}
|
@ -20,8 +20,7 @@ echo(" * Remote pin the IPFS CID")
|
||||
echo(" [Press Enter When Complete...]")
|
||||
_ = stdin()
|
||||
|
||||
echo(" * Create magnet link. ex: `mktorrent --web-seed https://futureporn-b2.b-cdn.net/projektmelody-fansly-2025-08-27.mp4 ~/Documents/voddo/projektmelody-fansly-2025-08-27.mp4`")
|
||||
echo(" * Create magnet link. ex: `torf ~/Downloads/projektmelody-chaturbate-2025-08-27.mp4 --notorrent --notracker`")
|
||||
echo(" * Create magnet link. ex: `imdl torrent create --link ~/Documents/voddo/projektmelody-fansly-2025-08-27.mp4`")
|
||||
echo(" [Press Enter When Complete...]")
|
||||
_ = stdin()
|
||||
|
||||
|
@ -39,6 +39,9 @@ ENV LD_LIBRARY_PATH="/app/whisper.cpp/build/src:/app/whisper.cpp/build/ggml/src:
|
||||
# Install b2-cli
|
||||
RUN wget https://github.com/Backblaze/B2_Command_Line_Tool/releases/download/v4.4.1/b2-linux -O /usr/local/bin/b2 && chmod +x /usr/local/bin/b2
|
||||
|
||||
# Install imdl
|
||||
RUN wget https://github.com/casey/intermodal/releases/download/v0.1.14/imdl-v0.1.14-x86_64-unknown-linux-musl.tar.gz -O /tmp/imdl.tar.gz && tar xvzf /tmp/imdl.tar.gz && cp /tmp/imdl /usr/local/bin/imdl
|
||||
|
||||
# Copy and install dependencies
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install --ignore-scripts=false --foreground-scripts --verbose
|
||||
|
57
services/our/package-lock.json
generated
57
services/our/package-lock.json
generated
@ -70,6 +70,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.1",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"buttplug": "^3.2.2",
|
||||
"chokidar": "^4.0.3",
|
||||
"esbuild": "^0.25.9",
|
||||
"esbuild-copy-static-files": "^0.1.0",
|
||||
@ -5527,6 +5528,19 @@
|
||||
"esbuild": ">=0.18"
|
||||
}
|
||||
},
|
||||
"node_modules/buttplug": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/buttplug/-/buttplug-3.2.2.tgz",
|
||||
"integrity": "sha512-TGkQzG6dxEjuFX29eRoWkh82vsQhGQ+E98tZtN8fWn1NOG7v/9H0FFkNXrpmeRt9FFS0GdHTvubfZ8dcIPGSAA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"class-transformer": "^0.5.1",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@ -5956,6 +5970,13 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
@ -7057,6 +7078,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
@ -10269,6 +10297,13 @@
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/require-addon": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.1.0.tgz",
|
||||
@ -12258,6 +12293,28 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
@ -26,6 +26,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.1",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"buttplug": "^3.2.2",
|
||||
"chokidar": "^4.0.3",
|
||||
"esbuild": "^0.25.9",
|
||||
"esbuild-copy-static-files": "^0.1.0",
|
||||
@ -110,4 +111,4 @@
|
||||
"prisma": {
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,269 +0,0 @@
|
||||
|
||||
// {{!--
|
||||
// Script 1: Load Buttplug.js from Skypack CDN and expose it to window.buttplug
|
||||
// --}}
|
||||
// <script type="module">
|
||||
// import {
|
||||
// ButtplugClient,
|
||||
// ButtplugBrowserWebsocketClientConnector
|
||||
// } from 'https://cdn.skypack.dev/buttplug';
|
||||
|
||||
// window.buttplug = { ButtplugClient, ButtplugBrowserWebsocketClientConnector };
|
||||
// </script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{!--
|
||||
{{!--
|
||||
Script 3: Main ButtplugPlugin class — handles connection, syncing, and device control
|
||||
--}}
|
||||
<script type="module">
|
||||
class ButtplugPlugin extends videojs.getPlugin('plugin') {
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
|
||||
this.funscripts = {}; // { vibrate: [...], thrust: [...] }
|
||||
this.activeFunscript = null;
|
||||
this.currentActionIndex = 0;
|
||||
this.lastPositionSent = null;
|
||||
this.devicePaused = false;
|
||||
this.client = new window.buttplug.ButtplugClient("future.porn");
|
||||
|
||||
this.statusIndicator = null;
|
||||
|
||||
// Preload funscripts if provided as { vibrate: url, thrust: url }
|
||||
if (options?.funscripts) {
|
||||
for (const [name, url] of Object.entries(options.funscripts)) {
|
||||
this.loadFunscript(name, url);
|
||||
}
|
||||
}
|
||||
|
||||
this.connectToIntiface();
|
||||
this.showStatusIndicator();
|
||||
|
||||
// Player event hooks
|
||||
this.on(player, 'timeupdate', this.handleTimeUpdate);
|
||||
this.on(player, 'pause', this.handlePause);
|
||||
this.on(player, 'playing', this.handlePlay);
|
||||
this.on(player, 'seeking', this.handleSeek);
|
||||
this.on(player, 'ended', this.handleEnded);
|
||||
}
|
||||
|
||||
// Inside ButtplugPlugin class
|
||||
|
||||
|
||||
// Helper to get an SVG string from your icons object
|
||||
getIcon(name, size = 20, color = 'currentColor') {
|
||||
const svg = window.icons?.[name]; // make sure icons are exposed globally
|
||||
if (!svg) return `<!-- icon "${name}" not found -->`;
|
||||
|
||||
// Remove any existing width/height/fill
|
||||
let sizedSvg = svg
|
||||
.replace(/\s(width|height|fill)="[^"]*"/g, '')
|
||||
.replace(/<svg([^>]*)>/, `<svg$1 width="${size}" height="${size}" fill="${color}">`);
|
||||
|
||||
return sizedSvg;
|
||||
}
|
||||
|
||||
// Rebuild the status indicator UI
|
||||
updateStatusIndicator() {
|
||||
if (!this.statusIndicator) return;
|
||||
|
||||
const connected = !!this.client?.connected;
|
||||
const funscriptLoaded = !!this.activeFunscript;
|
||||
|
||||
const connectionIconHtml = this.getIcon('buttplug3', 20, connected ? 'green' : 'red');
|
||||
const funscriptIconHtml = funscriptLoaded ? this.getIcon('buttplug3', 20, 'blue') : '';
|
||||
|
||||
const menuItemsHtml = Object.keys(this.funscripts).map(name => {
|
||||
const checked = this.activeFunscript === this.funscripts[name] ? 'true' : 'false';
|
||||
const selectedClass = checked === 'true' ? 'vjs-selected' : '';
|
||||
return `
|
||||
<li class="vjs-menu-item ${selectedClass}" role="menuitemradio" tabindex="-1" aria-disabled="false" aria-checked="${checked}" data-funscript-name="${name}">
|
||||
<span class="vjs-menu-item-text">${name}</span>
|
||||
<span class="vjs-control-text" aria-live="polite">${checked === 'true' ? ', selected' : ''}</span>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const noScriptChecked = !this.activeFunscript ? 'true' : 'false';
|
||||
const noScriptHtml = `
|
||||
<li class="vjs-menu-item ${noScriptChecked === 'true' ? 'vjs-selected' : ''}" role="menuitemradio" tabindex="-1" aria-disabled="false" aria-checked="${noScriptChecked}" data-funscript-name="">
|
||||
<span class="vjs-menu-item-text">No Script</span>
|
||||
<span class="vjs-control-text" aria-live="polite">${noScriptChecked === 'true' ? ', selected' : ''}</span>
|
||||
</li>
|
||||
`;
|
||||
|
||||
this.statusIndicator.el().innerHTML = `
|
||||
<div class="buttplug-status" style="display:flex; align-items:center; gap:4px;">
|
||||
${connectionIconHtml}
|
||||
${funscriptIconHtml}
|
||||
<div class="vjs-menu-button vjs-menu-button-popup vjs-control vjs-button" tabindex="0" aria-disabled="false" title="Funscript" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="vjs-button" aria-live="polite" title="Funscript">
|
||||
${this.getIcon('buttplug3', 20)} <!-- SVG icon here -->
|
||||
<span class="vjs-control-text">Funscript</span>
|
||||
</button>
|
||||
<div class="vjs-menu">
|
||||
<ul class="vjs-menu-content" role="menu">
|
||||
${noScriptHtml}
|
||||
${menuItemsHtml}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// click handlers
|
||||
const items = this.statusIndicator.el().querySelectorAll('.vjs-menu-item');
|
||||
items.forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
const name = item.dataset.funscriptName;
|
||||
this.activeFunscript = this.funscripts[name] || null;
|
||||
this.currentActionIndex = 0;
|
||||
this.lastPositionSent = null;
|
||||
this.updateStatusIndicator();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async loadFunscript(name, url) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const json = await res.json();
|
||||
this.funscripts[name] = json.actions || [];
|
||||
console.log(`[buttplug] Loaded funscript "${name}" with ${this.funscripts[name].length} actions`);
|
||||
|
||||
if (!this.activeFunscript) {
|
||||
this.activeFunscript = this.funscripts[name];
|
||||
}
|
||||
|
||||
this.updateStatusIndicator();
|
||||
} catch (e) {
|
||||
console.error(`[buttplug] Failed to load funscript "${name}":`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async connectToIntiface() {
|
||||
console.log(`[buttplug] connecting to intiface`);
|
||||
try {
|
||||
if (this.client.connected) {
|
||||
console.log("[buttplug] Already connected");
|
||||
return;
|
||||
}
|
||||
|
||||
const connector = new window.buttplug.ButtplugBrowserWebsocketClientConnector("ws://localhost:12345");
|
||||
await this.client.connect(connector);
|
||||
console.log("[buttplug] Connected to Intiface");
|
||||
this.updateStatusIndicator();
|
||||
|
||||
this.client.addListener('disconnect', this.handleDisconnect);
|
||||
this.client.addListener("deviceadded", (device) => {
|
||||
console.log(`[buttplug] Device connected: ${device.name}`);
|
||||
this.updateStatusIndicator();
|
||||
});
|
||||
this.client.addListener("deviceremoved", (device) => {
|
||||
console.log(`[buttplug] Device removed: ${device.name}`);
|
||||
this.updateStatusIndicator();
|
||||
});
|
||||
|
||||
await this.client.startScanning();
|
||||
console.log("[buttplug] Scanning for devices...");
|
||||
} catch (e) {
|
||||
console.error("[buttplug] Failed to connect to Intiface:", e);
|
||||
}
|
||||
}
|
||||
|
||||
handleDisconnect = () => {
|
||||
console.log('[Buttplug] Disconnected from Intiface');
|
||||
this.updateStatusIndicator();
|
||||
};
|
||||
|
||||
showStatusIndicator() {
|
||||
const controlBar = this.player.getChild('controlBar');
|
||||
const insertBeforeIndex = controlBar.children().length - 3;
|
||||
this.statusIndicator = controlBar.addChild('Component', {
|
||||
el: videojs.dom.createEl('div', { className: 'vjs-buttplug-indicator' })
|
||||
}, insertBeforeIndex);
|
||||
this.updateStatusIndicator();
|
||||
}
|
||||
|
||||
handleTimeUpdate = () => {
|
||||
if (!this.activeFunscript?.length || this.devicePaused) return;
|
||||
|
||||
const time = this.player.currentTime() * 1000;
|
||||
while (
|
||||
this.currentActionIndex < this.activeFunscript.length - 1 &&
|
||||
this.activeFunscript[this.currentActionIndex + 1].at <= time
|
||||
) {
|
||||
this.currentActionIndex++;
|
||||
}
|
||||
|
||||
const action = this.activeFunscript[this.currentActionIndex];
|
||||
if (!action || action.pos === this.lastPositionSent) return;
|
||||
|
||||
this.lastPositionSent = action.pos;
|
||||
this.sendToDevice(action.pos);
|
||||
};
|
||||
|
||||
async sendToDevice(position) {
|
||||
if (!this.client?.connected || this.client.devices.size === 0) {
|
||||
console.warn('[buttplug] No connected devices');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const devices = [...this.client.devices.values()];
|
||||
if (position === null) {
|
||||
await Promise.all(devices.map((d) => d.stop()));
|
||||
} else {
|
||||
const speed = position / 100;
|
||||
await Promise.all(devices.map((d) => d.vibrate?.(speed)));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[buttplug] Failed to send to device:", e);
|
||||
}
|
||||
}
|
||||
|
||||
handlePause = () => {
|
||||
this.devicePaused = true;
|
||||
this.sendToDevice(null);
|
||||
};
|
||||
|
||||
handlePlay = () => {
|
||||
this.devicePaused = false;
|
||||
};
|
||||
|
||||
handleEnded = () => {
|
||||
this.currentActionIndex = 0;
|
||||
this.lastPositionSent = null;
|
||||
this.devicePaused = true;
|
||||
this.sendToDevice(null);
|
||||
};
|
||||
|
||||
handleSeek = () => {
|
||||
if (!this.activeFunscript) return;
|
||||
const time = this.player.currentTime() * 1000;
|
||||
this.currentActionIndex = 0;
|
||||
for (let i = 0; i < this.activeFunscript.length; i++) {
|
||||
if (this.activeFunscript[i].at > time) break;
|
||||
this.currentActionIndex = i;
|
||||
}
|
||||
this.lastPositionSent = null;
|
||||
};
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
if (this.client?.connected) {
|
||||
this.client.disconnect().then(() => console.log("[buttplug] Client disconnected"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
videojs.registerPlugin('buttplug', ButtplugPlugin);
|
||||
</script>
|
||||
|
||||
--}}
|
31
services/our/src/client/downloader/download.js
Normal file
31
services/our/src/client/downloader/download.js
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Downloads a file from a given CDN URL and triggers a browser download.
|
||||
*
|
||||
* @async
|
||||
* @function download
|
||||
* @param {string} cdnUrl - The URL of the file to download.
|
||||
* @param {string} fileName - The name to give the downloaded file (including extension).
|
||||
* @returns {Promise<void>} A promise that resolves once the download has been triggered.
|
||||
*
|
||||
* @example
|
||||
* // Download an image and save as picture.jpg
|
||||
* await download('https://example.com/image.jpg', 'picture.jpg');
|
||||
*/
|
||||
export async function download(cdnUrl, fileName) {
|
||||
console.log(`downloading cdnUrl=${cdnUrl} fileName=${fileName}`)
|
||||
try {
|
||||
const response = await fetch(cdnUrl);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
import videojs from 'video.js'
|
||||
|
||||
const MenuButton = videojs.getComponent('MenuButton');
|
||||
const Menu = videojs.getComponent('Menu');
|
||||
const Component = videojs.getComponent('Component');
|
||||
const Dom = videojs.dom;
|
||||
|
||||
/**
|
||||
* Convert string to title case.
|
||||
*
|
||||
* @param {string} string - the string to convert
|
||||
* @return {string} the returned titlecase string
|
||||
*/
|
||||
function toTitleCase(string) {
|
||||
if (typeof string !== 'string') {
|
||||
return string;
|
||||
}
|
||||
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend vjs button class for funscripts button.
|
||||
*/
|
||||
export default class ConcreteButton extends MenuButton {
|
||||
|
||||
/**
|
||||
* Button constructor.
|
||||
*
|
||||
* @param {Player} player - videojs player instance
|
||||
*/
|
||||
constructor(player) {
|
||||
super(player, {
|
||||
title: player.localize('Funscript'),
|
||||
name: 'ScriptButton'
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates button items.
|
||||
*
|
||||
* @return {Array} - Button items
|
||||
*/
|
||||
createItems() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the menu and add all items to it.
|
||||
*
|
||||
* @return {Menu}
|
||||
* The constructed menu
|
||||
*/
|
||||
createMenu() {
|
||||
const menu = new Menu(this.player_, { menuButton: this });
|
||||
|
||||
this.hideThreshold_ = 0;
|
||||
|
||||
// Add a title list item to the top
|
||||
if (this.options_.title) {
|
||||
const titleEl = Dom.createEl('li', {
|
||||
className: 'vjs-menu-title',
|
||||
innerHTML: toTitleCase(this.options_.title),
|
||||
tabIndex: -1
|
||||
});
|
||||
const titleComponent = new Component(this.player_, { el: titleEl });
|
||||
|
||||
this.hideThreshold_ += 1;
|
||||
|
||||
menu.addItem(titleComponent);
|
||||
}
|
||||
|
||||
this.items = this.createItems();
|
||||
|
||||
if (this.items) {
|
||||
// Add menu items to the menu
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
menu.addItem(this.items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return menu;
|
||||
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
import videojs from 'video.js'
|
||||
// Concrete classes
|
||||
const VideoJsMenuItemClass = videojs.getComponent('MenuItem');
|
||||
|
||||
/**
|
||||
* Extend vjs menu item class.
|
||||
*/
|
||||
export default class ConcreteMenuItem extends VideoJsMenuItemClass {
|
||||
|
||||
/**
|
||||
* Menu item constructor.
|
||||
*
|
||||
* @param {Player} player - vjs player
|
||||
* @param {Object} item - Item object
|
||||
* @param {ConcreteButton} scriptButton - The containing button.
|
||||
* @param {FunscriptSelector} plugin - This plugin instance.
|
||||
*/
|
||||
constructor(player, item, scriptButton, plugin) {
|
||||
super(player, {
|
||||
label: item.label,
|
||||
selectable: true,
|
||||
selected: item.selected || false
|
||||
});
|
||||
this.item = item;
|
||||
this.scriptButton = scriptButton;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click event for menu item.
|
||||
*/
|
||||
handleClick() {
|
||||
|
||||
// Reset other menu items selected status.
|
||||
for (let i = 0; i < this.scriptButton.items.length; ++i) {
|
||||
this.scriptButton.items[i].selected(false);
|
||||
}
|
||||
|
||||
// Set this menu item to selected, and set quality.
|
||||
this.plugin.setFunscript(this.item.value);
|
||||
this.selected(true);
|
||||
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
inspired by https://github.com/chrisboustead/videojs-hls-quality-selector/tree/main
|
@ -1,133 +0,0 @@
|
||||
import videojs from 'video.js'
|
||||
import ConcreteButton from './ConcreteButton.js';
|
||||
import ConcreteMenuItem from './ConcreteMenuItem.js';
|
||||
import './plugin.css'
|
||||
|
||||
const VERSION = "0.0.1"
|
||||
const Plugin = videojs.getPlugin('plugin');
|
||||
const defaults = {};
|
||||
|
||||
/**
|
||||
* Funscript Selector Plugin
|
||||
*/
|
||||
class FunscriptSelector extends Plugin {
|
||||
|
||||
constructor(player, options) {
|
||||
super(player);
|
||||
|
||||
this.options = videojs.obj.merge(defaults, options);
|
||||
|
||||
this.player.ready(() => {
|
||||
this.player.addClass('vjs-funscript-selector');
|
||||
this.createFunscriptButton();
|
||||
});
|
||||
|
||||
// Player event hooks
|
||||
// this.on(player, 'timeupdate', this.handleTimeUpdate);
|
||||
this.on(player, 'pause', this.handlePause);
|
||||
this.on(player, 'playing', this.handlePlay);
|
||||
this.on(player, 'seeking', this.handleSeek);
|
||||
this.on(player, 'ended', this.handleEnded);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
handlePause = () => {
|
||||
this.devicePaused = true;
|
||||
// this.sendToDevice(null); // @todo
|
||||
};
|
||||
|
||||
handlePlay = () => {
|
||||
this.devicePaused = false;
|
||||
};
|
||||
|
||||
handleEnded = () => {
|
||||
this.currentActionIndex = 0;
|
||||
this.lastPositionSent = null;
|
||||
this.devicePaused = true;
|
||||
// this.sendToDevice(null); // @todo
|
||||
};
|
||||
|
||||
handleSeek = () => {
|
||||
if (!this.activeFunscript) return;
|
||||
const time = this.player.currentTime() * 1000;
|
||||
this.currentActionIndex = 0;
|
||||
for (let i = 0; i < this.activeFunscript.length; i++) {
|
||||
if (this.activeFunscript[i].at > time) break;
|
||||
this.currentActionIndex = i;
|
||||
}
|
||||
this.lastPositionSent = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the funscript menu button
|
||||
*/
|
||||
createFunscriptButton() {
|
||||
const player = this.player;
|
||||
|
||||
this._funscriptButton = new ConcreteButton(player);
|
||||
|
||||
const placementIndex = player.controlBar.children().length - 2;
|
||||
const buttonInstance = player.controlBar.addChild(
|
||||
this._funscriptButton,
|
||||
{ componentClass: 'funscriptSelector' },
|
||||
this.options.placementIndex || placementIndex
|
||||
);
|
||||
|
||||
buttonInstance.addClass('vjs-funscript-selector');
|
||||
this._funscriptButton.createItems = () => this.createFunscriptItems();
|
||||
this._funscriptButton.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create funscript menu items
|
||||
*/
|
||||
createFunscriptItems() {
|
||||
const player = this.player;
|
||||
|
||||
const funscripts = [
|
||||
{
|
||||
label: 'Vibrate',
|
||||
value: player.el().querySelector('#funscript-vibrate')?.dataset.url,
|
||||
},
|
||||
{
|
||||
label: 'Thrust',
|
||||
value: player.el().querySelector('#funscript-thrust')?.dataset.url,
|
||||
}
|
||||
];
|
||||
|
||||
return funscripts.map(item => this.getFunscriptMenuItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a ConcreteMenuItem for the funscript
|
||||
*/
|
||||
getFunscriptMenuItem(item) {
|
||||
return new ConcreteMenuItem(this.player, item, this._funscriptButton, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ConcreteMenuItem when clicked
|
||||
*/
|
||||
setFunscript(url) {
|
||||
this._currentFunscript = url;
|
||||
|
||||
// Trigger a custom event so your Alpine.js or other logic can pick it up
|
||||
this.player.trigger('funscriptSelected', { url });
|
||||
}
|
||||
|
||||
getCurrentFunscript() {
|
||||
return this._currentFunscript;
|
||||
}
|
||||
}
|
||||
|
||||
// Include the version number.
|
||||
FunscriptSelector.VERSION = VERSION;
|
||||
|
||||
|
||||
videojs.registerPlugin('funscripts', FunscriptSelector);
|
||||
|
||||
export default FunscriptSelector;
|
||||
|
||||
|
353
services/our/src/client/videojs-funscripts/Funscripts.js
Normal file
353
services/our/src/client/videojs-funscripts/Funscripts.js
Normal file
@ -0,0 +1,353 @@
|
||||
import videojs from 'video.js';
|
||||
import { ButtplugClient, ButtplugBrowserWebsocketClientConnector } from 'buttplug';
|
||||
import './plugin.css';
|
||||
|
||||
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;
|
||||
|
||||
// Assign this funscript
|
||||
this.actuator.assignedFunscript = this.funscript;
|
||||
|
||||
// Update UI: only this item is selected
|
||||
this.parentComponent_.children()
|
||||
.filter(c => c instanceof FunscriptMenuItem)
|
||||
.forEach(c => c.selected(c === this));
|
||||
|
||||
// Call external callback
|
||||
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 ------------------------- */
|
||||
|
||||
export default class FunscriptPlayer {
|
||||
constructor(player, { debug = false, funscripts, ...options } = {}) {
|
||||
this.player = player;
|
||||
this.debug = debug;
|
||||
this.funscriptCache = new Map(); // key: url, value: parsed funscript
|
||||
// Normalize funscripts: ensure they’re always { name, url }
|
||||
this.funscripts = (funscripts || []).map(f => {
|
||||
if (typeof f === 'string') {
|
||||
// Extract filename from URL
|
||||
const url = f;
|
||||
const name = url.split('/').pop().split('?')[0]; // remove query params
|
||||
return { name, url };
|
||||
}
|
||||
// Already in { name, url } format
|
||||
return f;
|
||||
});
|
||||
this.options = options;
|
||||
|
||||
this.menuButton = null;
|
||||
this.devices = []; // track connected devices
|
||||
|
||||
// refresh menu when devices change
|
||||
player.on('toyConnected', () => this.refreshMenu());
|
||||
player.on('toyDisconnected', () => this.refreshMenu());
|
||||
player.on('pause', () => this.stopAllDevices());
|
||||
}
|
||||
|
||||
async init() {
|
||||
const connector = new ButtplugBrowserWebsocketClientConnector("ws://localhost:12345");
|
||||
this.client = new 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;
|
||||
|
||||
this.debug && console.log(`getFunscriptValueAtTime timeMs=${timeMs}`)
|
||||
|
||||
// Find the latest action at or before current time
|
||||
let lastAction = null;
|
||||
for (const action of funscriptData.actions) {
|
||||
if (action.at <= timeMs) {
|
||||
lastAction = action;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return lastAction ? lastAction.pos / 100 : null; // normalize 0–1
|
||||
}
|
||||
|
||||
|
||||
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; // exit early if not playing. necessary for seeking while paused
|
||||
|
||||
const currentTime = this.player.currentTime() * 1000; // ms
|
||||
// this.debug && console.log(`timeupdate currentTime=${currentTime}`)
|
||||
|
||||
this.devices.forEach(device => {
|
||||
device.actuators.forEach(actuator => {
|
||||
const fun = actuator.assignedFunscript;
|
||||
if (!fun) return;
|
||||
// this.debug && console.log(`name=${fun.name} url=${fun.url}`)
|
||||
|
||||
const funscriptData = this.funscriptCache.get(fun.url);
|
||||
// console.log(funscriptData)
|
||||
if (!funscriptData) return;
|
||||
|
||||
const position = this.getFunscriptValueAtTime(funscriptData, currentTime);
|
||||
// this.debug && console.log(`position=${position}`)
|
||||
|
||||
if (position !== null) {
|
||||
this.debug && console.log(`actuator.name=${actuator.name}, position=${position}`)
|
||||
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 {
|
||||
// console.log()
|
||||
// await this.client.devices[device.index].scalar(actuator.index, position);
|
||||
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);
|
||||
|
||||
// kick off preload asynchronously, but don’t block UI
|
||||
if (assignedFunscript && !this.funscriptCache.has(assignedFunscript.url)) {
|
||||
this.loadFunscript(assignedFunscript).then(data => {
|
||||
if (data) this.funscriptCache.set(assignedFunscript.url, data);
|
||||
this.debug && console.log("Preloaded funscript:", assignedFunscript.name);
|
||||
}).catch(err => {
|
||||
console.error("Error preloading funscript:", assignedFunscript.name, err);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}`,
|
||||
type: cmd.ActuatorType,
|
||||
index: cmd.Index,
|
||||
assignedFunscript,
|
||||
};
|
||||
});
|
||||
|
||||
this.devices.push({
|
||||
name: device._deviceInfo.DeviceName,
|
||||
index: this.devices.length,
|
||||
actuators,
|
||||
});
|
||||
|
||||
this.debug && console.log("Processed device:", device._deviceInfo.DeviceName, "actuators:", actuators);
|
||||
}
|
||||
|
||||
|
||||
removeDevice(device) {
|
||||
this.devices = this.devices.filter(d => d.name !== device.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ------------------------- Menu Management ------------------------- */
|
||||
|
||||
createMenuButtons() {
|
||||
// Remove existing menu buttons
|
||||
if (this.menuButtons?.length) {
|
||||
this.menuButtons.forEach(btn => this.player.controlBar.removeChild(btn));
|
||||
}
|
||||
this.menuButtons = [];
|
||||
|
||||
const actuators = this.devices.flatMap(device =>
|
||||
device.actuators.map(actuator => ({
|
||||
...actuator,
|
||||
deviceName: device.name,
|
||||
deviceIndex: device.index,
|
||||
}))
|
||||
);
|
||||
|
||||
actuators.forEach(actuator => {
|
||||
const button = new ActuatorMenuButton(this.player, {
|
||||
actuator,
|
||||
funscripts: this.funscripts,
|
||||
onAssign: async (item, act, funscript) => {
|
||||
act.assignedFunscript = funscript;
|
||||
this.debug && console.log(`assignedFunscript = ${funscript.name}`);
|
||||
|
||||
// Load and cache funscript
|
||||
if (!this.funscriptCache.has(funscript.url)) {
|
||||
const data = await this.loadFunscript(funscript);
|
||||
console.log('load and cache funscript') // this is never being called by default.
|
||||
if (data) this.funscriptCache.set(funscript.url, data);
|
||||
}
|
||||
|
||||
// Update UI
|
||||
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);
|
||||
});
|
||||
|
||||
if (this.debug) {
|
||||
const actuatorNames = this.menuButtons.map(b => b.options_.title);
|
||||
console.log("Menu buttons created for actuators:", actuatorNames);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
1
services/our/src/client/videojs-funscripts/README.md
Normal file
1
services/our/src/client/videojs-funscripts/README.md
Normal file
@ -0,0 +1 @@
|
||||
menu selector code copied from https://github.com/chrisboustead/videojs-hls-quality-selector/tree/main
|
61
services/our/src/client/videojs-funscripts/deviceMap.js
Normal file
61
services/our/src/client/videojs-funscripts/deviceMap.js
Normal file
@ -0,0 +1,61 @@
|
||||
// deviceMap.js
|
||||
export default class DeviceMap {
|
||||
constructor() {
|
||||
// Map of deviceIndex => { name, funscript, index }
|
||||
this.devices = new Map();
|
||||
}
|
||||
|
||||
// addDevice(device) {
|
||||
// if (this.devices.has(device.index)) return;
|
||||
|
||||
// this.devices.set(device.index, {
|
||||
// name: device.name,
|
||||
// funscript: null,
|
||||
// });
|
||||
|
||||
// console.log(`[DeviceMap] Added device "${device.name}", index=${device.index}`);
|
||||
// }
|
||||
|
||||
addDevice(device) {
|
||||
const scalarCmds = device._deviceInfo?.DeviceMessages?.ScalarCmd || [];
|
||||
|
||||
const actuators = scalarCmds.map((cmd, i) => {
|
||||
return {
|
||||
name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}`,
|
||||
type: cmd.ActuatorType,
|
||||
index: cmd.Index,
|
||||
reportedValue: cmd.Default ?? 0, // example, could use other reported info
|
||||
assignedFunscript: null, // initially empty
|
||||
};
|
||||
});
|
||||
|
||||
this.devices.push({
|
||||
name: device._deviceInfo.DeviceName,
|
||||
index: this.devices.length,
|
||||
actuators
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
removeDevice(device) {
|
||||
this.devices.delete(device.index);
|
||||
console.log(`[DeviceMap] Removed device "${device.name}"`);
|
||||
}
|
||||
|
||||
getDevices() {
|
||||
// Return array for easy menu mapping
|
||||
return Array.from(this.devices.entries()).map(([index, { name, funscript }]) => ({
|
||||
index,
|
||||
name,
|
||||
funscript,
|
||||
}));
|
||||
}
|
||||
|
||||
assignFunscript(deviceIndex, funscript) {
|
||||
const d = this.devices.get(deviceIndex);
|
||||
if (!d) return;
|
||||
|
||||
d.funscript = funscript;
|
||||
console.log(`[DeviceMap] Assigned funscript to "${d.name}"`);
|
||||
}
|
||||
}
|
35
services/our/src/client/videojs-funscripts/devices.js
Normal file
35
services/our/src/client/videojs-funscripts/devices.js
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
|
||||
export function mapDeviceToFunscriptFeatures(device) {
|
||||
var mappings = [];
|
||||
|
||||
if (!device || typeof device !== "object") return mappings;
|
||||
|
||||
var attrs = device.messageAttributes || {};
|
||||
|
||||
// Helper to safely iterate
|
||||
function forEachFeature(arr, actuatorType, funscriptType) {
|
||||
if (!arr) return;
|
||||
try {
|
||||
// coerce to array in case it's undefined or not an array
|
||||
var features = Array.isArray(arr) ? arr : [];
|
||||
features.forEach(function (f, i) {
|
||||
mappings.push({
|
||||
actuatorIndex: typeof f.Index === "number" ? f.Index : i,
|
||||
actuatorType: actuatorType,
|
||||
funscriptType: funscriptType,
|
||||
stepCount: typeof f.StepCount === "number" ? f.StepCount : 100
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("[buttplug] Failed to process features for", actuatorType, err);
|
||||
}
|
||||
}
|
||||
|
||||
forEachFeature(attrs.VibrateCmd, "vibrate", "Vibrate");
|
||||
forEachFeature(attrs.LinearCmd, "linear", "Oscillate");
|
||||
forEachFeature(attrs.RotateCmd, "rotate", "Rotate");
|
||||
|
||||
return mappings;
|
||||
}
|
11
services/our/src/client/videojs-funscripts/dom.js
Normal file
11
services/our/src/client/videojs-funscripts/dom.js
Normal file
@ -0,0 +1,11 @@
|
||||
export function collectFunscripts() {
|
||||
return Array.from(document.querySelectorAll('.funscript'))
|
||||
.map(div => {
|
||||
const url = div.getAttribute('data-url');
|
||||
if (!url) return null;
|
||||
|
||||
const name = url.split('/').pop().split('?')[0]; // filename without query
|
||||
return { name, url };
|
||||
})
|
||||
.filter(Boolean); // remove nulls
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.vjs-menu-button.vjs-funscript-selector button::before {
|
||||
@ -12,7 +13,5 @@
|
||||
display: block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
background: no-repeat center/contain url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-5 -10 51.480372 52.012249'><path d='m 20.221554,24.21173 3.6797,3.6797 c 0.53906,0.53906 0.73047,1.3281 0.48828,2.058599 l -1.6211,4.878901 c -0.51953,1.558599 -0.30859,3.261698 0.578121,4.6719 0.980469,1.539099 2.952215,2.636214 4.730499,2.5 5.799113,-1.246595 18.520815,-15.042558 18.4025,-18.562601 -0.200697,-2.557237 -2.4414,-5.441399 -5.4414,-5.441399 -0.57813,0 -1.1602,0.08984 -1.7188,0.28125 -1.808761,0.530862 -3.664125,1.462123 -5.48046,1.71874 -0.533759,0.07541 -1.078099,-0.21094 -1.4492,-0.57812 l -3.6797,-3.679701 C 31.639694,10.239 30.889819,4.5008743 26.659194,0.2699988 22.42882,-3.960626 11.331194,-10.000001 3.4091936,-10 c -16.3546746,0.5198707 -4.9007052,25.702135 1.3475999,32.1723 4.7260967,4.893907 9.7749655,4.584497 15.4647605,2.03943 z' fill='currentColor'/></svg>");
|
||||
color: cyan;
|
||||
/* SVG fill via currentColor */
|
||||
background: no-repeat center/contain url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-5 -10 51.480372 52.012249'><path d='m 20.221554,24.21173 3.6797,3.6797 c 0.53906,0.53906 0.73047,1.3281 0.48828,2.058599 l -1.6211,4.878901 c -0.51953,1.558599 -0.30859,3.261698 0.578121,4.6719 0.980469,1.539099 2.952215,2.636214 4.730499,2.5 5.799113,-1.246595 18.520815,-15.042558 18.4025,-18.562601 -0.200697,-2.557237 -2.4414,-5.441399 -5.4414,-5.441399 -0.57813,0 -1.1602,0.08984 -1.7188,0.28125 -1.808761,0.530862 -3.664125,1.462123 -5.48046,1.71874 -0.533759,0.07541 -1.078099,-0.21094 -1.4492,-0.57812 l -3.6797,-3.679701 C 31.639694,10.239 30.889819,4.5008743 26.659194,0.2699988 22.42882,-3.960626 11.331194,-10.000001 3.4091936,-10 c -16.3546746,0.5198707 -4.9007052,25.702135 1.3475999,32.1723 4.7260967,4.893907 9.7749655,4.584497 15.4647605,2.03943 z' fill='white'/></svg>");
|
||||
}
|
@ -1,19 +1,33 @@
|
||||
|
||||
import videojs from 'video.js'
|
||||
import 'video.js/dist/video-js.min.css'
|
||||
|
||||
// import 'videojs-contrib-quality-levels'
|
||||
import { download } from './downloader/download.js'
|
||||
import { collectFunscripts } from './videojs-funscripts/dom.js'
|
||||
import 'videojs-hls-quality-selector'
|
||||
import './videojs-buttplug/plugin.js'
|
||||
import './videojs-funscripts/Funscripts.js'
|
||||
|
||||
window.download = download
|
||||
|
||||
window.HELP_IMPROVE_VIDEOJS = false; // disable videojs tracking
|
||||
|
||||
var player = videojs('#player');
|
||||
|
||||
|
||||
const player = videojs('player');
|
||||
|
||||
player.ready(() => {
|
||||
|
||||
player.funscripts();
|
||||
// player.qualityLevels();
|
||||
|
||||
|
||||
// set up plugins
|
||||
const funscripts = collectFunscripts()
|
||||
const funscriptsOptions = {
|
||||
buttplugClientName: "future.porn",
|
||||
debug: false,
|
||||
funscripts,
|
||||
}
|
||||
|
||||
player.funscriptPlayer(funscriptsOptions);
|
||||
|
||||
|
||||
player.hlsQualitySelector({
|
||||
displayCurrentQuality: true,
|
||||
|
@ -40,16 +40,16 @@ const scheduleVodProcessing: Task = async (payload: unknown, helpers) => {
|
||||
const jobs: Promise<Job>[] = [];
|
||||
|
||||
jobs.push(helpers.addJob("copyV1S3ToV2", { vodId }));
|
||||
// if (!vod.sourceVideo) jobs.push(helpers.addJob("getSourceVideo", { vodId }));
|
||||
// if (!vod.sourceVideoDuration) jobs.push(helpers.addJob("getSourceVideoMetadata", { vodId }))
|
||||
// if (!vod.sha256sum) jobs.push(helpers.addJob("generateVideoChecksum", { vodId }));
|
||||
// if (!vod.thumbnail) jobs.push(helpers.addJob("createVideoThumbnail", { vodId }));
|
||||
// if (!vod.hlsPlaylist) jobs.push(helpers.addJob("createHlsPlaylist", { vodId }));
|
||||
// if (!vod.cidv1) jobs.push(helpers.addJob("createIpfsCid", { vodId }));
|
||||
if (!vod.sourceVideo) jobs.push(helpers.addJob("getSourceVideo", { vodId }));
|
||||
if (!vod.sourceVideoDuration) jobs.push(helpers.addJob("getSourceVideoMetadata", { vodId }))
|
||||
if (!vod.sha256sum) jobs.push(helpers.addJob("generateVideoChecksum", { vodId }));
|
||||
if (!vod.thumbnail) jobs.push(helpers.addJob("createVideoThumbnail", { vodId }));
|
||||
if (!vod.hlsPlaylist) jobs.push(helpers.addJob("createHlsPlaylist", { vodId }));
|
||||
if (!vod.cidv1) jobs.push(helpers.addJob("createIpfsCid", { vodId }));
|
||||
if (!vod.funscriptVibrate || !vod.funscriptThrust) jobs.push(helpers.addJob("createFunscript", { vodId }));
|
||||
// if (!vod.asrVttKey) jobs.push(helpers.addJob("createTranscription", { vodId }));
|
||||
// if (!vod.slvttVTTKey) jobs.push(helpers.addJob("createStoryboard", { vodId }));
|
||||
// if (!vod.magnetLink) jobs.push(helpers.addJob("createTorrent", { vodId }));
|
||||
if (!vod.asrVttKey) jobs.push(helpers.addJob("createTranscription", { vodId }));
|
||||
if (!vod.slvttVTTKey) jobs.push(helpers.addJob("createStoryboard", { vodId }));
|
||||
if (!vod.magnetLink) jobs.push(helpers.addJob("createTorrent", { vodId }));
|
||||
|
||||
const changes = jobs.length;
|
||||
if (changes > 0) {
|
||||
|
@ -47,7 +47,10 @@
|
||||
data-setup='{}' data-playlist="{{signedHlsUrl vod.hlsPlaylist}}">
|
||||
<source src="/hls/{{vod.id}}/master.m3u8" type="application/x-mpegURL">
|
||||
{{#if (hasRole "supporterTier1" user)}}
|
||||
<track kind="captions" src="{{getCdnUrl vod.asrVttKey}}" srclang="en" label="English" default>{{/if}}
|
||||
<track kind="captions" src="{{getCdnUrl vod.asrVttKey}}" srclang="en" label="English" default>
|
||||
<div class="funscript" data-url="{{getCdnUrl vod.funscriptVibrate}}"></div>
|
||||
<div class="funscript" data-url="{{getCdnUrl vod.funscriptThrust}}"></div>
|
||||
{{/if}}
|
||||
<p class="vjs-no-js">
|
||||
To view this video please enable JavaScript, and consider upgrading to a
|
||||
web browser that
|
||||
@ -372,27 +375,6 @@
|
||||
|
||||
|
||||
|
||||
async function download(cdnUrl, fileName) {
|
||||
|
||||
console.log(`downloading cdnUrl=${cdnUrl} fileName=${fileName}`)
|
||||
try {
|
||||
const response = await fetch(cdnUrl);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@ -457,7 +439,6 @@ Script 4: Initialize the buttplug plugin and pass in the funscript URL from the
|
||||
</script> --}}
|
||||
|
||||
|
||||
|
||||
<script type="module" src="/assets/vod.js"></script>
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user