Room Export Blueprint — Reproducing Sessions from Exports

Purpose

A room export is a self-contained ZIP archive of an entire video identification session. This blueprint explains exactly what’s inside, how to extract useful data from it, and how to reproduce/replay the session for debugging, auditing, or development purposes.

What Is a Room Export?

A room export captures everything about a single videochat identification session:

  • The room page (rendered HTML with embedded data)
  • The replay page (interactive session replay)
  • Flow pages (workflow step views)
  • Video/audio recordings (decrypted from encrypted storage)
  • Screenshots (face photos, document scans, etc.)
  • Presentation attachments (PDFs shown during call)
  • data_index.json — structured metadata about the session
  • screenshots.json — screenshot metadata
  • replaydata.js — full activity timeline for replay

Export ZIP Structure

room-export-{roomId}.zip
├── index.html                          # Room detail page (static HTML)
├── replay.html                         # Interactive replay page
├── flow-{flowId}.html                  # Workflow views (one per flow)
├── replaydata.js                       # Activity timeline (JS variable)
├── data_index.json                     # Structured session metadata
├── screenshots.json                    # Screenshot metadata array
│
├── records/{roomId}/                   # Media files
│   ├── {filename}.webm                 # Video recordings (customer + operator)
│   ├── {filename}.mkv                  # Alternative codec containers
│   └── {filename}.mp4                  # H.264 recordings
│
├── raw/                                # Raw MJR files (if exportMjr=true)
│   └── {janus recording files}
│
├── presentation/                       # Documents shown during session
│   ├── attachment_{id}.pdf             # PDF attachments
│   └── attachments.js                  # Base64-encoded attachment data
│
├── api/
│   ├── screenshot/{roomId}/            # Screenshot images
│   │   └── {attachmentId}
│   └── emrtd-photo/{roomId}/           # eMRTD passport photos
│       └── {photoId}
│
├── css/                                # Stylesheets (for offline viewing)
├── js/                                 # JavaScript (for offline viewing)
├── img/                                # Images and icons
├── font/                               # Fonts
└── metronic/                           # UI framework assets

Key Data Files

data_index.json — The Rosetta Stone

{
  "sourceSystemName": "facekom-nusz",
  "exportedAt": 1710758400,
  "replayData": {
    "room": { /* OperatorRoomData */ },
    "activityLog": [ /* Activity[] */ ],
    "flowLog": [ /* FlowLogEntry[] */ ],
    "customerData": [ /* CustomerDataField[] */ ],
    "validationLog": [ /* ValidationEntry[] */ ],
    "recognitionLog": [ /* RecognitionResult[] */ ],
    "roomSettings": { /* RoomSettings */ },
    "portalData": { /* PortalData */ },
    "configState": { /* ConfigState */ },
    "attachments": [ /* Attachment[] */ ]
  },
  "customer": {
    "id": 123,
    "name": "...",
    "email": "...",
    "phone": "...",
    "searchFields": { /* ... */ }
  }
}

replaydata.js — Activity Timeline

This is the same replayData object but wrapped as a JS variable for the offline replay page:

const replaydata = {
  room: { /* room metadata */ },
  activityLog: [
    {
      type: "videochat:join",
      from: "operator",
      createdAt: 1710758400,
      content: { /* event-specific data */ }
    },
    {
      type: "videochat:webrtcStream",
      from: "customer",
      createdAt: 1710758401,
      content: {
        videoUrl: "records/123/customer_video.webm",
        audioUrl: "records/123/customer_audio.webm"
      }
    },
    {
      type: "attachment",
      createdAt: 1710758500,
      content: {
        attachmentUrl: "api/screenshot/123/456",
        screenshotCategory: "face_photo"
      }
    },
    {
      type: "videochat:flow:state",
      content: {
        flowLog: [ /* task completions, conditions, results */ ]
      }
    },
    {
      type: "videochat:customerData:update",
      content: {
        customerData: { /* portal field values */ }
      }
    }
    // ... many more activity types
  ]
}

