# Field Observer: Realtime Phone Photo & Video Sharing

## What It Is

A browser-based app where multiple phones in the field can capture or upload photos and short videos, and every connected peer sees them appear in realtime. No app install required: open a URL, join a group, start sharing.

Built on the acequia.io peer-to-peer framework, so media flows directly between phones via WebRTC data channels, with WebDAV as persistent storage and fallback relay.

## Core User Flows

### Flow 1: Capture and share
1. Open URL on phone browser (e.g. `https://geo.camera/apps/field-observer/`)
2. Join or create a group (room code, QR code, or shared link)
3. Capture photo or short video using the device camera (via `getUserMedia`)
4. Media appears instantly on all connected peers' screens

### Flow 2: Upload from camera roll
1. Tap "Upload" button (uses `<input type="file" accept="image/*,video/*" capture>`)
2. Pick one or more photos/videos from the phone's gallery
3. Thumbnails generate locally, metadata extracted (EXIF GPS, date)
4. Shared to group same as captured media

### Flow 3: Browse history
1. Join an existing group
2. Feed loads from WebDAV manifest (all previously shared media)
3. Scroll, filter by author or date, tap to view full-res
4. For long videos, scrub to any point (byte-range seeking)

## Why Acequia

The acequia framework already provides every building block:

| Need | Acequia Primitive |
|------|-------------------|
| Peer discovery | Group + Discovery server (WebSocket) |
| Realtime media delivery | WebRTC data channels (`dataBig()` for chunked transfer) |
| Shared feed state | Group Shared State (peer-elected leader, shallow merge) |
| "New photo" notifications | Event Bus (topic: `media/new`, `media/delete`) |
| Persistent storage | WebDAV PUT to `/{group}/media/` |
| Auth | JWT three-mode system (device identity, no signup required) |
| Offline support | Service worker cache + OPFS local storage |

## Architecture Overview

```
┌──────────────────────────────────────────────────┐
│  Phone Browser A                                  │
│  ┌────────────┐  ┌──────────┐  ┌──────────────┐ │
│  │ Camera UI  │→ │ Encoder  │→ │ Share Engine │ │
│  │ getUserMedia│  │ canvas   │  │ WebRTC send  │ │
│  │ -- or --   │  │ thumbnail│  │ WebDAV PUT   │ │
│  │ File picker│  │ EXIF read│  │ OPFS cache   │ │
│  └────────────┘  └──────────┘  └──────┬───────┘ │
│                                       │          │
│  ┌────────────────────────────────────┘          │
│  │ Feed UI (grid of thumbnails + lightbox)       │
│  │ Map view (geo-tagged markers)                 │
│  └───────────────────────────────────────────────│
└──────────────────────────────┬───────────────────┘
                               │ WebRTC data channel
                               │ (+ WS fallback)
┌──────────────────────────────┴───────────────────┐
│  Phone Browser B                                  │
│  (same components, bidirectional)                 │
│  OPFS mirror ← replicates media for availability │
└──────────────────────────────────────────────────┘
         │
         │ WebDAV PUT (persistent copy)
         ▼
┌──────────────────────────┐
│  WebDAV Server           │
│  /{group}/media/         │
│  /{group}/manifest.json  │
│  /{group}/index.json     │  ← byte-offset index for long videos
└──────────────────────────┘
```

## Key Design Decisions

### 1. Capture OR upload, user's choice
Two input paths:
- **Capture:** `getUserMedia` + `<canvas>` for photos, `MediaRecorder` for video
- **Upload:** `<input type="file" accept="image/*,video/*">` for existing camera roll items (supports `multiple` for batch upload)

Both paths converge at the same point: generate thumbnail, extract metadata, feed into share engine.

