UNPKG

eye-analysis

Version:

Eye Analysis - Browser-based eye tracking and screen recording library for research and experiments

432 lines (430 loc) 15.3 kB
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 };