UNPKG

eye-analysis

Version:

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

1,533 lines (1,524 loc) 102 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); // interaction.ts var _interactionState, interactionState; var init_interaction = __esm(() => { _interactionState = {}; if (typeof globalThis !== "undefined") { if (!globalThis.__eyeAnalysisInteractionState) { globalThis.__eyeAnalysisInteractionState = _interactionState; } } interactionState = new Proxy(_interactionState, { get(target, prop) { const globalState = globalThis.__eyeAnalysisInteractionState || target; return globalState[prop]; }, set(target, prop, value) { const globalState = globalThis.__eyeAnalysisInteractionState || target; globalState[prop] = value; return true; } }); }); // recorder/state.ts var exports_state = {}; __export(exports_state, { subscribe: () => subscribe, resetState: () => resetState, getSubscriberCount: () => getSubscriberCount, getState: () => getState, dispatch: () => dispatch }); var getInitialState = () => ({ status: "idle", currentSession: null, isRecording: false, recordingDuration: 0, gazeDataCount: 0, eventsCount: 0, videoChunksCount: 0, error: null, lastUpdate: Date.now(), recordingConfig: undefined, startBrowserTime: undefined, recordingStream: null }), ensureGlobalState = () => { if (!globalThis.__eyeAnalysisRecorderState) { globalThis.__eyeAnalysisRecorderState = getInitialState(); } if (!globalThis.__eyeAnalysisStateSubscribers) { globalThis.__eyeAnalysisStateSubscribers = new Set; } }, getCurrentState = () => { ensureGlobalState(); return globalThis.__eyeAnalysisRecorderState; }, getSubscribers = () => { ensureGlobalState(); return globalThis.__eyeAnalysisStateSubscribers; }, stateReducer = (state, action) => { switch (action.type) { case "INITIALIZE": return { ...state, status: "initialized", error: null, lastUpdate: Date.now() }; case "CREATE_SESSION": return { ...state, currentSession: action.payload, error: null, lastUpdate: Date.now() }; case "UPDATE_SESSION": return { ...state, currentSession: action.payload, error: null, lastUpdate: Date.now() }; case "START_RECORDING": return { ...state, status: "recording", isRecording: true, recordingDuration: 0, error: null, lastUpdate: Date.now(), startBrowserTime: performance.now() }; case "STOP_RECORDING": return { ...state, status: "stopped", isRecording: false, error: null, lastUpdate: Date.now(), startBrowserTime: undefined, recordingStream: null }; case "ADD_GAZE_DATA": return { ...state, gazeDataCount: state.gazeDataCount + 1, lastUpdate: Date.now() }; case "ADD_EVENT": return { ...state, eventsCount: state.eventsCount + 1, lastUpdate: Date.now() }; case "UPDATE_DURATION": return { ...state, recordingDuration: action.payload, lastUpdate: Date.now() }; case "SET_ERROR": return { ...state, status: "error", error: action.payload, lastUpdate: Date.now() }; case "CLEAR_ERROR": return { ...state, error: null, lastUpdate: Date.now() }; case "CLEAR_SESSION": return { ...state, status: "initialized", currentSession: null, gazeDataCount: 0, eventsCount: 0, videoChunksCount: 0, recordingDuration: 0, startBrowserTime: undefined, recordingStream: null, lastUpdate: Date.now() }; case "SET_RECORDING_CONFIG": return { ...state, recordingConfig: action.payload, lastUpdate: Date.now() }; case "SET_RECORDING_STREAM": return { ...state, recordingStream: action.payload, lastUpdate: Date.now() }; case "RESET": return { ...getInitialState(), lastUpdate: Date.now() }; default: return state; } }, getState = () => getCurrentState(), dispatch = (action) => { const currentState = getCurrentState(); const newState = stateReducer(currentState, action); if (newState !== currentState) { globalThis.__eyeAnalysisRecorderState = newState; const subscribers = getSubscribers(); subscribers.forEach((subscriber) => { try { subscriber(newState); } catch (error) { console.error("State subscriber error:", error); } }); } }, subscribe = (subscriber) => { const subscribers = getSubscribers(); subscribers.add(subscriber); return () => { subscribers.delete(subscriber); }; }, getSubscriberCount = () => getSubscribers().size, resetState = () => { dispatch({ type: "RESET" }); }; var init_state = __esm(() => { ensureGlobalState(); }); // 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")); }); }; // ../../node_modules/fflate/esm/browser.js function deflateSync(data, opts) { return dopt(data, opts || {}, 0, 0); } function strToU8(str, latin1) { if (latin1) { var ar_1 = new u8(str.length); for (var i2 = 0;i2 < str.length; ++i2) ar_1[i2] = str.charCodeAt(i2); return ar_1; } if (te) return te.encode(str); var l = str.length; var ar = new u8(str.length + (str.length >> 1)); var ai = 0; var w = function(v) { ar[ai++] = v; }; for (var i2 = 0;i2 < l; ++i2) { if (ai + 5 > ar.length) { var n = new u8(ai + 8 + (l - i2 << 1)); n.set(ar); ar = n; } var c = str.charCodeAt(i2); if (c < 128 || latin1) w(c); else if (c < 2048) w(192 | c >> 6), w(128 | c & 63); else if (c > 55295 && c < 57344) c = 65536 + (c & 1023 << 10) | str.charCodeAt(++i2) & 1023, w(240 | c >> 18), w(128 | c >> 12 & 63), w(128 | c >> 6 & 63), w(128 | c & 63); else w(224 | c >> 12), w(128 | c >> 6 & 63), w(128 | c & 63); } return slc(ar, 0, ai); } function zipSync(data, opts) { if (!opts) opts = {}; var r = {}; var files = []; fltn(data, "", r, opts); var o = 0; var tot = 0; for (var fn in r) { var _a2 = r[fn], file = _a2[0], p = _a2[1]; var compression = p.level == 0 ? 0 : 8; var f = strToU8(fn), s = f.length; var com = p.comment, m = com && strToU8(com), ms = m && m.length; var exl = exfl(p.extra); if (s > 65535) err(11); var d = compression ? deflateSync(file, p) : file, l = d.length; var c = crc(); c.p(file); files.push(mrg(p, { size: file.length, crc: c.d(), c: d, f, m, u: s != fn.length || m && com.length != ms, o, compression })); o += 30 + s + exl + l; tot += 76 + 2 * (s + exl) + (ms || 0) + l; } var out = new u8(tot + 22), oe = o, cdl = tot - o; for (var i2 = 0;i2 < files.length; ++i2) { var f = files[i2]; wzh(out, f.o, f, f.f, f.u, f.c.length); var badd = 30 + f.f.length + exfl(f.extra); out.set(f.c, f.o + badd); wzh(out, o, f, f.f, f.u, f.c.length, f.o, f.m), o += 16 + badd + (f.m ? f.m.length : 0); } wzf(out, o, files.length, cdl, oe); return out; } var u8, u16, i32, fleb, fdeb, clim, freb = function(eb, start) { var b = new u16(31); for (var i = 0;i < 31; ++i) { b[i] = start += 1 << eb[i - 1]; } var r = new i32(b[30]); for (var i = 1;i < 30; ++i) { for (var j = b[i];j < b[i + 1]; ++j) { r[j] = j - b[i] << 5 | i; } } return { b, r }; }, _a, fl, revfl, _b, fd, revfd, rev, x, i, hMap = function(cd, mb, r) { var s = cd.length; var i2 = 0; var l = new u16(mb); for (;i2 < s; ++i2) { if (cd[i2]) ++l[cd[i2] - 1]; } var le = new u16(mb); for (i2 = 1;i2 < mb; ++i2) { le[i2] = le[i2 - 1] + l[i2 - 1] << 1; } var co; if (r) { co = new u16(1 << mb); var rvb = 15 - mb; for (i2 = 0;i2 < s; ++i2) { if (cd[i2]) { var sv = i2 << 4 | cd[i2]; var r_1 = mb - cd[i2]; var v = le[cd[i2] - 1]++ << r_1; for (var m = v | (1 << r_1) - 1;v <= m; ++v) { co[rev[v] >> rvb] = sv; } } } } else { co = new u16(s); for (i2 = 0;i2 < s; ++i2) { if (cd[i2]) { co[i2] = rev[le[cd[i2] - 1]++] >> 15 - cd[i2]; } } } return co; }, flt, i, i, i, i, fdt, i, flm, fdm, shft = function(p) { return (p + 7) / 8 | 0; }, slc = function(v, s, e) { if (s == null || s < 0) s = 0; if (e == null || e > v.length) e = v.length; return new u8(v.subarray(s, e)); }, ec, err = function(ind, msg, nt) { var e = new Error(msg || ec[ind]); e.code = ind; if (Error.captureStackTrace) Error.captureStackTrace(e, err); if (!nt) throw e; return e; }, wbits = function(d, p, v) { v <<= p & 7; var o = p / 8 | 0; d[o] |= v; d[o + 1] |= v >> 8; }, wbits16 = function(d, p, v) { v <<= p & 7; var o = p / 8 | 0; d[o] |= v; d[o + 1] |= v >> 8; d[o + 2] |= v >> 16; }, hTree = function(d, mb) { var t = []; for (var i2 = 0;i2 < d.length; ++i2) { if (d[i2]) t.push({ s: i2, f: d[i2] }); } var s = t.length; var t2 = t.slice(); if (!s) return { t: et, l: 0 }; if (s == 1) { var v = new u8(t[0].s + 1); v[t[0].s] = 1; return { t: v, l: 1 }; } t.sort(function(a, b) { return a.f - b.f; }); t.push({ s: -1, f: 25001 }); var l = t[0], r = t[1], i0 = 0, i1 = 1, i22 = 2; t[0] = { s: -1, f: l.f + r.f, l, r }; while (i1 != s - 1) { l = t[t[i0].f < t[i22].f ? i0++ : i22++]; r = t[i0 != i1 && t[i0].f < t[i22].f ? i0++ : i22++]; t[i1++] = { s: -1, f: l.f + r.f, l, r }; } var maxSym = t2[0].s; for (var i2 = 1;i2 < s; ++i2) { if (t2[i2].s > maxSym) maxSym = t2[i2].s; } var tr = new u16(maxSym + 1); var mbt = ln(t[i1 - 1], tr, 0); if (mbt > mb) { var i2 = 0, dt = 0; var lft = mbt - mb, cst = 1 << lft; t2.sort(function(a, b) { return tr[b.s] - tr[a.s] || a.f - b.f; }); for (;i2 < s; ++i2) { var i2_1 = t2[i2].s; if (tr[i2_1] > mb) { dt += cst - (1 << mbt - tr[i2_1]); tr[i2_1] = mb; } else break; } dt >>= lft; while (dt > 0) { var i2_2 = t2[i2].s; if (tr[i2_2] < mb) dt -= 1 << mb - tr[i2_2]++ - 1; else ++i2; } for (;i2 >= 0 && dt; --i2) { var i2_3 = t2[i2].s; if (tr[i2_3] == mb) { --tr[i2_3]; ++dt; } } mbt = mb; } return { t: new u8(tr), l: mbt }; }, ln = function(n, l, d) { return n.s == -1 ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) : l[n.s] = d; }, lc = function(c) { var s = c.length; while (s && !c[--s]) ; var cl = new u16(++s); var cli = 0, cln = c[0], cls = 1; var w = function(v) { cl[cli++] = v; }; for (var i2 = 1;i2 <= s; ++i2) { if (c[i2] == cln && i2 != s) ++cls; else { if (!cln && cls > 2) { for (;cls > 138; cls -= 138) w(32754); if (cls > 2) { w(cls > 10 ? cls - 11 << 5 | 28690 : cls - 3 << 5 | 12305); cls = 0; } } else if (cls > 3) { w(cln), --cls; for (;cls > 6; cls -= 6) w(8304); if (cls > 2) w(cls - 3 << 5 | 8208), cls = 0; } while (cls--) w(cln); cls = 1; cln = c[i2]; } } return { c: cl.subarray(0, cli), n: s }; }, clen = function(cf, cl) { var l = 0; for (var i2 = 0;i2 < cl.length; ++i2) l += cf[i2] * cl[i2]; return l; }, wfblk = function(out, pos, dat) { var s = dat.length; var o = shft(pos + 2); out[o] = s & 255; out[o + 1] = s >> 8; out[o + 2] = out[o] ^ 255; out[o + 3] = out[o + 1] ^ 255; for (var i2 = 0;i2 < s; ++i2) out[o + i2 + 4] = dat[i2]; return (o + 4 + s) * 8; }, wblk = function(dat, out, final, syms, lf, df, eb, li, bs, bl, p) { wbits(out, p++, final); ++lf[256]; var _a2 = hTree(lf, 15), dlt = _a2.t, mlb = _a2.l; var _b2 = hTree(df, 15), ddt = _b2.t, mdb = _b2.l; var _c = lc(dlt), lclt = _c.c, nlc = _c.n; var _d = lc(ddt), lcdt = _d.c, ndc = _d.n; var lcfreq = new u16(19); for (var i2 = 0;i2 < lclt.length; ++i2) ++lcfreq[lclt[i2] & 31]; for (var i2 = 0;i2 < lcdt.length; ++i2) ++lcfreq[lcdt[i2] & 31]; var _e = hTree(lcfreq, 7), lct = _e.t, mlcb = _e.l; var nlcc = 19; for (;nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc) ; var flen = bl + 5 << 3; var ftlen = clen(lf, flt) + clen(df, fdt) + eb; var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + 2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]; if (bs >= 0 && flen <= ftlen && flen <= dtlen) return wfblk(out, p, dat.subarray(bs, bs + bl)); var lm, ll, dm, dl; wbits(out, p, 1 + (dtlen < ftlen)), p += 2; if (dtlen < ftlen) { lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt; var llm = hMap(lct, mlcb, 0); wbits(out, p, nlc - 257); wbits(out, p + 5, ndc - 1); wbits(out, p + 10, nlcc - 4); p += 14; for (var i2 = 0;i2 < nlcc; ++i2) wbits(out, p + 3 * i2, lct[clim[i2]]); p += 3 * nlcc; var lcts = [lclt, lcdt]; for (var it = 0;it < 2; ++it) { var clct = lcts[it]; for (var i2 = 0;i2 < clct.length; ++i2) { var len = clct[i2] & 31; wbits(out, p, llm[len]), p += lct[len]; if (len > 15) wbits(out, p, clct[i2] >> 5 & 127), p += clct[i2] >> 12; } } } else { lm = flm, ll = flt, dm = fdm, dl = fdt; } for (var i2 = 0;i2 < li; ++i2) { var sym = syms[i2]; if (sym > 255) { var len = sym >> 18 & 31; wbits16(out, p, lm[len + 257]), p += ll[len + 257]; if (len > 7) wbits(out, p, sym >> 23 & 31), p += fleb[len]; var dst = sym & 31; wbits16(out, p, dm[dst]), p += dl[dst]; if (dst > 3) wbits16(out, p, sym >> 5 & 8191), p += fdeb[dst]; } else { wbits16(out, p, lm[sym]), p += ll[sym]; } } wbits16(out, p, lm[256]); return p + ll[256]; }, deo, et, dflt = function(dat, lvl, plvl, pre, post, st) { var s = st.z || dat.length; var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7000)) + post); var w = o.subarray(pre, o.length - post); var lst = st.l; var pos = (st.r || 0) & 7; if (lvl) { if (pos) w[0] = st.r >> 3; var opt = deo[lvl - 1]; var n = opt >> 13, c = opt & 8191; var msk_1 = (1 << plvl) - 1; var prev = st.p || new u16(32768), head = st.h || new u16(msk_1 + 1); var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1; var hsh = function(i3) { return (dat[i3] ^ dat[i3 + 1] << bs1_1 ^ dat[i3 + 2] << bs2_1) & msk_1; }; var syms = new i32(25000); var lf = new u16(288), df = new u16(32); var lc_1 = 0, eb = 0, i2 = st.i || 0, li = 0, wi = st.w || 0, bs = 0; for (;i2 + 2 < s; ++i2) { var hv = hsh(i2); var imod = i2 & 32767, pimod = head[hv]; prev[imod] = pimod; head[hv] = imod; if (wi <= i2) { var rem = s - i2; if ((lc_1 > 7000 || li > 24576) && (rem > 423 || !lst)) { pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i2 - bs, pos); li = lc_1 = eb = 0, bs = i2; for (var j = 0;j < 286; ++j) lf[j] = 0; for (var j = 0;j < 30; ++j) df[j] = 0; } var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767; if (rem > 2 && hv == hsh(i2 - dif)) { var maxn = Math.min(n, rem) - 1; var maxd = Math.min(32767, i2); var ml = Math.min(258, rem); while (dif <= maxd && --ch_1 && imod != pimod) { if (dat[i2 + l] == dat[i2 + l - dif]) { var nl = 0; for (;nl < ml && dat[i2 + nl] == dat[i2 + nl - dif]; ++nl) ; if (nl > l) { l = nl, d = dif; if (nl > maxn) break; var mmd = Math.min(dif, nl - 2); var md = 0; for (var j = 0;j < mmd; ++j) { var ti = i2 - dif + j & 32767; var pti = prev[ti]; var cd = ti - pti & 32767; if (cd > md) md = cd, pimod = ti; } } } imod = pimod, pimod = prev[imod]; dif += imod - pimod & 32767; } } if (d) { syms[li++] = 268435456 | revfl[l] << 18 | revfd[d]; var lin = revfl[l] & 31, din = revfd[d] & 31; eb += fleb[lin] + fdeb[din]; ++lf[257 + lin]; ++df[din]; wi = i2 + l; ++lc_1; } else { syms[li++] = dat[i2]; ++lf[dat[i2]]; } } } for (i2 = Math.max(i2, wi);i2 < s; ++i2) { syms[li++] = dat[i2]; ++lf[dat[i2]]; } pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i2 - bs, pos); if (!lst) { st.r = pos & 7 | w[pos / 8 | 0] << 3; pos -= 7; st.h = head, st.p = prev, st.i = i2, st.w = wi; } } else { for (var i2 = st.w || 0;i2 < s + lst; i2 += 65535) { var e = i2 + 65535; if (e >= s) { w[pos / 8 | 0] = lst; e = s; } pos = wfblk(w, pos + 1, dat.subarray(i2, e)); } st.i = s; } return slc(o, 0, pre + shft(pos) + post); }, crct, crc = function() { var c = -1; return { p: function(d) { var cr = c; for (var i2 = 0;i2 < d.length; ++i2) cr = crct[cr & 255 ^ d[i2]] ^ cr >>> 8; c = cr; }, d: function() { return ~c; } }; }, dopt = function(dat, opt, pre, post, st) { if (!st) { st = { l: 1 }; if (opt.dictionary) { var dict = opt.dictionary.subarray(-32768); var newDat = new u8(dict.length + dat.length); newDat.set(dict); newDat.set(dat, dict.length); dat = newDat; st.w = dict.length; } } return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? st.l ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 20 : 12 + opt.mem, pre, post, st); }, mrg = function(a, b) { var o = {}; for (var k in a) o[k] = a[k]; for (var k in b) o[k] = b[k]; return o; }, wbytes = function(d, b, v) { for (;v; ++b) d[b] = v, v >>>= 8; }, fltn = function(d, p, t, o) { for (var k in d) { var val = d[k], n = p + k, op = o; if (Array.isArray(val)) op = mrg(o, val[1]), val = val[0]; if (val instanceof u8) t[n] = [val, op]; else { t[n += "/"] = [new u8(0), op]; fltn(val, n, t, o); } } }, te, td, tds = 0, exfl = function(ex) { var le = 0; if (ex) { for (var k in ex) { var l = ex[k].length; if (l > 65535) err(9); le += l + 4; } } return le; }, wzh = function(d, b, f, fn, u, c, ce, co) { var fl2 = fn.length, ex = f.extra, col = co && co.length; var exl = exfl(ex); wbytes(d, b, ce != null ? 33639248 : 67324752), b += 4; if (ce != null) d[b++] = 20, d[b++] = f.os; d[b] = 20, b += 2; d[b++] = f.flag << 1 | (c < 0 && 8), d[b++] = u && 8; d[b++] = f.compression & 255, d[b++] = f.compression >> 8; var dt = new Date(f.mtime == null ? Date.now() : f.mtime), y = dt.getFullYear() - 1980; if (y < 0 || y > 119) err(10); wbytes(d, b, y << 25 | dt.getMonth() + 1 << 21 | dt.getDate() << 16 | dt.getHours() << 11 | dt.getMinutes() << 5 | dt.getSeconds() >> 1), b += 4; if (c != -1) { wbytes(d, b, f.crc); wbytes(d, b + 4, c < 0 ? -c - 2 : c); wbytes(d, b + 8, f.size); } wbytes(d, b + 12, fl2); wbytes(d, b + 14, exl), b += 16; if (ce != null) { wbytes(d, b, col); wbytes(d, b + 6, f.attrs); wbytes(d, b + 10, ce), b += 14; } d.set(fn, b); b += fl2; if (exl) { for (var k in ex) { var exf = ex[k], l = exf.length; wbytes(d, b, +k); wbytes(d, b + 2, l); d.set(exf, b + 4), b += 4 + l; } } if (col) d.set(co, b), b += col; return b; }, wzf = function(o, b, c, d, e) { wbytes(o, b, 101010256); wbytes(o, b + 8, c); wbytes(o, b + 10, c); wbytes(o, b + 12, d); wbytes(o, b + 16, e); }; var init_browser = __esm(() => { u8 = Uint8Array; u16 = Uint16Array; i32 = Int32Array; fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0]); fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0]); clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); _a = freb(fleb, 2); fl = _a.b; revfl = _a.r; fl[28] = 258, revfl[258] = 28; _b = freb(fdeb, 0); fd = _b.b; revfd = _b.r; rev = new u16(32768); for (i = 0;i < 32768; ++i) { x = (i & 43690) >> 1 | (i & 21845) << 1; x = (x & 52428) >> 2 | (x & 13107) << 2; x = (x & 61680) >> 4 | (x & 3855) << 4; rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1; } flt = new u8(288); for (i = 0;i < 144; ++i) flt[i] = 8; for (i = 144;i < 256; ++i) flt[i] = 9; for (i = 256;i < 280; ++i) flt[i] = 7; for (i = 280;i < 288; ++i) flt[i] = 8; fdt = new u8(32); for (i = 0;i < 32; ++i) fdt[i] = 5; flm = /* @__PURE__ */ hMap(flt, 9, 0); fdm = /* @__PURE__ */ hMap(fdt, 5, 0); ec = [ "unexpected EOF", "invalid block type", "invalid length/literal", "invalid distance", "stream finished", "no stream handler", , "no callback", "invalid UTF-8 data", "extra field too long", "date not in range 1980-2099", "filename too long", "stream finishing", "invalid zip data" ]; deo = /* @__PURE__ */ new i32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]); et = /* @__PURE__ */ new u8(0); crct = /* @__PURE__ */ function() { var t = new Int32Array(256); for (var i2 = 0;i2 < 256; ++i2) { var c = i2, k = 9; while (--k) c = (c & 1 && -306674912) ^ c >>> 1; t[i2] = c; } return t; }(); te = typeof TextEncoder != "undefined" && /* @__PURE__ */ new TextEncoder; td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder; try { td.decode(et, { stream: true }); tds = 1; } catch (e) {} }); // recorder/export.ts var exports_export = {}; __export(exports_export, { gazeDataToCSV: () => gazeDataToCSV, exportExperimentDataset: () => exportExperimentDataset, eventsToCSV: () => eventsToCSV, downloadSession: () => downloadSession, downloadFile: () => downloadFile, downloadCompleteSessionData: () => downloadCompleteSessionData, createSessionSummaryText: () => createSessionSummaryText, createMetadataJSON: () => createMetadataJSON }); var fieldExtractors, createFieldExtractors = (startBrowserTime) => [ { header: "sessionId", getValue: (p) => p.sessionId }, { header: "elapsedTime", getValue: (p) => p.browserTimestamp !== undefined ? p.browserTimestamp - startBrowserTime : null }, ...fieldExtractors.slice(1) ], gazeDataToCSV = (gazeData, startBrowserTime) => { if (startBrowserTime === undefined) { throw new Error("startBrowserTime is required for elapsedTime calculation"); } if (gazeData.length === 0) { return ""; } const extractors = createFieldExtractors(startBrowserTime); const activeExtractors = extractors.filter((extractor) => gazeData.some((point) => { const value = extractor.getValue(point); return value !== null && value !== undefined; })); const headers = activeExtractors.map((extractor) => extractor.header); const csvRows = [headers.join(",")]; for (const point of gazeData) { const row = activeExtractors.map((extractor) => { const value = extractor.getValue(point); return value ?? ""; }); csvRows.push(row.join(",")); } return csvRows.join(` `); }, eventsToCSV = (events, startBrowserTime) => { if (startBrowserTime === undefined) { throw new Error("startBrowserTime is required for elapsedTime calculation"); } const headers = [ "id", "sessionId", "type", "timestamp", "elapsedTime", "data" ]; const csvRows = [headers.join(",")]; for (const event of events) { const elapsedTime = event.browserTimestamp !== undefined ? event.browserTimestamp - startBrowserTime : null; const row = [ event.id, event.sessionId, event.type, event.timestamp, elapsedTime !== null ? elapsedTime : "", event.data ? JSON.stringify(event.data).replace(/"/g, '""') : "" ]; csvRows.push(row.map((field) => `"${field}"`).join(",")); } return csvRows.join(` `); }, createMetadataJSON = (sessionData) => { return { sessionInfo: sessionData.session, metadata: sessionData.metadata, videoChunks: sessionData.videoChunks.map((chunk) => ({ ...chunk, note: `Video chunk ${chunk.chunkIndex} - ${chunk.size} bytes` })), summary: { totalGazePoints: sessionData.gazeData.length, totalEvents: sessionData.events.length, totalVideoChunks: sessionData.videoChunks.length, sessionDuration: sessionData.metadata.totalDuration, recordingStartTime: new Date(sessionData.session.startTime).toISOString(), recordingEndTime: sessionData.session.endTime ? new Date(sessionData.session.endTime).toISOString() : null, startBrowserTime: sessionData.metadata.startBrowserTime, endBrowserTime: sessionData.metadata.endBrowserTime, elapsedTimeNote: sessionData.metadata.startBrowserTime !== undefined ? "elapsedTime = browserTimestamp - startBrowserTime (in milliseconds)" : "elapsedTime not available - recording was never started" } }; }, downloadFile = (content, filename, mimeType = "text/plain") => { const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, exportExperimentDataset = async (sessionIds, options = {}) => { const allFiles = {}; const includeOptions = { metadata: true, gaze: true, events: true, video: true, ...options.include }; for (const sessionId of sessionIds) { try { const sessionFolder = `session_${sessionId}`; const filesToDownload = await collectSessionFiles(sessionId, { includeOptions, prefix: sessionFolder }); for (const file of filesToDownload) { if (file.content instanceof Blob) { allFiles[file.filename] = new Uint8Array(await file.content.arrayBuffer()); } else { allFiles[file.filename] = strToU8(file.content); } } } catch (error) { console.error(`Failed to process session ${sessionId}:`, error); } } const datasetSummary = { exportedAt: new Date().toISOString(), totalSessions: sessionIds.length, sessionIds, description: "Combined dataset from Eye Analysis", includeOptions }; allFiles["dataset-summary.json"] = strToU8(JSON.stringify(datasetSummary, null, 2)); const zipped = zipSync(allFiles); const zipBlob = new Blob([zipped], { type: "application/zip" }); const filename = `experiment-dataset-${new Date().toISOString().split("T")[0]}.zip`; downloadFile(zipBlob, filename, "application/zip"); }, getSessionName = (sessionId) => { return `session-${sessionId}-${new Date().toISOString().split("T")[0]}`; }, collectSessionFiles = async (sessionId, options) => { await initializeStorage(); const sessionData = await getSessionData(sessionId); if ((options.includeOptions.gaze || options.includeOptions.events) && sessionData.metadata.startBrowserTime === undefined) { throw new Error("Cannot export CSV data: startBrowserTime is required for elapsedTime calculation but is not available. This usually means recording was never started."); } const sessionName = getSessionName(sessionId); const filesToDownload = []; const getFilename = (baseName) => { if (options.prefix) { return `${options.prefix}/${baseName}`; } return `${sessionName}-${baseName}`; }; if (options.includeOptions.metadata) { const metadata = createMetadataJSON(sessionData); filesToDownload.push({ content: JSON.stringify(metadata, null, 2), filename: getFilename("metadata.json"), mimeType: "application/json" }); } if (options.includeOptions.gaze && sessionData.gazeData.length > 0) { const gazeDataCSV = gazeDataToCSV(sessionData.gazeData, sessionData.metadata.startBrowserTime); filesToDownload.push({ content: gazeDataCSV, filename: getFilename("gaze.csv"), mimeType: "text/csv" }); } if (options.includeOptions.events && sessionData.events.length > 0) { const eventsCSV = eventsToCSV(sessionData.events, sessionData.metadata.startBrowserTime); filesToDownload.push({ content: eventsCSV, filename: getFilename("events.csv"), mimeType: "text/csv" }); } if (options.includeOptions.video && sessionData.videoChunks.length > 0) { try { const videoBlobs = []; let videoFormat = "webm"; for (const chunk of sessionData.videoChunks) { const chunkData = await getVideoChunkData(chunk.id); if (chunkData) { videoBlobs.push(chunkData); if (chunkData.type.includes("mp4")) { videoFormat = "mp4"; } else if (chunkData.type.includes("webm")) { videoFormat = "webm"; } } } if (videoBlobs.length > 0) { const sessionVideoFormat = sessionData.session.config.videoFormat || videoFormat; const mimeType = sessionVideoFormat === "mp4" ? "video/mp4" : "video/webm"; const videoBlob = new Blob(videoBlobs, { type: mimeType }); filesToDownload.push({ content: videoBlob, filename: getFilename(`recording.${sessionVideoFormat}`), mimeType }); } } catch (error) { console.error("Failed to prepare video data:", error); } } return filesToDownload; }, downloadSession = async (sessionId, options = {}) => { const includeOptions = { metadata: true, gaze: true, events: true, video: true, ...options.include }; const asZip = options.asZip ?? false; const filesToDownload = await collectSessionFiles(sessionId, { includeOptions, prefix: options.prefix }); if (filesToDownload.length === 0) { console.warn("No files to download"); return; } if (asZip) { const files = {}; for (const file of filesToDownload) { const filename = options.prefix ? file.filename.split("/").pop() || file.filename : file.filename.split("-").pop() || file.filename; const content = file.content instanceof Blob ? new Uint8Array(await file.content.arrayBuffer()) : strToU8(file.content); files[filename] = content; } const sessionName = getSessionName(sessionId); const zipped = zipSync(files); const zipBlob = new Blob([zipped], { type: "application/zip" }); downloadFile(zipBlob, `${sessionName}.zip`, "application/zip"); } else { for (const file of filesToDownload) { downloadFile(file.content, file.filename, file.mimeType); } } }, downloadCompleteSessionData = async (sessionId) => { const sessionData = await getSessionData(sessionId); if ((sessionData.gazeData.length > 0 || sessionData.events.length > 0) && sessionData.metadata.startBrowserTime === undefined) { throw new Error("Cannot export CSV data: startBrowserTime is required for elapsedTime calculation but is not available. This usually means recording was never started."); } const sessionName = `session-${sessionId}-${new Date().toISOString().split("T")[0]}`; const metadata = createMetadataJSON(sessionData); downloadFile(JSON.stringify(metadata, null, 2), `${sessionName}-metadata.json`, "application/json"); if (sessionData.gazeData.length > 0) { const gazeCSV = gazeDataToCSV(sessionData.gazeData, sessionData.metadata.startBrowserTime); downloadFile(gazeCSV, `${sessionName}-gaze.csv`, "text/csv"); } if (sessionData.events.length > 0) { const eventsCSV = eventsToCSV(sessionData.events, sessionData.metadata.startBrowserTime); downloadFile(eventsCSV, `${sessionName}-events.csv`, "text/csv"); } if (sessionData.videoChunks.length > 0) { try { const videoBlobs = []; for (const chunk of sessionData.videoChunks) { const chunkData = await getVideoChunkData(chunk.id); if (chunkData) { videoBlobs.push(chunkData); } } if (videoBlobs.length > 0) { const combinedVideo = new Blob(videoBlobs, { type: "video/webm" }); downloadFile(combinedVideo, `${sessionName}-recording.webm`, "video/webm"); } } catch (error) { console.error("Failed to download video data:", error); } } }, createSessionSummaryText = (sessionData) => { const duration = Math.round(sessionData.metadata.totalDuration / 1000); const minutes = Math.floor(duration / 60); const seconds = duration % 60; return `Web Eye Tracking Recorder - Session Summary ========================================== Session ID: ${sessionData.session.sessionId} Participant: ${sessionData.session.participantId} Experiment: ${sessionData.session.experimentType} Start Time: ${new Date(sessionData.session.startTime).toLocaleString()} End Time: ${sessionData.session.endTime ? new Date(sessionData.session.endTime).toLocaleString() : "N/A"} Duration: ${minutes}m ${seconds}s Data Summary: - Gaze Data Points: ${sessionData.gazeData.length} - Events Recorded: ${sessionData.events.length} - Video Chunks: ${sessionData.videoChunks.length} Recording Settings: - Frame Rate: ${sessionData.session.config.frameRate} fps - Quality: ${sessionData.session.config.quality} - Chunk Duration: ${sessionData.session.config.chunkDuration}s Export Date: ${new Date().toLocaleString()} Generated by Web Eye Tracking Recorder `; }; var init_export = __esm(() => { init_browser(); fieldExtractors = [ { header: "sessionId", getValue: (p) => p.sessionId }, { header: "deviceTimeStamp", getValue: (p) => p.deviceTimeStamp }, { header: "systemTimestamp", getValue: (p) => p.systemTimestamp }, { header: "screenX", getValue: (p) => p.screenX }, { header: "screenY", getValue: (p) => p.screenY }, { header: "screenWidth", getValue: (p) => p.screenWidth }, { header: "screenHeight", getValue: (p) => p.screenHeight }, { header: "normalized", getValue: (p) => p.normalized }, { header: "contentX", getValue: (p) => p.contentX }, { header: "contentY", getValue: (p) => p.contentY }, { header: "confidence", getValue: (p) => p.confidence }, { header: "leftEye - screenX", getValue: (p) => p.leftEye?.screenX }, { header: "leftEye - screenY", getValue: (p) =>