@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akhilpulse/samadhaan-session-replay",
3
- "version": "0.2.1",
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.1";
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
- if (config.disableScreenshots || !sessionId) return;
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
- const interval = liveMode ? 200 : 10000;
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
- if (paused || keyboardVisible) { setTimeout(tick, interval); return; }
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
- // Cap capture width to keep payloads small. Dashboard viewport is 960px;
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: liveMode ? 0.3 : 0.45,
236
+ quality: 0.25,
228
237
  result: "base64",
229
- width: liveMode ? 480 : 540,
238
+ width: 360,
230
239
  });
231
- // upload via request-url then PUT
232
240
  const blob = base64ToUint8Array(uri);
233
- let path: string | null = null;
234
- try {
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
- setTimeout(tick, interval);
254
+ const elapsed = Date.now() - startedAt;
255
+ setTimeout(tick, Math.max(0, liveTargetMs - elapsed));
255
256
  };
256
- setTimeout(tick, interval);
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);