screenshots.json — Screenshot Index

[
  {
    "screenshotUrl": "api/screenshot/123/456",
    "category": "face_photo",
    "createdAt": "2026-03-15T10:30:00.000Z"
  },
  {
    "screenshotUrl": "api/screenshot/123/789",
    "category": "document_front",
    "createdAt": "2026-03-15T10:31:00.000Z"
  }
]

Activity Types You’ll Find

Activity TypeWhat It Records
videochat:joinOperator/customer joined the room
videochat:leaveOperator/customer left
videochat:closeRoom was closed
videochat:webrtcStreamVideo/audio recording file paths
videochat:chat:messageChat messages exchanged
videochat:flow:stateComplete workflow execution log
videochat:customerData:updateCustomer data changes during session
videochat:config:stateFeature configuration snapshot
videochat:presentation:openDocument presented to customer
videochat:documents:showDocument shown in call
attachmentScreenshot captured (face, document, etc.)
selfService:attachmentSelf-service screenshot
videochat:screenshot:remoteRemote screenshot request
videochat:selfMuteMute/unmute events
videochat:remoteUserHoldMediaHold media state

How to Reproduce a Session

Step 1: Extract and Examine

# Unzip the export
unzip room-export-123.zip -d room-123/
cd room-123/
 
# View the data index
cat data_index.json | jq .
 
# List all activities by type
cat data_index.json | jq '.replayData.activityLog | group_by(.type) | map({type: .[0].type, count: length})'
 
# View customer data
cat data_index.json | jq '.customer'
 
# View flow execution
cat data_index.json | jq '.replayData.flowLog'

Step 2: View the Session Offline

# Open the replay page in a browser (works offline!)
open replay.html
 
# Or open the room detail page
open index.html
 
# Or open a specific flow page
open flow-456.html

Offline Viewing

The export is fully self-contained. All CSS, JS, fonts, and images are included. The replay.html page will play back the session timeline using replaydata.js, showing video recordings and activity events in sequence.

Step 3: Extract Screenshots

# List all screenshots
cat screenshots.json | jq '.[].screenshotUrl'
 
# Screenshots are in api/screenshot/{roomId}/ as raw image files
ls api/screenshot/123/
 
# View a specific screenshot
open api/screenshot/123/456

Step 4: Extract Video Recordings

# Videos are in records/{roomId}/
ls records/123/
 
# Play a recording
vlc records/123/customer_video.webm
 
# Extract audio only
ffmpeg -i records/123/customer_video.webm -vn -acodec copy customer_audio.opus

Operator Blackout

If the export was created with blackoutOperator=true, operator video tracks are replaced with audio-only files (video stripped via ffmpeg). Customer video remains intact.

Step 5: Analyze the Workflow

# Extract the flow log
cat data_index.json | jq '.replayData.flowLog'
 
# Each flow log entry contains:
# - task name, status (completed/skipped/failed)
# - task data (customer inputs, verification results)
# - conditions evaluated
# - timestamps
 
# Extract all task results
cat data_index.json | jq '.replayData.activityLog[] | select(.type == "videochat:flow:state") | .content.flowLog[] | {task: .taskName, status: .status, result: .result}'

Step 6: Extract Face Recognition Data

# Recognition results are in the activity log
cat data_index.json | jq '.replayData.recognitionLog'
 
# This includes:
# - Face detection results (bounding boxes, confidence)
# - Face comparison scores (document photo vs live)
# - Liveness detection results
# - MRZ/OCR data from documents

Step 7: Extract Customer Verification Data

# Customer data as captured during the session
cat data_index.json | jq '.replayData.customerData'
 
# Validation results (identity checks)
cat data_index.json | jq '.replayData.validationLog'
 
# Portal data (structured customer info)
cat data_index.json | jq '.replayData.portalData'

How Room Exports Are Created

Export Flow (Code Path)

