Video Chat Architecture
Technical implementation guide for the embedded video conferencing feature using Daily.co.
Status: Phase 1 (Core), Phase 2 (Specialized Layouts), & Phase 3 (Layout Preview Mode) are VERIFIED and COMMITTED (Jan 29, 2026).
Overview
On Book Pro uses Daily.co for "Headless Video"—embedding media streams directly into the React UI rather than using iframe embeds. This enables specialized, context-aware layouts for theatrical workflows (e.g., script panels alongside actor video feeds).
Directory Structure
src/features/video/
├── api/
│ ├── createRoom.ts # Cloud Function wrapper
│ └── getRoomToken.ts # Token generation wrapper
├── components/
│ ├── VideoView.tsx # Main orchestration component
│ ├── VideoLobby.tsx # Pre-join device selection
│ ├── VideoGrid.tsx # Participant grid management
│ ├── VideoTile.tsx # Individual participant tile
│ ├── VideoControls.tsx # Mute, camera, share, leave
│ └── LayoutPreview.tsx # Development-time mock preview
├── layouts/
│ ├── MeetingLayout.tsx # Screen share focus mode
│ ├── OneOnOneLayout.tsx # Equal side-by-side panels
│ ├── TableReadLayout.tsx # 60/40 PDF + video split
│ └── RehearsalLayout.tsx # Minimal video strip
├── store.ts # Zustand video state
├── permissions.ts # RBAC integration
├── types.ts # Session and participant types
└── index.ts # Barrel exportsCore Patterns
JIT Room Management
Rooms are created on-demand to minimize costs and ensure security:
// Backend: Firebase Cloud Function
const room = await daily.rooms.create({
name: `onbook-${projectId}-${timestamp}`,
properties: {
exp: Math.floor(Date.now() / 1000) + 7200, // 2-hour expiry
enable_transcription: true
}
});- Lifecycle: Rooms expire after 2 hours of inactivity
- Naming:
onbook-{projectId}-{timestamp}for uniqueness - Tokens: Stage Managers receive "owner" tokens; others receive join tokens
Isolated State Management
Video state is intentionally not in the global useAppStore:
// src/features/video/store.ts
export const useVideoStore = create<VideoState>((set) => ({
isInSession: false,
participants: [],
selectedLayout: 'meeting',
isAudioMuted: true, // Privacy default
isVideoMuted: false,
// ... high-frequency updates
}));Rationale: Video metadata (speaking state, media toggles) changes at 60fps. Isolating these updates prevents expensive global re-renders.
Component Orchestration
VideoView.tsx manages the state machine:
- Pre-Session: Shows
VideoLobbyfor device selection - In-Session: Wraps UI in
DailyProviderfrom@daily-co/daily-react - Layout Switching: Dynamically renders the selected layout component
- Cleanup: Calls
daily.destroy()on unmount to release hardware
// Simplified orchestration flow
function VideoView() {
const { isInSession, selectedLayout } = useVideoStore();
if (!isInSession) {
return <VideoLobby onJoin={handleJoin} />;
}
return (
<DailyProvider callObject={callObject}>
{getLayoutComponent(selectedLayout)}
</DailyProvider>
);
}Lobby Implementation
Device Selection
The lobby provides "hair check" functionality before joining:
- Camera Preview: Uses native
getUserMedia(not Daily) for instant feedback - Device Enumeration: Populates dropdowns via
navigator.mediaDevices.enumerateDevices() - Hot-Plug Support: Listens to
devicechangeevents for live hardware updates
Mic Level Visualizer
Real-time audio feedback using Web Audio API:
// Simplified implementation
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
// Connect mic stream to analyser
const source = audioContext.createMediaStreamSource(micStream);
source.connect(analyser);
// Animation loop with RMS calculation
function updateLevel() {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteTimeDomainData(data);
// Calculate RMS
const rms = Math.sqrt(data.reduce((sum, val) => {
const normalized = (val - 128) / 128;
return sum + normalized * normalized;
}, 0) / data.length);
// Direct DOM manipulation for 60fps performance
levelRef.current.style.width = `${rms * 800}%`;
requestAnimationFrame(updateLevel);
}Key Implementation Details:
- Uses
useReffor direct DOM manipulation (avoids React re-renders) - Time Domain analysis (
getByteTimeDomainData) for vocal frequencies - RMS calculation scaled to 0-100% for visual representation
Hardware Switching State Machine
Device switching requires careful handling of hardware locks:
- Stop existing stream tracks
- Clear stream state (
setStream(null)) - Wait 200ms for hardware release
- Request new device with exact constraint
- Fallback to any device on error
// Exact Fallback Chain pattern
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: selectedCameraId } }
});
} catch {
// Fallback to any available camera
stream = await navigator.mediaDevices.getUserMedia({ video: true });
}Layouts
Meeting Layout
- Central screen share area
- Floating participant bubbles at bottom
- Active speaker highlighting
One-on-One Layout
- 50/50 horizontal split
- Equal-sized video tiles
- Optimized for face-to-face conversations
Table Read Layout
- 60/40 vertical split
- Left panel: Script/PDF placeholder
- Right panel: Scrolling video grid
Rehearsal Layout
- Horizontal video strip (fixed height)
- Remaining space for workspace tools
- Minimal visual footprint
Layout Preview Mode
For development without live Daily sessions:
// MockVideoTile - gradient placeholder instead of live stream
function MockVideoTile({ name }: { name: string }) {
return (
<div className="mock-video-tile">
<div className="gradient-placeholder" />
<span className="participant-name">{name}</span>
</div>
);
}
// MockVideoGrid - generates N mock participants
function MockVideoGrid({ count }: { count: number }) {
const participants = Array.from({ length: count }, (_, i) => ({
id: `mock-${i}`,
name: `Participant ${i + 1}`
}));
// ...
}Toggle preview mode via a local state flag in VideoView.
Permissions
Integration with the RBAC system:
// src/features/video/permissions.ts
export function canStartMeeting(roleId: string): boolean {
return ['owner', 'stage-manager', 'production-manager'].includes(roleId);
}
export function canJoinMeeting(roleId: string): boolean {
// All project members can join
return true;
}Important: Use cloudMember.roleId (string) to look up roles, not a complete role object.
Navigation Integration
Workspace Configuration
// src/features/layout/workspace-config.ts
{
id: 'video',
label: 'Video',
icon: 'videocam',
route: '/video'
}Workspace Visibility
Added to primaryTools for Stage Manager, Director, and Production Manager workspaces for immediate sidebar visibility.
Testing
Store Tests (tests/video/video-store.test.ts)
- Session lifecycle
- Participant updates
- Hardware toggle states
- Privacy defaults (audio starts muted)
Permission Tests (tests/video/video-permissions.test.ts)
- Role-based meeting creation
- Join permissions for all roles
Known Issues
Daily-React Deprecation Warning
The @daily-co/daily-react library may show an atomFamily deprecation warning. This is an internal dependency issue and doesn't affect functionality.
Browser Autoplay Policy
AudioContext may start suspended. It resumes automatically on user interaction (button clicks in lobby).
Infrastructure
Firebase Secrets
firebase functions:secrets:set DAILY_API_KEYCloud Functions
createDailyRoom: Room provisioninggetDailyToken: Participant tokensfetchTranscript: Post-meeting transcript retrieval
See Video API Reference for function signatures.