### 2. OPFS for local media storage
The Origin Private File System replaces IndexedDB for all binary data:
- **No serialization overhead**: files are stored as-is, not wrapped in structured clone
- **Synchronous access in workers**: `createSyncAccessHandle()` for fast reads in service worker
- **No blob size limits**: IndexedDB chokes on large video files in some browsers
- **Natural file hierarchy**: mirrors the WebDAV path structure locally

Layout in OPFS:
```
/field-observer/{groupId}/
  thumb/{mediaId}.jpg
  full/{mediaId}.jpg
  clip/{mediaId}.webm
```

IndexedDB is still used for the small metadata index (manifest entries, peer state, settings) where query-by-key is useful.

### 3. Thumbnail-first, full-res on demand
When media is captured or uploaded:
- Generate a 400px thumbnail on-canvas (fast, small)
- Send thumbnail to all peers via WebRTC event bus immediately (~20-50KB)
- Write full-res to OPFS immediately (local)
- Upload full-res to WebDAV in background
- Peers fetch full-res from WebDAV (or via WebRTC route) when tapped

### 4. Feed as shared state
The media feed is a shared JSON manifest managed by Group Shared State:
```json
{
  "media": {
    "cuid123": {
      "type": "photo",
      "author": "deviceId-abc",
      "authorName": "Steph",
      "timestamp": 1711300000,
      "location": { "lat": 35.88, "lon": -106.30, "alt": 2100 },
      "size": 2456789,
      "duration": null,
      "thumbUrl": "/acq/geo.camera/field-observer/{group}/thumb/cuid123.jpg",
      "fullUrl": "/acq/geo.camera/field-observer/{group}/full/cuid123.jpg",
      "caption": ""
    }
  }
}
```

### 5. Multi-device backup for availability
Any peer can opt in as a **mirror**. Mirrors replicate media they receive into their own OPFS and can serve it to other peers via WebRTC routes. This means:
- If the original author goes offline, mirrors can still serve the full-res file
- If WebDAV is unreachable, the mesh of mirrors keeps media available
- Mirrors advertise what they have via shared state (`mirrors` field per media entry)
- Replication is opportunistic: mirrors pull full-res in background when on WiFi

```json
{
  "cuid123": {
    "mirrors": ["deviceId-abc", "deviceId-def", "deviceId-ghi"],
    "webdav": true
  }
}
```

Fetch priority: OPFS local > WebRTC from nearest mirror > WebDAV server.

### 6. Byte-offset seeking for long videos
Long videos (>30s) need random access without downloading the entire file. Two mechanisms:

**Server-side (WebDAV):** Standard HTTP Range requests. WebDAV servers support `Range: bytes=START-END` headers natively. The browser's `<video>` element issues range requests automatically when the user scrubs the timeline. This works out of the box for videos served from WebDAV URLs.

**Peer-side (WebRTC mirrors):** Since WebRTC data channels don't support HTTP Range headers, we build a time-to-byte index:

```json
{
  "mediaId": "cuid456",
  "duration": 187.4,
  "codec": "video/webm; codecs=vp9,opus",
  "keyframes": [
    { "time": 0.0, "byte": 0 },
    { "time": 2.1, "byte": 84992 },
    { "time": 4.0, "byte": 171008 },
    { "time": 6.1, "byte": 258048 }
  ]
}
```

- **Built at capture/upload time** by scanning the WebM/MP4 container for keyframe offsets (Cluster/Cue elements in WebM, `mdat`/`stss` atoms in MP4)
- **Stored alongside the video** as `{mediaId}-index.json` in OPFS and WebDAV
- **Used by the player**: when user seeks to time T, binary-search the index for the nearest preceding keyframe, request that byte range from the mirror via a custom WebRTC route
- **MediaSource API** on the receiving end: append the byte chunk to a `SourceBuffer` for seamless playback from the seek point

### 7. No signup required
Acequia's device identity (auto-generated key pair) is sufficient. Users pick a display name on first visit, stored in localStorage.

## Screens