sequenceDiagram
    participant Op as Operator
    participant API as exportroom.js
    participant BG as backgroundProcess
    participant Export as ExportRoomPageService
    participant Crawl as simplecrawler
    participant DB as Database
    participant FS as File System

    Op->>API: GET /api/supervisor/export/:id
    API->>API: Check room status (must be closed + converted)
    API->>API: Check view access (ACL)

    alt features.roomExportFromFileSystem
        API->>Export: exportRoomFromFileSystem()
        Export->>Export: createRoomExportZip()
    else Background export (default)
        API->>BG: create BackgroundProcess('roomExport')
        API-->>Op: redirect to /download/{id}
        BG->>Export: createRoomExportZip()
    end

    Export->>Crawl: crawl room page + replay page
    Crawl->>Crawl: Fetch HTML, CSS, JS, images, fonts
    Crawl->>Crawl: Fetch /api/replay-data/{roomId}
    Crawl->>Crawl: Fix paths for offline viewing

    Export->>DB: Load MediaFile + Encryption records
    Export->>FS: Decrypt and copy video files
    Export->>DB: Load Activities (screenshots)
    Export->>FS: Copy screenshot files
    Export->>DB: Load Attachments (presentations)
    Export->>FS: Decrypt and copy PDF attachments
    Export->>DB: getReplayData(roomId)
    Export->>FS: Write data_index.json
    Export->>FS: Write screenshots.json

    Note over Export: Hook: roomExport:encryption
    Note over Export: Hook: roomExport:fileName
    Note over Export: Hook: roomExport:rabRoutesList

    Export->>Export: zipDirectory() (optional encryption)
    Export-->>Op: ZIP file download

Key Services

ServiceFilePurpose
ExportRoomPageServiceserver/service/ExportRoomPageService.jsMain room export logic
ExportSelfServiceRoomPageServiceserver/service/ExportSelfServiceRoomPageService.jsSelf-service room export
ExportImportedRoomPageServiceserver/service/ExportImportedRoomPageService.jsRe-export of imported rooms
ExportServiceBaseserver/service/ExportServiceBase.jsBase class (video, screenshot, ZIP)
ExportScreenshotsServiceserver/service/ExportScreenshotsService.jsScreenshot PDF generation
ExportRawRoomVideoFilesServiceserver/service/ExportRawRoomVideoFilesService.jsRaw MJR file export
RoomExportProcessserver/backgroundProcess/roomExport.process.jsBackground process handler
ImportServiceserver/service/ImportService.jsImport from legacy databases

Export Configuration

{
  "features": {
    "export": true,
    "exportMjr": false,
    "roomExportFromFileSystem": false,
    "roomExportMissingAttachmentsFillEmptyFiles": false
  },
  "roomExport": {
    "exportFlows": true
  },
  "roomExportUrl": "https://oss-lederera.facekomdev.net"
}

Customization Hooks

HookPurpose
roomExport:encryptionProvide ZIP password + encryption method
roomExport:fileNameCustomize export file name
roomExport:rabRoutesListAdd custom routes to the crawl whitelist
roomExport:blackoutMediaTypesCustomize which media types to blackout

Importing Exported Data (Legacy Migration)

The ImportService allows importing room data from legacy FaceKom databases into the current system. This is used when migrating between deployments.

Import Data Model

