ive-connect
Version:
A universal haptic device control library for interactive experiences
362 lines (361 loc) • 11.3 kB
JavaScript
"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;