### 1. Join Screen
- Group code input (or scan QR)
- Display name field
- "Create New Group" button
- Recent groups list (localStorage)

### 2. Live Feed (main screen)
- Grid of thumbnails, newest first
- Tap to expand (lightbox with swipe)
- Floating action button: capture (camera icon) + upload (gallery icon)
- Peer count badge (top right)
- Map toggle (show photos on map by geo-tag)
- Mirror status indicator (how many copies exist)

### 3. Capture Overlay
- Camera viewfinder (full screen)
- Photo / Video mode toggle
- Front / back camera switch
- Capture button
- Optional caption field before sharing

### 4. Upload Picker
- Native file picker (triggers `<input type="file">`)
- Multi-select supported
- Progress bar per file during upload
- EXIF metadata extraction (GPS, date) shown before confirming

### 5. Map View
- Leaflet map with photo markers at GPS coordinates
- Cluster when zoomed out
- Tap marker to see photo

### 6. Video Player
- Full-screen playback with controls
- Seek bar with keyframe markers
- Buffering indicator for range-loaded segments
- Mirror count badge (availability indicator)

## File Structure

```
apps/field-observer/
  index.html              # SPA shell
  manifest.json           # PWA manifest
  sw-field-observer.js        # App-specific service worker
  css/
    field-observer.css        # All styles
  js/
    app.js                # Entry point, acequia bootstrap
    capture.js            # getUserMedia, MediaRecorder, canvas thumbnail
    upload.js             # File picker, EXIF extraction, batch upload
    feed.js               # Feed state management, render grid
    share-engine.js       # WebRTC send/receive, WebDAV upload, offline queue
    opfs-store.js         # OPFS read/write/mirror management
    video-index.js        # Keyframe index builder and seeker
    map-view.js           # Geo-tagged photo map
    mirror.js             # Replication engine (pull full-res, advertise availability)
    ui/
      join-screen.js      # Group join/create UI
      lightbox.js         # Full-screen photo viewer
      video-player.js     # Seekable video player (MediaSource API)
      capture-overlay.js  # Camera viewfinder UI
  lib/
    acequia.js            # Acequia client (symlink or copy)
```

## Phased Implementation

### Phase 1: Local capture + upload + feed
- Camera capture via getUserMedia
- File upload via `<input type="file">`
- Thumbnail generation (canvas resize)
- OPFS storage for media blobs
- Local-only gallery from OPFS
- No networking yet

### Phase 2: Group join + realtime sharing
- Acequia group bootstrap
- Event bus for `media/new` events
- WebRTC thumbnail distribution
- Live feed updates across peers

### Phase 3: Persistent storage + history
- WebDAV upload (full-res + thumbnail)
- Manifest sync via Group Shared State
- Feed loads from WebDAV on join (history)

### Phase 4: Multi-device backup
- Mirror opt-in per device
- Background replication of full-res to mirrors' OPFS
- Mirror registry in shared state
- Fetch routing: local OPFS > WebRTC mirror > WebDAV

### Phase 5: Geo-tagging + map
- Geolocation API integration
- EXIF GPS extraction from uploaded files
- Map view with photo markers

### Phase 6: Long video + byte-offset seeking
- Keyframe index builder (WebM Cues / MP4 stss parsing)
- Index stored alongside video in OPFS + WebDAV
- MediaSource API player with range loading
- WebRTC route for byte-range requests from mirrors

### Phase 7: Polish
- PWA manifest + install prompt
- Offline queue drain on reconnect
- Notifications for new media
- Caption/annotation support
- Delete/archive actions
- Storage quota management (OPFS eviction policy)

## Non-Goals (for now)

- Live video streaming (this is capture-and-share, not a video call)
- Editing/filters (capture raw, edit elsewhere)
- User accounts or server-side auth (device identity only)
- Desktop-optimized layout (phone-first, desktop works but not priority)
- Transcoding (store media as-captured, play back natively)