erDiagram
    ImportedCustomer ||--o{ ImportedRoom : has
    ImportedRoom ||--o{ ImportedFlow : has
    ImportedFlow ||--o{ ImportedFlowTranslation : has
ModelPurpose
ImportedCustomerCustomer record from legacy system
ImportedRoomRoom record with replayData JSON blob
ImportedFlowWorkflow data from legacy system
ImportedFlowTranslationFlow translations from legacy

Import Configuration

{
  "importData": {
    "attachmentDirectory": "/workspace/import_data/attachments",
    "recordDirectory": "/workspace/import_data/records/converted",
    "db": {
      "legacyDB1": "postgresql://dev:dev@localhost:5432/legacy_db_1"
    },
    "encryption": {
      "legacyDB1": "customer-room"
    },
    "excludedColumns": {
      "room": { "legacyDB1": ["roomCodecContainer", "data"] },
      "flow": { "legacyDB1": ["cleared"] },
      "customer": { "legacyDB1": [] }
    }
  }
}

Import Order (Must Be Sequential)

# The ImportService.autoImport() runs in this order:
1. importCustomers()    # ImportedCustomer records
2. importRooms()        # ImportedRoom records + replayData blob
3. importReplays()      # Replay data enrichment
4. importTranslations() # Flow translations
5. importFlows()        # Flow definitions

Reproduction Recipes

Recipe 1: Debug a Failed Verification

# 1. Get the export
# 2. Find the flow execution log
cat data_index.json | jq '.replayData.activityLog[] | select(.type == "videochat:flow:state") | .content.flowLog[] | select(.status == "failed")'
 
# 3. Find what verification step failed and why
# 4. Check the recognition results
cat data_index.json | jq '.replayData.recognitionLog'
 
# 5. View the screenshots that were captured at that point
cat screenshots.json | jq 'sort_by(.createdAt)'
 
# 6. Open the replay to watch the exact moment
open replay.html

Recipe 2: Reproduce a Face Comparison Issue

# 1. Extract face photos
mkdir face_photos
cp api/screenshot/123/* face_photos/
 
# 2. Find the face comparison activity
cat data_index.json | jq '.replayData.activityLog[] | select(.type | contains("face"))'
 
# 3. Get the document photo and live photo paths
# 4. Send them to vuer_cv for re-comparison:
curl -X POST https://cv-lederera.facekomdev.net/face/compare \
  -F "image1=@face_photos/document_face.jpg" \
  -F "image2=@face_photos/live_face.jpg"

Recipe 3: Reproduce a Document OCR Issue

# 1. Find document screenshots
cat screenshots.json | jq '.[] | select(.category | contains("document"))'
 
# 2. Get the document image
cp api/screenshot/123/{document_attachment_id} document.jpg
 
# 3. Re-run OCR on it:
curl -X POST https://cv-lederera.facekomdev.net/document/ocr \
  -F "image=@document.jpg"
 
# 4. Compare with what was captured in the activity log
cat data_index.json | jq '.replayData.activityLog[] | select(.type | contains("ocr"))'

Recipe 4: Check What the Customer Saw

# 1. Open the replay page
open replay.html
 
# 2. The replay shows the session from the operator's perspective
# 3. To understand customer perspective, check:
cat data_index.json | jq '.replayData.activityLog[] | select(.from == "customer")'
 
# 4. Customer-initiated events include:
# - videochat:chat:message (from customer)
# - videochat:selfMute
# - videochat:close (if customer disconnected)

Recipe 5: Reproduce the Entire Session in Dev

# 1. Extract all customer data
cat data_index.json | jq '.customer' > customer.json
cat data_index.json | jq '.replayData.portalData' > portal.json
 
# 2. Create a test customer with the same data in dev environment
# 3. Set up the same flow configuration
cat data_index.json | jq '.replayData.configState' > config_state.json
 
# 4. Start a room and manually replicate the flow steps
# 5. Use the screenshots as reference documents for CV testing

Gotchas and Warnings

Encrypted Exports

Some deployments encrypt the ZIP via the roomExport:encryption hook. You’ll need the password to open it. Check the customization branch for the encryption logic.

Missing Attachments

If features.roomExportMissingAttachmentsFillEmptyFiles is enabled, missing attachment files are replaced with empty files. Check file sizes!

Bug in importedRoom.js setter

The set function for replayData references parsed (undeclared variable) instead of val on line 99. This is a bug that may cause import issues. See tech-debt.

Crawler Timeout

The export crawler has a 120-second timeout (setTimeout(reject, 120 * 1000)). Large rooms with many screenshots may timeout. The background process approach (default) is more reliable.

Self-Signed Certs

The crawler sets ignoreInvalidSSL = true because dev environments use self-signed certificates.