@akhilpulse/samadhaan-session-replay 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/SessionReplayProvider.tsx +54 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akhilpulse/samadhaan-session-replay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "React Native SDK for Samadhaan Guided Support — session recording, live remote screen-share, take-control, and guided-tour overlays.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -35,7 +35,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
|
35
35
|
import { captureRef } from "react-native-view-shot";
|
|
36
36
|
|
|
37
37
|
const STORAGE_KEY = "@akhilpulse/samadhaan-session-replay/sessionId";
|
|
38
|
-
const SDK_VERSION = "0.2.
|
|
38
|
+
const SDK_VERSION = "0.2.3";
|
|
39
39
|
|
|
40
40
|
/** Default Samadhaan production endpoint. Override via `config.serverUrl` for staging. */
|
|
41
41
|
export const DEFAULT_SERVER_URL = "https://samadhaan.pulseenergy.io";
|
|
@@ -211,49 +211,50 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
211
211
|
// ---- Screenshot capture ----
|
|
212
212
|
const liveMode = status === "live";
|
|
213
213
|
useEffect(() => {
|
|
214
|
-
|
|
214
|
+
// Only capture screenshots in live mode. Idle sessions emit interaction
|
|
215
|
+
// events (taps, navigation, etc.) but do not waste CPU/battery/network on
|
|
216
|
+
// periodic screenshots — frames stream only when an agent is actively viewing.
|
|
217
|
+
if (config.disableScreenshots || !sessionId || !liveMode) return;
|
|
215
218
|
let stopped = false;
|
|
216
|
-
|
|
219
|
+
// ~16 fps target. Capture takes time, so the loop self-paces — we schedule
|
|
220
|
+
// the next tick AFTER the previous capture completes rather than at a fixed cadence.
|
|
221
|
+
const liveTargetMs = 60;
|
|
222
|
+
let lastUploadFrameAt = 0;
|
|
217
223
|
const tick = async () => {
|
|
218
224
|
if (stopped) return;
|
|
219
|
-
|
|
225
|
+
const startedAt = Date.now();
|
|
226
|
+
if (paused || keyboardVisible) {
|
|
227
|
+
setTimeout(tick, liveTargetMs);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
220
230
|
try {
|
|
221
231
|
const ref = rootRef.current;
|
|
222
232
|
if (ref) {
|
|
223
|
-
//
|
|
224
|
-
// 540px wide JPEGs render fine and load 4-6× faster than full-resolution captures.
|
|
233
|
+
// Aggressively downscaled + low quality so capture+encode stays under ~50ms on mid devices.
|
|
225
234
|
const uri = await captureRef(ref as any, {
|
|
226
235
|
format: "jpg",
|
|
227
|
-
quality:
|
|
236
|
+
quality: 0.25,
|
|
228
237
|
result: "base64",
|
|
229
|
-
width:
|
|
238
|
+
width: 360,
|
|
230
239
|
});
|
|
231
|
-
// upload via request-url then PUT
|
|
232
240
|
const blob = base64ToUint8Array(uri);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const r = await fetch(`${serverUrl}/api/guided-support/storage/uploads/request-url`, {
|
|
236
|
-
method: "POST",
|
|
237
|
-
headers: { "Content-Type": "application/json", "x-ingest-token": ingestToken || "" },
|
|
238
|
-
body: JSON.stringify({ sessionId, name: `frame-${Date.now()}.jpg`, size: blob.length, contentType: "image/jpeg" }),
|
|
239
|
-
});
|
|
240
|
-
const meta = await r.json();
|
|
241
|
-
const fd = new FormData();
|
|
242
|
-
// @ts-ignore
|
|
243
|
-
fd.append("file", { uri: `data:image/jpeg;base64,${uri}`, name: "frame.jpg", type: "image/jpeg" });
|
|
244
|
-
const up = await fetch(`${serverUrl}${meta.uploadUrl}`, { method: "POST", body: fd as any });
|
|
245
|
-
if (up.ok) path = meta.path;
|
|
246
|
-
} catch {}
|
|
247
|
-
queueEvent("screenshot", { w: 0, h: 0 }, path || undefined);
|
|
248
|
-
if (liveMode && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
249
|
-
// also stream binary frame
|
|
241
|
+
// Stream binary over WS — no REST upload in the hot path.
|
|
242
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
250
243
|
sendBinaryFrame(wsRef.current, blob, 0, 0);
|
|
251
244
|
}
|
|
245
|
+
// Persist a sampled keyframe at most once every 10s for the replay timeline.
|
|
246
|
+
if (startedAt - lastUploadFrameAt > 10000) {
|
|
247
|
+
lastUploadFrameAt = startedAt;
|
|
248
|
+
uploadFrameAsync(serverUrl, sessionId, ingestToken, uri, blob).then((path) => {
|
|
249
|
+
if (path) queueEvent("screenshot", { w: 0, h: 0 }, path);
|
|
250
|
+
}).catch(() => {});
|
|
251
|
+
}
|
|
252
252
|
}
|
|
253
253
|
} catch {}
|
|
254
|
-
|
|
254
|
+
const elapsed = Date.now() - startedAt;
|
|
255
|
+
setTimeout(tick, Math.max(0, liveTargetMs - elapsed));
|
|
255
256
|
};
|
|
256
|
-
setTimeout(tick,
|
|
257
|
+
setTimeout(tick, liveTargetMs);
|
|
257
258
|
return () => { stopped = true; };
|
|
258
259
|
}, [sessionId, ingestToken, liveMode, paused, keyboardVisible, config.disableScreenshots, serverUrl, queueEvent]);
|
|
259
260
|
|
|
@@ -447,6 +448,31 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
447
448
|
}
|
|
448
449
|
|
|
449
450
|
// ---- helpers ----
|
|
451
|
+
async function uploadFrameAsync(
|
|
452
|
+
serverUrl: string,
|
|
453
|
+
sessionId: string | null,
|
|
454
|
+
ingestToken: string | null,
|
|
455
|
+
base64: string,
|
|
456
|
+
blob: Uint8Array,
|
|
457
|
+
): Promise<string | null> {
|
|
458
|
+
if (!sessionId || !ingestToken) return null;
|
|
459
|
+
try {
|
|
460
|
+
const r = await fetch(`${serverUrl}/api/guided-support/storage/uploads/request-url`, {
|
|
461
|
+
method: "POST",
|
|
462
|
+
headers: { "Content-Type": "application/json", "x-ingest-token": ingestToken },
|
|
463
|
+
body: JSON.stringify({ sessionId, name: `frame-${Date.now()}.jpg`, size: blob.length, contentType: "image/jpeg" }),
|
|
464
|
+
});
|
|
465
|
+
const meta = await r.json();
|
|
466
|
+
const fd = new FormData();
|
|
467
|
+
// @ts-ignore - RN FormData accepts {uri,name,type}
|
|
468
|
+
fd.append("file", { uri: `data:image/jpeg;base64,${base64}`, name: "frame.jpg", type: "image/jpeg" });
|
|
469
|
+
const up = await fetch(`${serverUrl}${meta.uploadUrl}`, { method: "POST", body: fd as any });
|
|
470
|
+
return up.ok ? meta.path : null;
|
|
471
|
+
} catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
450
476
|
function base64ToUint8Array(b64: string): Uint8Array {
|
|
451
477
|
const bin = globalThis.atob ? globalThis.atob(b64) : Buffer.from(b64, "base64").toString("binary");
|
|
452
478
|
const out = new Uint8Array(bin.length);
|