UNPKG

ive-connect

Version:

A universal haptic device control library for interactive experiences

650 lines (649 loc) 20.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createHandyApi = exports.HandyApi = void 0; class HandyApi { constructor(baseV3Url, baseV2Url, applicationId, connectionKey = "") { this.serverTimeOffset = 0; this._eventSource = null; this.baseV3Url = baseV3Url; this.baseV2Url = baseV2Url; this.applicationId = applicationId; this.connectionKey = connectionKey; } /** * Set the connection key for API requests */ setConnectionKey(connectionKey) { this.connectionKey = connectionKey; } /** * Get the connection key */ getConnectionKey() { return this.connectionKey; } /** * Set the server time offset for synchronization */ setServerTimeOffset(offset) { this.serverTimeOffset = offset; } /** * Get the server time offset */ getServerTimeOffset() { return this.serverTimeOffset; } /** * Estimate the current server time based on local time and offset */ estimateServerTime() { return Math.round(Date.now() + this.serverTimeOffset); } /** * Get headers for API requests */ getHeaders() { return { "X-Connection-Key": this.connectionKey, Authorization: `Bearer ${this.applicationId}`, "Content-Type": "application/json", Accept: "application/json", }; } /** * Make an API request with error handling */ async request(endpoint, options = {}, useV2 = false) { try { const baseUrl = useV2 ? this.baseV2Url : this.baseV3Url; const response = await fetch(`${baseUrl}${endpoint}`, { ...options, headers: { ...this.getHeaders(), ...options.headers, }, }); const data = await response.json(); return data; } catch (error) { console.error(`API error (${endpoint}):`, error); throw error; } } /** * Check if the device is connected */ async isConnected() { var _a; try { const response = await this.request("/connected"); return !!((_a = response.result) === null || _a === void 0 ? void 0 : _a.connected); } catch (error) { console.error("Handy: Error checking connection:", error); return false; } } /** * Get device information */ async getDeviceInfo() { try { const response = await this.request("/info"); return response.result || null; } catch (error) { console.error("Handy: Error getting device info:", error); return null; } } /** * Get the current device mode */ async getMode() { var _a, _b; try { const response = await this.request("/mode"); return (_b = (_a = response.result) === null || _a === void 0 ? void 0 : _a.mode) !== null && _b !== void 0 ? _b : null; } catch (error) { console.error("Handy: Error getting mode:", error); return null; } } /** * Upload a script file to the hosting service * Returns the URL where the script can be accessed */ async uploadScript(scriptFile) { try { // Convert Blob to File if needed const file = scriptFile instanceof File ? scriptFile : new File([scriptFile], "script.funscript", { type: "application/json", }); const formData = new FormData(); formData.append("file", file); const response = await fetch(`${this.baseV2Url}/upload`, { method: "POST", body: formData, mode: "cors", credentials: "omit", }); const data = (await response.json()); return data.url || null; } catch (error) { console.error("Handy: Error uploading script:", error); return null; } } // ============================================ // HSSP (Handy Synchronized Script Protocol) // ============================================ /** * Setup script for HSSP playback */ async setupScript(scriptUrl) { var _a; try { const response = await this.request("/hssp/setup", { method: "PUT", body: JSON.stringify({ url: scriptUrl }), }); return !!((_a = response.result) === null || _a === void 0 ? void 0 : _a.stream_id); } catch (error) { console.error("Handy: Error setting up script:", error); return false; } } /** * Start playback with the HSSP protocol */ async play(videoTime, playbackRate = 1.0, loop = false) { try { const response = await this.request("/hssp/play", { method: "PUT", body: JSON.stringify({ start_time: Math.round(videoTime), server_time: this.estimateServerTime(), playback_rate: playbackRate, loop, }), }); return response.result || null; } catch (error) { console.error("Handy: Error starting playback:", error); return null; } } /** * Stop HSSP playback */ async stop() { try { const response = await this.request("/hssp/stop", { method: "PUT", }); return response.result || null; } catch (error) { console.error("Handy: Error stopping playback:", error); return null; } } /** * Synchronize the device's time with video time (HSSP) */ async syncVideoTime(videoTime, filter = 0.5) { var _a; try { const response = await this.request("/hssp/synctime", { method: "PUT", body: JSON.stringify({ current_time: Math.round(videoTime), server_time: this.estimateServerTime(), filter, }), }); return !!((_a = response.result) === null || _a === void 0 ? void 0 : _a.stream_id); } catch (error) { console.error("Handy: Error syncing video time:", error); return false; } } // ============================================ // HSP (Handy Streaming Protocol) // ============================================ /** * Setup a new HSP session on the device. * This clears any existing HSP session state. * @param streamId Optional session identifier. If not provided, one will be generated. */ async hspSetup(streamId) { try { const body = streamId ? { stream_id: streamId } : {}; const response = await this.request("/hsp/setup", { method: "PUT", body: JSON.stringify(body), }); return response.result || null; } catch (error) { console.error("Handy: Error setting up HSP:", error); return null; } } /** * Get the current HSP state */ async hspGetState() { try { const response = await this.request("/hsp/state"); return response.result || null; } catch (error) { console.error("Handy: Error getting HSP state:", error); return null; } } /** * Add points to the HSP buffer. * You can add up to 100 points in a single command. * @param points Array of points to add (max 100) * @param tailPointStreamIndex The index of the last point relative to the overall stream * @param flush If true, clears buffer before adding new points * @param tailPointThreshold Optional threshold for starving notifications */ async hspAddPoints(points, tailPointStreamIndex, flush = false, tailPointThreshold) { try { const body = { points, tail_point_stream_index: tailPointStreamIndex, flush, }; if (tailPointThreshold !== undefined) { body.tail_point_threshold = tailPointThreshold; } const response = await this.request("/hsp/add", { method: "PUT", body: JSON.stringify(body), }); return response.result || null; } catch (error) { console.error("Handy: Error adding HSP points:", error); return null; } } /** * Start HSP playback * @param startTime The start time in milliseconds * @param options Optional playback options */ async hspPlay(startTime, options = {}) { var _a; try { const body = { start_time: Math.round(startTime), server_time: (_a = options.serverTime) !== null && _a !== void 0 ? _a : this.estimateServerTime(), }; if (options.playbackRate !== undefined) { body.playback_rate = options.playbackRate; } if (options.pauseOnStarving !== undefined) { body.pause_on_starving = options.pauseOnStarving; } if (options.loop !== undefined) { body.loop = options.loop; } if (options.addPoints) { body.add = options.addPoints; } const response = await this.request("/hsp/play", { method: "PUT", body: JSON.stringify(body), }); return response.result || null; } catch (error) { console.error("Handy: Error starting HSP playback:", error); return null; } } /** * Stop HSP playback */ async hspStop() { try { const response = await this.request("/hsp/stop", { method: "PUT", }); return response.result || null; } catch (error) { console.error("Handy: Error stopping HSP:", error); return null; } } /** * Pause HSP playback */ async hspPause() { try { const response = await this.request("/hsp/pause", { method: "PUT", }); return response.result || null; } catch (error) { console.error("Handy: Error pausing HSP:", error); return null; } } /** * Resume HSP playback * @param pickUp If true, resumes from current 'live' position. If false, resumes from paused position. */ async hspResume(pickUp = false) { try { const response = await this.request("/hsp/resume", { method: "PUT", body: JSON.stringify({ pick_up: pickUp }), }); return response.result || null; } catch (error) { console.error("Handy: Error resuming HSP:", error); return null; } } /** * Flush the HSP buffer (remove all points) */ async hspFlush() { try { const response = await this.request("/hsp/flush", { method: "PUT", }); return response.result || null; } catch (error) { console.error("Handy: Error flushing HSP buffer:", error); return null; } } /** * Set the HSP loop flag */ async hspSetLoop(loop) { try { const response = await this.request("/hsp/loop", { method: "PUT", body: JSON.stringify({ loop }), }); return response.result || null; } catch (error) { console.error("Handy: Error setting HSP loop:", error); return null; } } /** * Set the HSP playback rate */ async hspSetPlaybackRate(playbackRate) { try { const response = await this.request("/hsp/playbackrate", { method: "PUT", body: JSON.stringify({ playback_rate: playbackRate }), }); return response.result || null; } catch (error) { console.error("Handy: Error setting HSP playback rate:", error); return null; } } /** * Set the HSP tail point stream index threshold */ async hspSetThreshold(threshold) { try { const response = await this.request("/hsp/threshold", { method: "PUT", body: JSON.stringify({ tail_point_threshold: threshold }), }); return response.result || null; } catch (error) { console.error("Handy: Error setting HSP threshold:", error); return null; } } /** * Set the HSP pause-on-starving flag */ async hspSetPauseOnStarving(pauseOnStarving) { try { const response = await this.request("/hsp/pause/onstarving", { method: "PUT", body: JSON.stringify({ pause_on_starving: pauseOnStarving }), }); return response.result || null; } catch (error) { console.error("Handy: Error setting HSP pause on starving:", error); return null; } } /** * Sync HSP time with external source * @param currentTime Current time from external source * @param filter Filter value for gradual adjustment (0-1) */ async hspSyncTime(currentTime, filter = 0.5) { try { const response = await this.request("/hsp/synctime", { method: "PUT", body: JSON.stringify({ current_time: Math.round(currentTime), server_time: this.estimateServerTime(), filter, }), }); return response.result || null; } catch (error) { console.error("Handy: Error syncing HSP time:", error); return null; } } // ============================================ // HSTP (Handy Simple Timing Protocol) // ============================================ /** * Get the current time offset */ async getOffset() { var _a; try { const response = await this.request("/hstp/offset"); return ((_a = response.result) === null || _a === void 0 ? void 0 : _a.offset) || 0; } catch (error) { console.error("Handy: Error getting offset:", error); return 0; } } /** * Set the time offset */ async setOffset(offset) { try { const response = await this.request("/hstp/offset", { method: "PUT", body: JSON.stringify({ offset }), }); return response.result === "ok"; } catch (error) { console.error("Handy: Error setting offset:", error); return false; } } /** * Get device time info */ async getDeviceTimeInfo() { try { const response = await this.request("/hstp/info"); return response.result || null; } catch (error) { console.error("Handy: Error getting device time info:", error); return null; } } /** * Trigger a server-device clock synchronization */ async clockSync(synchronous = true) { try { const response = await this.request(`/hstp/clocksync?s=${synchronous}`); return response.result || null; } catch (error) { console.error("Handy: Error triggering clock sync:", error); return null; } } // ============================================ // Slider Settings // ============================================ /** * Get the current stroke settings */ async getStrokeSettings() { try { const response = await this.request("/slider/stroke"); return response.result || null; } catch (error) { console.error("Handy: Error getting stroke settings:", error); return null; } } /** * Set the stroke settings */ async setStrokeSettings(settings) { try { const response = await this.request("/slider/stroke", { method: "PUT", body: JSON.stringify(settings), }); return response.result || null; } catch (error) { console.error("Handy: Error setting stroke settings:", error); return null; } } // ============================================ // Server Time Sync // ============================================ /** * Get the server time for synchronization calculations */ async getServerTime() { try { const response = await fetch(`${this.baseV3Url}/servertime`); const data = await response.json(); return data.server_time || null; } catch (error) { console.error("Handy: Error getting server time:", error); return null; } } /** * Synchronize time with the server * Returns calculated server time offset */ async syncServerTime(sampleCount = 10) { try { const samples = []; for (let i = 0; i < sampleCount; i++) { try { const start = Date.now(); const serverTime = await this.getServerTime(); if (!serverTime) continue; const end = Date.now(); const rtd = end - start; // Round trip delay const serverTimeEst = rtd / 2 + serverTime; samples.push({ rtd, offset: serverTimeEst - end, }); } catch (error) { console.warn("Error during time sync sample:", error); // Continue with other samples } } // Sort samples by RTD (Round Trip Delay) to get the most accurate ones if (samples.length > 0) { samples.sort((a, b) => a.rtd - b.rtd); // Use 80% of the most accurate samples if we have enough const usableSamples = samples.length > 3 ? samples.slice(0, Math.ceil(samples.length * 0.8)) : samples; const averageOffset = usableSamples.reduce((acc, sample) => acc + sample.offset, 0) / usableSamples.length; this.serverTimeOffset = averageOffset; return averageOffset; } return this.serverTimeOffset; } catch (error) { console.error("Error syncing time:", error); return this.serverTimeOffset; } } // ============================================ // SSE (Server-Sent Events) // ============================================ /** * Create an EventSource for server-sent events */ createEventSource() { if (this._eventSource) { this._eventSource.close(); } this._eventSource = new EventSource(`${this.baseV3Url}/sse?ck=${this.connectionKey}&apikey=${this.applicationId}`); return this._eventSource; } /** * Close the EventSource if it exists */ closeEventSource() { if (this._eventSource) { this._eventSource.close(); this._eventSource = null; } } } exports.HandyApi = HandyApi; // Factory function to create HandyApi instances const createHandyApi = (baseV3Url, baseV2Url, applicationId, connectionKey = "") => { return new HandyApi(baseV3Url, baseV2Url, applicationId, connectionKey); }; exports.createHandyApi = createHandyApi;