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;