eye-analysis
Version:
Eye Analysis - Browser-based eye tracking and screen recording library for research and experiments
432 lines (430 loc) • 15.3 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
// recorder/storage.ts
var DB_NAME = "EyeAnalysisDB", DB_VERSION = 4, getDb = () => {
return globalThis.__eyeAnalysisDB || null;
}, setDb = (database) => {
globalThis.__eyeAnalysisDB = database;
}, initializeStorage = () => {
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (currentDb) {
resolve();
return;
}
const request = indexedDB.open(DB_NAME, DB_VERSION);
setupDatabase(request, resolve, reject);
});
}, setupDatabase = (request, resolve, reject) => {
request.onerror = () => {
reject(new Error("Failed to open database"));
};
request.onsuccess = () => {
setDb(request.result);
resolve();
};
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains("sessions")) {
const sessionStore = database.createObjectStore("sessions", {
keyPath: "sessionId"
});
sessionStore.createIndex("participantId", "participantId", {
unique: false
});
sessionStore.createIndex("startTime", "startTime", { unique: false });
}
if (!database.objectStoreNames.contains("events")) {
const eventStore = database.createObjectStore("events", {
keyPath: "id"
});
eventStore.createIndex("sessionId", "sessionId", { unique: false });
eventStore.createIndex("timestamp", "timestamp", { unique: false });
}
if (!database.objectStoreNames.contains("gazeData")) {
const gazeStore = database.createObjectStore("gazeData", {
autoIncrement: true
});
gazeStore.createIndex("sessionId", "sessionId", { unique: false });
gazeStore.createIndex("systemTimestamp", "systemTimestamp", {
unique: false
});
}
if (!database.objectStoreNames.contains("videoChunks")) {
const videoStore = database.createObjectStore("videoChunks", {
keyPath: "id"
});
videoStore.createIndex("sessionId", "sessionId", { unique: false });
videoStore.createIndex("timestamp", "timestamp", { unique: false });
}
};
}, resetDatabase = () => {
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (currentDb) {
currentDb.close();
setDb(null);
}
const deleteRequest = indexedDB.deleteDatabase(DB_NAME);
deleteRequest.onsuccess = () => {
resolve();
};
deleteRequest.onerror = () => {
reject(new Error("Failed to reset database"));
};
});
}, saveSession = async (session) => {
await ensureDatabaseReady();
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (!currentDb) {
reject(new Error("Database not initialized"));
return;
}
const transaction = currentDb.transaction(["sessions"], "readwrite");
const store = transaction.objectStore("sessions");
const request = store.put(session);
request.onsuccess = () => resolve();
request.onerror = () => reject(new Error("Failed to save session"));
});
}, getSession = async (sessionId) => {
await ensureDatabaseReady();
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (!currentDb) {
reject(new Error("Database not initialized"));
return;
}
const transaction = currentDb.transaction(["sessions"], "readonly");
const store = transaction.objectStore("sessions");
const request = store.get(sessionId);
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(new Error("Failed to get session"));
});
}, saveEvent = (event) => {
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (!currentDb) {
reject(new Error("Database not initialized"));
return;
}
const transaction = currentDb.transaction(["events"], "readwrite");
const store = transaction.objectStore("events");
const request = store.add(event);
request.onsuccess = () => resolve();
request.onerror = () => reject(new Error("Failed to save event"));
});
}, saveGazeData = (sessionId, gazePoint) => {
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (!currentDb) {
reject(new Error("Database not initialized"));
return;
}
const dataWithSession = {
...gazePoint,
storageId: `${sessionId}-${gazePoint.systemTimestamp}-${Math.random()}`
};
const transaction = currentDb.transaction(["gazeData"], "readwrite");
const store = transaction.objectStore("gazeData");
const request = store.add(dataWithSession);
request.onsuccess = () => resolve();
request.onerror = () => reject(new Error("Failed to save gaze data"));
});
}, saveVideoChunk = (chunk) => {
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (!currentDb) {
reject(new Error("Database not initialized"));
return;
}
const transaction = currentDb.transaction(["videoChunks"], "readwrite");
const store = transaction.objectStore("videoChunks");
const request = store.add(chunk);
request.onsuccess = () => resolve();
request.onerror = () => reject(new Error("Failed to save video chunk"));
});
}, ensureDatabaseReady = (retries = 10, delay = 500) => {
return new Promise((resolve, reject) => {
const check = (attemptsLeft) => {
const currentDb = getDb();
if (currentDb && currentDb.version === DB_VERSION) {
resolve();
return;
}
if (attemptsLeft === 0) {
reject(new Error(`Database not initialized or ready after ${retries} attempts. Current db: ${currentDb ? `version ${currentDb.version}` : "null"}`));
return;
}
setTimeout(() => check(attemptsLeft - 1), delay);
};
check(retries);
});
}, getSessionData = async (sessionId, options) => {
await ensureDatabaseReady();
const currentDb = getDb();
if (!currentDb) {
throw new Error("Database not initialized");
}
const session = await getSession(sessionId);
if (!session) {
throw new Error("Session not found");
}
const events = await new Promise((resolveEvents, rejectEvents) => {
const currentDb2 = getDb();
if (!currentDb2) {
rejectEvents(new Error("Database not initialized"));
return;
}
const transaction = currentDb2.transaction(["events"], "readonly");
const store = transaction.objectStore("events");
const index = store.index("sessionId");
const request = index.getAll(sessionId);
request.onsuccess = () => resolveEvents(request.result);
request.onerror = () => rejectEvents(new Error("Failed to get events"));
});
const gazeData = await new Promise((resolveGaze, rejectGaze) => {
const currentDb2 = getDb();
if (!currentDb2) {
rejectGaze(new Error("Database not initialized"));
return;
}
const transaction = currentDb2.transaction(["gazeData"], "readonly");
const store = transaction.objectStore("gazeData");
const index = store.index("sessionId");
const request = index.getAll(sessionId);
request.onsuccess = () => {
const data = request.result.map((item) => {
const { sessionId: _, ...gazePoint } = item;
return gazePoint;
});
resolveGaze(data);
};
request.onerror = () => rejectGaze(new Error("Failed to get gaze data"));
});
const videoChunks = await new Promise((resolveVideo, rejectVideo) => {
const currentDb2 = getDb();
if (!currentDb2) {
rejectVideo(new Error("Database not initialized"));
return;
}
const transaction = currentDb2.transaction(["videoChunks"], "readonly");
const store = transaction.objectStore("videoChunks");
const index = store.index("sessionId");
const request = index.getAll(sessionId);
request.onsuccess = () => {
const chunks = request.result.map((chunk) => ({
id: chunk.id,
sessionId: chunk.sessionId,
timestamp: chunk.timestamp,
chunkIndex: chunk.chunkIndex,
duration: chunk.duration,
size: chunk.data?.size || 0
}));
resolveVideo(chunks);
};
request.onerror = () => rejectVideo(new Error("Failed to get video chunks"));
});
let startBrowserTime = options?.startBrowserTime ?? session.metadata?.startBrowserTime;
let endBrowserTime = options?.endBrowserTime ?? session.metadata?.endBrowserTime;
if (!startBrowserTime || !endBrowserTime) {
const recordingStartEvent = events.find((e) => e.type === "recording_start");
const recordingStopEvent = events.find((e) => e.type === "recording_stop");
if (recordingStartEvent) {
startBrowserTime = startBrowserTime ?? recordingStartEvent.browserTimestamp;
}
if (recordingStopEvent) {
endBrowserTime = endBrowserTime ?? recordingStopEvent.browserTimestamp;
}
}
let filteredGazeData = gazeData;
let filteredEvents = events;
if (startBrowserTime !== undefined || endBrowserTime !== undefined) {
filteredGazeData = gazeData.filter((gaze) => {
if (startBrowserTime !== undefined && gaze.browserTimestamp < startBrowserTime) {
return false;
}
if (endBrowserTime !== undefined && gaze.browserTimestamp > endBrowserTime) {
return false;
}
return true;
});
filteredEvents = events.filter((event) => {
if ([
"session_start",
"session_stop",
"recording_start",
"recording_stop"
].includes(event.type)) {
return true;
}
if (startBrowserTime !== undefined && event.browserTimestamp < startBrowserTime) {
return false;
}
if (endBrowserTime !== undefined && event.browserTimestamp > endBrowserTime) {
return false;
}
return true;
});
}
const sessionData = {
session,
events: filteredEvents,
gazeData: filteredGazeData,
videoChunks,
metadata: {
totalDuration: session.endTime ? session.endTime - session.startTime : 0,
gazeDataPoints: filteredGazeData.length,
eventsCount: filteredEvents.length,
chunksCount: videoChunks.length,
exportedAt: new Date().toISOString(),
startBrowserTime,
endBrowserTime
}
};
return sessionData;
}, getVideoChunkData = async (chunkId) => {
await ensureDatabaseReady();
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (!currentDb) {
reject(new Error("Database not initialized"));
return;
}
const transaction = currentDb.transaction(["videoChunks"], "readonly");
const store = transaction.objectStore("videoChunks");
const request = store.get(chunkId);
request.onsuccess = () => {
const chunk = request.result;
resolve(chunk?.data || null);
};
request.onerror = () => reject(new Error("Failed to get video chunk data"));
});
}, getStorageUsage = async () => {
if ("storage" in navigator && "estimate" in navigator.storage) {
const estimate = await navigator.storage.estimate();
const used = estimate.usage || 0;
const available = estimate.quota || 0;
const percentage = available > 0 ? used / available * 100 : 0;
return { used, available, percentage };
}
return { used: 0, available: 0, percentage: 0 };
}, cleanupOldVideoChunks = async (keepRecentHours = 24) => {
const currentDb = getDb();
if (!currentDb) {
throw new Error("Database not initialized");
}
const cutoffTime = Date.now() - keepRecentHours * 60 * 60 * 1000;
let deletedCount = 0;
return new Promise((resolve, reject) => {
const currentDb2 = getDb();
if (!currentDb2) {
reject(new Error("Database not available"));
return;
}
const transaction = currentDb2.transaction(["videoChunks"], "readwrite");
const store = transaction.objectStore("videoChunks");
const index = store.index("timestamp");
const request = index.openCursor(IDBKeyRange.upperBound(cutoffTime));
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
cursor.delete();
deletedCount++;
cursor.continue();
}
};
transaction.oncomplete = () => resolve(deletedCount);
transaction.onerror = () => reject(new Error("Failed to cleanup video chunks"));
});
}, autoCleanupStorage = async (triggerPercentage = 80) => {
const usage = await getStorageUsage();
if (usage.percentage >= triggerPercentage) {
console.warn(`Storage usage at ${usage.percentage.toFixed(1)}%, starting cleanup...`);
await cleanupOldVideoChunks(12);
const newUsage = await getStorageUsage();
if (newUsage.percentage >= triggerPercentage) {
await cleanupOldVideoChunks(6);
}
}
}, getAllSessions = () => {
return new Promise((resolve, reject) => {
const currentDb = getDb();
if (!currentDb) {
reject(new Error("Database not initialized"));
return;
}
const transaction = currentDb.transaction(["sessions"], "readonly");
const store = transaction.objectStore("sessions");
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(new Error("Failed to get sessions"));
});
}, deleteSession = async (sessionId) => {
const currentDb = getDb();
if (!currentDb) {
throw new Error("Database not initialized");
}
const transaction = currentDb.transaction(["sessions", "events", "gazeData", "videoChunks"], "readwrite");
const sessionStore = transaction.objectStore("sessions");
sessionStore.delete(sessionId);
const eventStore = transaction.objectStore("events");
const eventIndex = eventStore.index("sessionId");
const eventRequest = eventIndex.openCursor(IDBKeyRange.only(sessionId));
eventRequest.onsuccess = () => {
const cursor = eventRequest.result;
if (cursor) {
cursor.delete();
cursor.continue();
}
};
const gazeStore = transaction.objectStore("gazeData");
const gazeIndex = gazeStore.index("sessionId");
const gazeRequest = gazeIndex.openCursor(IDBKeyRange.only(sessionId));
gazeRequest.onsuccess = () => {
const cursor = gazeRequest.result;
if (cursor) {
cursor.delete();
cursor.continue();
}
};
const videoStore = transaction.objectStore("videoChunks");
const videoIndex = videoStore.index("sessionId");
const videoRequest = videoIndex.openCursor(IDBKeyRange.only(sessionId));
videoRequest.onsuccess = () => {
const cursor = videoRequest.result;
if (cursor) {
cursor.delete();
cursor.continue();
}
};
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(new Error("Failed to delete session"));
});
};
export {
saveVideoChunk,
saveSession,
saveGazeData,
saveEvent,
resetDatabase,
initializeStorage,
getVideoChunkData,
getStorageUsage,
getSessionData,
getSession,
getAllSessions,
deleteSession,
cleanupOldVideoChunks,
autoCleanupStorage
};