Package: @lookout/react v0.1.0
Peer Dependencies: React 18+ or 19+
Exports: ESM + CJS with TypeScript declarations
import { LookoutProvider, LookoutRecorder } from "@lookout/react";
function App() {
return (
<LookoutProvider token="your-64-char-hex-token" apiBaseUrl="https://lookout.hackclub.com">
<LookoutRecorder />
</LookoutProvider>
);
}For headless usage:
import { LookoutProvider, useLookout } from "@lookout/react";
function MyRecorder() {
const { state, actions } = useLookout();
return (
<div>
<p>Status: {state.status}</p>
<p>Time: {state.displaySeconds}s</p>
<button onClick={actions.startSharing}>Start</button>
<button onClick={actions.pause}>Pause</button>
<button onClick={() => actions.stop({ name: "My timelapse" })}>Stop</button>
</div>
);
}
function App() {
return (
<LookoutProvider token="..." apiBaseUrl="https://lookout.hackclub.com">
<MyRecorder />
</LookoutProvider>
);
}For camera (webcam) capture:
import { LookoutProvider, LookoutRecorder } from "@lookout/react";
function App() {
return (
<LookoutProvider
token="your-64-char-hex-token"
apiBaseUrl="https://lookout.hackclub.com"
capture={{ mode: "camera" }}
>
<LookoutRecorder />
</LookoutProvider>
);
}Context provider that configures the API client and settings for all child hooks/components.
<LookoutProvider
token="..."
apiBaseUrl="https://lookout.hackclub.com"
capture={{ intervalMs: 30000, jpegQuality: 0.9 }}
autoStart
>
{children}
</LookoutProvider>Props (LookoutProviderProps extends LookoutConfig):
| Prop | Type | Default | Description |
|---|---|---|---|
token |
TokenProvider |
required | Session token — string, sync getter, or async getter |
apiBaseUrl |
string |
"" (same origin) |
Server API base URL |
capture |
CaptureSettings |
See below | Screenshot capture settings |
retry |
RetrySettings |
See below | Upload retry/buffer settings |
callbacks |
LookoutCallbacks |
{} |
Lifecycle event callbacks |
statusPollIntervalMs |
number |
3000 |
Compilation status poll interval (ms) |
autoStart |
boolean |
false |
Auto-start screen sharing on mount |
children |
ReactNode |
required | Child components |
type TokenProvider =
| string // static token
| (() => string) // sync getter
| (() => Promise<string>); // async getter (e.g. fetch from your backend)| Field | Type | Default | Description |
|---|---|---|---|
intervalMs |
number |
60000 |
Screenshot interval in ms |
jpegQuality |
number |
0.85 |
JPEG quality (0–1) |
maxWidth |
number |
1920 |
Max capture width in px |
maxHeight |
number |
1080 |
Max capture height in px |
displayMediaConstraints |
DisplayMediaStreamOptions |
— | Override getDisplayMedia constraints |
mode |
CaptureMode |
"screen" |
Capture source: "screen" or "camera" |
camera |
CameraSettings |
{} |
Camera-specific settings (only used when mode is "camera") |
type CaptureMode = "screen" | "camera";| Field | Type | Default | Description |
|---|---|---|---|
deviceId |
string |
— | Preferred camera device ID (from enumerateDevices). Omit for default camera |
userMediaConstraints |
MediaTrackConstraints |
— | Additional getUserMedia video constraints (merged with defaults) |
| Field | Type | Default | Description |
|---|---|---|---|
maxRetries |
number |
3 |
Max retries per upload step |
retryDelays |
number[] |
[2000, 4000, 8000] |
Backoff delays per attempt (ms) |
maxPendingBuffer |
number |
5 |
Max screenshots buffered in memory |
Primary hook — composes all lower-level hooks and orchestrates the capture-upload loop. Must be used within <LookoutProvider>.
const { state, actions } = useLookout();Returns:
| Field | Type | Description |
|---|---|---|
status |
RecorderStatus |
Current status (see below) |
isSharing |
boolean |
Whether media capture is active (screen sharing or camera recording) |
isRecording |
boolean |
true when actively capturing (isSharing && (status === "active" || status === "pending")). Use this instead of compound checks in UI logic. |
trackedSeconds |
number |
Best-known tracked time — max of server value, last upload confirmation, and local estimate from completed uploads. Updates per-upload, not just on poll. |
displaySeconds |
number |
Client-interpolated display time (ticks every second via RAF). Monotonic — never jumps backward on server sync. |
screenshotCount |
number |
Number of confirmed screenshots — max of server count and local upload count, so it updates immediately on upload. |
uploads |
UploadState |
Upload queue: { pending, completed, failed } |
lastScreenshotUrl |
string | null |
Object URL of last captured screenshot |
videoUrl |
string | null |
Video URL when complete. Auto-fetched from server when status reaches "complete". |
error |
string | null |
Error message when status is "error" |
captureMode |
CaptureMode |
Active capture mode ("screen" or "camera") |
availableCameras |
MediaDeviceInfo[] |
Available camera devices (populated when mode is "camera") |
selectedCameraId |
string | null |
Currently selected camera device ID |
isPreviewing |
boolean |
Whether camera is in preview mode (stream live, capture loop not started). Camera mode only. |
previewStream |
MediaStream | null |
Live camera MediaStream for rendering in a <video> element. Available during preview and recording in camera mode. |
| Method | Signature | Description |
|---|---|---|
startSharing |
() => Promise<void> |
Start capture source and begin the capture-upload loop. In camera mode, reuses the preview stream if one is active. |
stopSharing |
() => void |
Stop capture source without stopping session (auto-pauses) |
pause |
() => Promise<void> |
Pause the session |
resume |
() => Promise<void> |
Resume a paused session |
stop |
(options?: { name?: string }) => Promise<void> |
Stop the session and trigger compilation. Optionally name the timelapse before stopping. |
selectCamera |
(deviceId: string) => void |
Select a camera device by ID. Works during preview and recording. |
startPreview |
() => Promise<void> |
Acquire camera stream for live preview without starting the capture loop. Camera mode only. |
stopPreview |
() => void |
Stop the preview stream. Camera mode only. |
Server states plus client-only states:
type RecorderStatus =
| "pending" // session created, not yet started
| "active" // recording in progress
| "paused" // paused by user or auto-pause
| "stopped" // stopped, waiting for compilation
| "compiling" // video being compiled
| "complete" // video ready
| "failed" // compilation failed
| "loading" // (client-only) initial session fetch
| "no-token" // (client-only) no token provided/resolved
| "error"; // (client-only) error stateHandles getDisplayMedia, canvas snapshots, and stream lifecycle. Can be used standalone (without provider) by passing explicit settings.
const { isSharing, startSharing, takeScreenshot, stopSharing } = useScreenCapture();Parameters:
| Param | Type | Description |
|---|---|---|
overrides |
CaptureSettings |
Optional overrides (merged with provider config) |
Returns:
| Field | Type | Description |
|---|---|---|
isSharing |
boolean |
Whether screen sharing is active |
startSharing |
() => Promise<void> |
Prompt user for screen sharing |
takeScreenshot |
() => Promise<CaptureResult | null> |
Capture current frame as JPEG blob |
stopSharing |
() => void |
Stop all media tracks |
interface CaptureResult {
blob: Blob; // JPEG image blob
width: number; // Pixel width
height: number; // Pixel height
}Handles getUserMedia (webcam), device enumeration, canvas snapshots, and stream lifecycle. Supports a two-phase flow: preview (stream live, no capture loop) → recording (isSharing = true, triggers capture loop in useLookout). Can be used standalone (without provider) by passing explicit settings.
const {
isSharing, startSharing, takeScreenshot, stopSharing,
devices, selectedDeviceId, selectDevice,
isPreviewing, previewStream, startPreview, stopPreview,
} = useCameraCapture();Parameters:
| Param | Type | Description |
|---|---|---|
overrides |
CaptureSettings |
Optional overrides (merged with provider config) |
Returns:
| Field | Type | Description |
|---|---|---|
isSharing |
boolean |
Whether camera is recording (capture loop active) |
startSharing |
() => Promise<void> |
Start recording — reuses preview stream if active, otherwise acquires one |
takeScreenshot |
() => Promise<CaptureResult | null> |
Capture current frame as JPEG blob |
stopSharing |
() => void |
Stop recording and release camera stream |
devices |
MediaDeviceInfo[] |
Available camera devices (auto-updated on connect/disconnect) |
selectedDeviceId |
string | null |
Currently selected camera device ID |
selectDevice |
(deviceId: string) => void |
Switch to a different camera (restarts stream, preserves preview/recording mode) |
isPreviewing |
boolean |
Whether camera is in preview mode (stream live, not recording) |
previewStream |
MediaStream | null |
Live camera MediaStream — render in a <video> element for live preview |
startPreview |
() => Promise<void> |
Acquire camera stream for preview without starting the capture loop |
stopPreview |
() => void |
Stop preview and release camera stream |
Notes:
- Enumerates devices on mount and on the
devicechangeevent. - Safari may return devices with empty labels before first
getUserMediacall — labels are populated after the first stream is acquired. selectDevicewhile streaming will restart the stream with the new device, preserving the current mode (preview or recording).- Stream is cleaned up on unmount if the user navigates away without recording.
Manages the upload queue with retries and backoff. Must be used within <LookoutProvider>.
const { enqueue, uploads, trackedSeconds, lastScreenshotUrl, nextExpectedAt, lastError } = useUploader();Returns:
| Field | Type | Description |
|---|---|---|
enqueue |
(capture: CaptureResult) => void |
Add a capture to the upload queue |
uploads |
UploadState |
{ pending, completed, failed } counts |
trackedSeconds |
number |
Server-reported tracked time after last confirmation |
lastScreenshotUrl |
string | null |
Object URL of last uploaded screenshot |
nextExpectedAt |
string | null |
ISO timestamp of when server expects next screenshot |
lastError |
string | null |
Last upload error message |
Manages session state, status polling, and server interactions. Must be used within <LookoutProvider>.
const session = useSession();Returns:
| Field | Type | Description |
|---|---|---|
status |
RecorderStatus |
Current session status |
name |
string |
Timelapse name |
trackedSeconds |
number |
Server-tracked seconds |
screenshotCount |
number |
Confirmed screenshot count |
startedAt |
string | null |
Session start timestamp |
createdAt |
string | null |
Session creation timestamp |
totalActiveSeconds |
number |
Accumulated active time |
error |
string | null |
Error message |
pause |
() => Promise<void> |
Pause the session |
resume |
() => Promise<void> |
Resume the session |
stop |
(name?: string) => Promise<void> |
Stop the session. Optionally name the timelapse before stopping (non-fatal if rename fails). |
reload |
() => Promise<void> |
Re-fetch session from server |
updateTrackedSeconds |
(seconds: number) => void |
Update tracked seconds locally |
setError |
(error: string | null) => void |
Set error state |
Client-side interpolated timer. Uses server-provided seconds as a base, ticks every second via requestAnimationFrame, and maintains a monotonic ratchet so the display never jumps backward when the server syncs.
When a new serverTrackedSeconds arrives, the timer takes the higher of the current display value and the server value as its new base, then continues counting from there. This prevents visible snaps (e.g., display at 11:05, server reports 11:00 → display stays at 11:05).
const displaySeconds = useSessionTimer(trackedSeconds, isActive);Parameters:
| Param | Type | Description |
|---|---|---|
serverTrackedSeconds |
number |
Base tracked time (from server or local estimate) |
isActive |
boolean |
Whether to tick the timer |
Returns: number — interpolated display seconds (monotonically increasing while active)
Manages session tokens in localStorage with cross-tab sync. No provider required.
const store = useTokenStore();Returns (UseTokenStore):
| Field | Type | Description |
|---|---|---|
tokens |
TokenEntry[] |
Active (non-archived) tokens |
archivedTokens |
TokenEntry[] |
Archived tokens |
addToken |
(token: string, label?: string) => void |
Add a token |
archiveToken |
(token: string) => void |
Archive a token |
unarchiveToken |
(token: string) => void |
Unarchive a token |
removeToken |
(token: string) => void |
Permanently remove a token |
getAllTokenValues |
() => string[] |
Get all active token strings |
hasToken |
(token: string) => boolean |
Check if a token exists |
interface TokenEntry {
token: string;
addedAt: string; // ISO timestamp
label?: string;
archived: boolean;
}Storage key: lookout-tokens
Fetches multiple sessions for gallery display via the batch endpoint. Auto-refreshes on tab focus. No provider required.
const { sessions, loading, error, refresh } = useGallery({
apiBaseUrl: "https://lookout.hackclub.com",
tokens: ["token1", "token2"],
});Parameters (UseGalleryOptions):
| Field | Type | Description |
|---|---|---|
apiBaseUrl |
string |
Server API base URL |
tokens |
string[] |
Token strings to fetch |
Returns (UseGallery):
| Field | Type | Description |
|---|---|---|
sessions |
SessionSummary[] |
Fetched sessions (newest first) |
loading |
boolean |
Whether fetch is in progress |
error |
string | null |
Fetch error message |
refresh |
() => void |
Manually re-fetch |
Simple hash-based router for single-page app navigation. No provider required.
const { route, navigate } = useHashRouter();Returns:
| Field | Type | Description |
|---|---|---|
route |
Route |
Current route |
navigate |
(route: Route) => void |
Navigate to a route |
type Route =
| { page: "gallery" } // #/
| { page: "record"; token: string } // #/record?token=...
| { page: "session"; token: string }; // #/session?token=...Drop-in recorder widget. Handles the full lifecycle: capture, upload, pause/resume/stop, compilation polling, and video display. Adapts its UI based on the configured capture.mode. Must be used within <LookoutProvider>.
<LookoutRecorder />No props — reads everything from context.
Renders based on status:
loading— spinnerno-token— "no session token" messageerror— error displaystopped/compiling/complete/failed—<ProcessingState>pending/active/paused— capture UI (varies by mode, see below)
Screen mode (capture.mode: "screen", default):
<StatusBar>+<ScreenPreview>+<RecordingControls>- Copy: "Share Screen & Start Recording", "Share Screen & Resume"
Camera mode (capture.mode: "camera"):
- Three-phase flow: idle → preview → recording
- Idle: "Start Camera" button (acquires camera stream for preview)
- Preview:
<CameraPreview>(live video) +<CameraSelector>(if multiple cameras) + "Start Recording" / "Cancel" - Recording:
<CameraPreview>+ standard Pause/Stop controls - Copy adapts: "Start Camera", "Start Recording", "Start Camera & Resume"
Displays timer, screenshot count, and upload queue status.
<StatusBar displaySeconds={120} screenshotCount={5} uploads={{ pending: 1, completed: 4, failed: 0 }} />Props (StatusBarProps):
| Prop | Type | Description |
|---|---|---|
displaySeconds |
number |
Seconds to display (formatted as H:MM:SS) |
screenshotCount |
number |
Confirmed screenshot count |
uploads |
UploadState |
{ pending, completed, failed } |
Action buttons for start/pause/resume/stop, adapts to current state.
<RecordingControls
status="active"
isSharing={true}
onStartSharing={() => {}}
onPause={() => {}}
onResume={() => {}}
onStop={() => {}}
/>Props (RecordingControlsProps):
| Prop | Type | Description |
|---|---|---|
status |
RecorderStatus |
Current session status |
isSharing |
boolean |
Whether screen sharing is active |
onStartSharing |
() => void |
Start screen sharing callback |
onPause |
() => void |
Pause callback |
onResume |
() => void |
Resume callback |
onStop |
() => void |
Stop callback |
loading |
boolean? |
Show loading state on buttons |
Displays the last captured screenshot. Renders nothing if no image.
<ScreenPreview imageUrl={lastScreenshotUrl} />Props (ScreenPreviewProps):
| Prop | Type | Description |
|---|---|---|
imageUrl |
string | null |
Object URL of the screenshot |
Live camera preview using a <video> element. Falls back to a static image when no stream is provided. Mirrors the video horizontally for a natural selfie-view.
<CameraPreview stream={state.previewStream} fallbackImageUrl={state.lastScreenshotUrl} />Props (CameraPreviewProps):
| Prop | Type | Description |
|---|---|---|
stream |
MediaStream | null |
Live camera MediaStream to display |
fallbackImageUrl |
string | null? |
Fallback static image URL (e.g. last captured screenshot) |
Camera device picker dropdown. Renders nothing if no devices are available.
<CameraSelector
devices={state.availableCameras}
selectedDeviceId={state.selectedCameraId}
onSelect={actions.selectCamera}
disabled={state.isSharing}
/>Props (CameraSelectorProps):
| Prop | Type | Description |
|---|---|---|
devices |
MediaDeviceInfo[] |
Available camera devices |
selectedDeviceId |
string | null |
Currently selected device ID |
onSelect |
(deviceId: string) => void |
Device selection callback |
disabled |
boolean? |
Disable selection (e.g., while recording) |
Displays compilation progress, video player, or failure state.
<ProcessingState status="compiling" trackedSeconds={300} />
<ProcessingState status="complete" trackedSeconds={300} videoUrl="https://..." />Props (ProcessingStateProps):
| Prop | Type | Description |
|---|---|---|
status |
string |
Session status |
trackedSeconds |
number |
Tracked time to display |
videoUrl |
string? |
Video URL (shown when complete) |
error |
string? |
Error message |
onVideoLoaded |
() => void? |
Callback when video element loads |
Wraps <ProcessingState> with automatic video URL fetching from the API. Must be used within <LookoutProvider>.
<ResultView status="complete" trackedSeconds={300} />Props (ResultViewProps):
| Prop | Type | Description |
|---|---|---|
status |
RecorderStatus |
Session status |
trackedSeconds |
number |
Tracked time |
Grid of session cards with loading, empty, and error states.
<Gallery
sessions={sessions}
loading={false}
error={null}
onSessionClick={(token) => navigate({ page: "session", token })}
onArchive={(token) => store.archiveToken(token)}
onRefresh={refresh}
/>Props (GalleryProps):
| Prop | Type | Description |
|---|---|---|
sessions |
SessionSummary[] |
Sessions to display |
loading |
boolean |
Show skeleton loader |
error |
string | null |
Error message |
onSessionClick |
(token: string) => void? |
Card click handler |
onArchive |
(token: string) => void? |
Archive button handler |
onRefresh |
() => void? |
Refresh button handler |
Individual session card with thumbnail, status badge, timelapse name, tracked time, and recording date.
<SessionCard session={session} onClick={() => {}} onArchive={() => {}} />Props (SessionCardProps):
| Prop | Type | Description |
|---|---|---|
session |
SessionSummary |
Session data |
onClick |
() => void? |
Click handler |
onArchive |
() => void? |
Archive button handler |
Full session detail view with video player, stats, and compilation polling. Standalone (no provider needed).
<SessionDetail
token="..."
apiBaseUrl="https://lookout.hackclub.com"
onBack={() => navigate({ page: "gallery" })}
onArchive={() => store.archiveToken(token)}
/>Props (SessionDetailProps):
| Prop | Type | Description |
|---|---|---|
token |
string |
Session token |
apiBaseUrl |
string |
Server API base URL |
onBack |
() => void? |
Back button handler |
onArchive |
() => void? |
Archive button handler |
Pass via LookoutProvider's callbacks prop:
<LookoutProvider
token="..."
callbacks={{
onShareStart: () => console.log("sharing started"),
onCapture: (capture) => console.log("captured", capture.width, "x", capture.height),
onUploadSuccess: ({ screenshotId, trackedSeconds }) => {},
onUploadFailure: (error) => {},
onPause: ({ totalActiveSeconds }) => {},
onResume: () => {},
onStop: ({ trackedSeconds, totalActiveSeconds }) => {},
onComplete: ({ videoUrl }) => {},
onCompilationFailed: () => {},
onError: (error, context) => {},
onStatusChange: (prev, next) => {},
}}
>| Callback | Arguments | When |
|---|---|---|
onShareStart |
— | Screen sharing started |
onShareStop |
— | Screen sharing ended |
onCapture |
CaptureResult |
Screenshot captured (before upload) |
onUploadSuccess |
{ screenshotId, trackedSeconds } |
Screenshot uploaded and confirmed |
onUploadFailure |
Error |
Upload failed after all retries |
onPause |
{ totalActiveSeconds } |
Session paused |
onResume |
— | Session resumed |
onStop |
{ trackedSeconds, totalActiveSeconds } |
Session stopped |
onComplete |
{ videoUrl } |
Compilation complete, video ready |
onCompilationFailed |
— | Compilation failed |
onError |
(Error, context: string) |
Any non-fatal error |
onStatusChange |
(prev, next) |
Status transition |
Standalone API client with no React dependency. Useful for server-side or non-React contexts.
import { createLookoutClient } from "@lookout/react";
const client = createLookoutClient({
baseUrl: "https://lookout.hackclub.com",
token: "your-token",
});
const session = await client.getSession();Options (CreateClientOptions):
| Field | Type | Description |
|---|---|---|
baseUrl |
string |
Server API base URL |
token |
TokenProvider |
Session token (string, sync, or async getter) |
Returns (LookoutClient):
| Method | Signature | Description |
|---|---|---|
resolveToken |
() => Promise<string> |
Resolve the token value |
getSession |
() => Promise<SessionResponse> |
Fetch session status |
getUploadUrl |
() => Promise<UploadUrlResponse> |
Get presigned upload URL |
confirmScreenshot |
(body) => Promise<ConfirmScreenshotResponse> |
Confirm upload |
uploadToR2 |
(uploadUrl, blob) => Promise<void> |
PUT blob to presigned URL |
pause |
() => Promise<PauseResponse> |
Pause session |
resume |
() => Promise<ResumeResponse> |
Resume session |
stop |
() => Promise<StopResponse> |
Stop session |
rename |
(name: string) => Promise<RenameSessionResponse> |
Rename the timelapse |
getStatus |
() => Promise<StatusResponse> |
Poll compilation status |
getVideo |
() => Promise<VideoResponse> |
Get video URL |
The SDK exports styled UI primitives used by its components. All use inline styles (no CSS imports needed).
| Export | Description |
|---|---|
Button |
Styled button with variants: primary, secondary, success, warning, danger, ghost and sizes: sm, md, lg |
Spinner |
Loading spinner with sizes: sm, md, lg |
Badge |
Status badge with variants: default, overlay |
Card |
Styled card container |
ErrorDisplay |
Error message display with variants: inline, banner, page |
PageContainer |
Page layout wrapper |
Skeleton / GallerySkeleton / SessionDetailSkeleton / RecordPageSkeleton |
Loading skeletons |
colors / spacing / radii / fontSize / fontWeight / statusConfig |
Theme tokens |
Formats seconds as H:MM:SS or M:SS. Used for the live timer display.
import { formatTime } from "@lookout/react";
formatTime(0); // "0:00"
formatTime(65); // "1:05"
formatTime(3661); // "1:01:01"Formats seconds as human-readable tracked time. Used for static time displays (gallery cards, stats) where second-level precision is unnecessary.
import { formatTrackedTime } from "@lookout/react";
formatTrackedTime(0); // "< 1min"
formatTrackedTime(300); // "5min"
formatTrackedTime(5640); // "1h 34min"
formatTrackedTime(7200); // "2h"