UNPKG

ive-connect

Version:

A universal haptic device control library for interactive experiences

362 lines (361 loc) 11.3 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; } } /** * 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 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 */ 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; } } /** * 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 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; } } /** * 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; } } /** * 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;