Skip to content

Upload Frame

The client uploads individual JPEG frames to the server during a session. The upload path depends on the platform:

  • iOS: Uses URLSessionConfiguration.background() to upload directly to S3 via presigned URLs. Uploads persist through app suspension, termination, and reboot.
  • Android: Uses fetch API to POST frame bytes to the server (existing path).

The server Lambda maintains a rolling frame counter, writes each frame to S3 inside its window directory, and triggers ingest when 60 frames (one full window) have been received and the matching audio chunk is also ready.

iOS Background Upload Path

Presigned URL Generation

When the iOS app enqueues a frame for background upload, it first requests a presigned URL:

GET /sessions/{sessionId}/frames/presigned-url?filePath={encodeURIComponent(filepath)}
Response: { "presignedURL": "https://s3.amazonaws.com/bucket/..." }

The presigned URL is a PUT endpoint (not POST) that allows direct S3 uploads without authentication.

IMPORTANT: Current TTL is 3600 seconds (1 hour). This is insufficient for the design intent: - Network unavailable when enqueue is called → upload queued on disk - App backgrounded/suspended for hours → TTL expires before upload resumes - Device rebooted → TTL may expire before OS allows upload - App killed and relaunched days later (cold-start recovery) → presigned URL has expired

This is a known limitation. A follow-up issue should increase the TTL to >= 86400 seconds (24 hours). See: https://github.com/EncacheAI/encache/issues (TBD after merge).

Native Enqueue Contract

Once the presigned URL is obtained, the iOS app calls:

// filePath: plain filesystem path (no file:// prefix)
// presignedURL: S3 presigned PUT URL (TTL >= 24h required)
// headers: must include Content-Type
const result = await WearablesModule.enqueueBackgroundUpload(
  "/path/to/recordings/sessionId/frame-001.jpg",
  "https://s3.amazonaws.com/...",
  { "Content-Type": "image/jpeg" },
);
// result: { taskId: number, status: "enqueued" }

This is an Expo native function that: - Creates a URLSessionUploadTask with the presigned URL - Calls resume() to start the background upload - Returns immediately (does NOT wait for completion) - The OS manages retries and will resume uploads even if the app is terminated

Dedup Index

The native delegate writes completed uploads to a disk-persisted dedup index:

${FileSystem.documentDirectory}/recordings/{sessionId}/uploaded_frames.json

This is an array of filenames that have been successfully uploaded. On cold-start recovery, unuploaded frames are re-enqueued.

Android / Server Path (POST)

Flow

  1. Client sends frame — A POST request is sent to /sessions/{sessionId}/frames with a JPEG image as the body (possibly base64-encoded by API Gateway).

  2. Session validationsessionId is extracted from path parameters; missing returns HTTP 400.

  3. Atomic frame counter increment — DynamoDB frameCount is incremented by 1 using UpdateExpression SET frameCount = frameCount + :one with ReturnValues=ALL_NEW. The current windowIndex and userId are read from the returned attributes.

  4. Frame storage — The frame is written to S3 at sessions/{sessionId}/window_{windowIndex:03d}/frame_{frameNum:03d}.jpg (0-indexed within the window).

  5. Window completion check — If frameCount >= FRAMES_PER_WINDOW (60):

  6. DynamoDB is atomically updated: frameCount resets to 0, currentFrameWindow increments by 1, and windowIndex is added to completedFrameWindows.
  7. Ingest is triggered if captureMode == "audio_only" OR windowIndex already appears in completedAudioWindows.

  8. Ingest deduplication — Same conditional ADD to ingestTriggeredWindows as the audio upload flow, preventing duplicate ingest invocations.

  9. Response — Returns { "frameCount": n, "windowIndex": n, "ingestTriggered": true/false }.

Entry Point

  • Lambda: main/server/api/sessions/frames/app.pylambda_handler
  • HTTP method: POST /sessions/{sessionId}/frames (API Gateway)

Key Constants

  • FRAMES_PER_WINDOW = 60 — number of frames that complete one window

Dependencies

  • S3 bucket: BUCKET_NAME
  • DynamoDB: SESSIONS_TABLE_NAME
  • Lambda: INGEST_FUNCTION_NAME

Error Cases

Condition Response
Missing sessionId path param 400