Timecode implementation
Technical specification for pluggable timecode acquisition system.
Overview
This system provides accurate SMPTE timecode for EDL generation by supporting multiple timecode sources with automatic failover. The primary use case is reading RP-188 timecode embedded in SDI signals via a HyperDeck proxy.
Architecture
┌─────────────────────────────────────────────────────────────────┐│ Application ││ │ ││ TimecodeManager ││ ┌────────┴────────┐ ││ │ │ ││ Primary Provider Fallback Provider ││ │ │ ││ ┌──────────────┼──────────────┐ │ ││ │ │ │ │ ││ ▼ ▼ ▼ ▼ ││ HyperDeck HyperDeck System Clock ││ SDI Internal Provider ││ │ ││ ┌─────────┴─────────┐ ││ │ │ ││ ▼ ▼ ││ TCP Client Polling ││ (notify mode) (fallback) │└─────────────────────────────────────────────────────────────────┘Data Flow
1. HyperDeck receives SDI with RP-188 from ATEM2. HyperDeck extracts embedded timecode3. Our TCP client queries transport info4. Response parsed into TimecodeSnapshot5. Source validated (SDI vs Internal)6. Snapshot emitted to consumers7. EDL generator uses snapshot for event timestampsComponents
1. TimecodeSnapshot (Data Model)
The unified output from all providers:
interface TimecodeSnapshot { readAt: number; // Date.now() when captured timecode: string | null; // "HH:MM:SS:FF" display TC timelineTimecode: string | null; source: TimecodeSource; // 'HYPERDECK_SDI' | 'SYSTEM' | etc. status: TimecodeStatus; // 'OK' | 'DEGRADED' | 'NO_SIGNAL' | 'ERROR' frameRate: number; dropFrame: boolean; transport?: { state, speed, slotId, clipId }; device?: { name, model, firmwareVersion }; error?: string;}2. HyperDeckProvider
Primary provider for “real” external timecode.
Connection Strategy:
- Connect via TCP to port 9993
- Query device info and protocol version
- Query timecode source configuration
- If protocol >= 1.11: enable notify for transport
- Else: start polling at configured rate
Timecode Source Validation:
// Only report OK status if source matches expectationif (config.requireSdiSource && timecodeSource !== 'external') { snapshot.status = 'DEGRADED'; snapshot.source = 'HYPERDECK_INTERNAL';} else if (timecodeSource === 'external') { snapshot.status = 'OK'; snapshot.source = 'HYPERDECK_SDI';}Error Handling:
- Connection timeout: retry with exponential backoff
- Protocol error: log and continue polling
- No signal: report NO_SIGNAL status
- Disconnect: attempt reconnection
HyperDeck Protocol Commands Used:
device info → Get model, firmware, protocol versiontransport info → Get timecode, displayTimecode, status, speednotify: transport: true → Subscribe to transport changes (1.11+)3. SystemClockProvider
Fallback provider using computer time.
Implementation:
- Calculate frame number from system time
- Apply configured start timecode offset
- Format as SMPTE timecode string
- Emit at configured rate (default: frame rate)
Drift Compensation:
- Use
performance.now()for sub-millisecond precision - Track cumulative drift and compensate
- Log warnings if drift exceeds threshold
4. TimecodeManager
Orchestrates providers and handles failover.
Responsibilities:
- Instantiate configured providers
- Connect primary, then fallback
- Monitor primary health
- Switch to fallback after timeout
- Attempt primary reconnection periodically
- Rate-limit emissions to consumers
- Provide unified API
Failover Logic:
if (primaryDisconnectedFor > fallbackDelayMs) { activeProvider = fallbackProvider; emit('failover', { from: 'primary', to: 'fallback' });}
// Periodically attempt to restore primarysetInterval(() => { if (activeProvider === fallbackProvider) { primaryProvider.connect().then(() => { activeProvider = primaryProvider; emit('restored', { provider: 'primary' }); }).catch(() => { /* stay on fallback */ }); }}, primaryRestoreIntervalMs);Configuration Schema
timecode: # Primary source: 'system' | 'hyperdeck' source: hyperdeck
# Frame rate for timecode generation/validation frameRate: 25
# Drop-frame (only for 29.97/59.94) dropFrame: false
# Starting timecode (for system clock mode) startTimecode: "01:00:00:00"
# HyperDeck configuration hyperdeck: host: "10.7.77.21" port: 9993
# Only accept SDI-embedded timecode as "real" requireSdiSource: true
# Polling rate when notifications unavailable pollRateHz: 10
# Use protocol notifications if available useNotifications: true
# Connection timeout connectionTimeoutMs: 5000
# Reconnection settings reconnect: enabled: true maxAttempts: 0 # 0 = infinite initialDelayMs: 1000 maxDelayMs: 30000
# Fallback settings fallback: enabled: true delayMs: 3000 # Switch after 3s of primary failure
# Rate limiting maxEmitRateHz: 25File Structure
src/providers/timecode/├── types.ts # All interfaces and types├── hyperdeck.ts # HyperDeck TCP provider├── system-clock.ts # System clock fallback├── manager.ts # Provider orchestration├── utils.ts # Timecode parsing/formatting└── index.ts # Public exports
tools/└── hyperdeck-tc.ts # CLI test toolImplementation Phases
Phase 1: Types & Utils
- types.ts — All interfaces
- utils.ts — Timecode parsing, formatting, validation
Phase 2: HyperDeck Provider
- TCP connection via hyperdeck-connection
- Protocol version detection
- Notify subscription (1.11+)
- Polling fallback
- Timecode source validation
- Reconnection logic
Phase 3: System Clock Provider
- Time-of-day to timecode conversion
- Configurable start offset
- Frame-accurate update loop
- Drift monitoring
Phase 4: Manager & Integration
- Provider orchestration
- Failover logic
- Rate limiting
- Config schema updates
- App integration
Phase 5: CLI Tool & Testing
- tools/hyperdeck-tc.ts
- Unit tests for utils
- Integration tests with mock
Error States
| Status | Meaning | Action |
|---|---|---|
| OK | Valid TC from expected source | Use timecode |
| DEGRADED | TC available but wrong source | Use with warning |
| NO_SIGNAL | No valid TC signal | Fall back |
| CONNECTING | Establishing connection | Wait |
| DISCONNECTED | Lost connection | Reconnect |
| ERROR | Unrecoverable error | Fall back + alert |
Testing Strategy
Unit Tests
- Timecode parsing edge cases
- Frame rate calculations
- Drop-frame handling
Integration Tests
- Mock HyperDeck responses
- Failover scenarios
- Reconnection behaviour
Manual Testing
- Real HyperDeck with SDI input
- Verify TC matches device display
- Test disconnect/reconnect
- Verify fallback activation
Dependencies
{ "hyperdeck-connection": "^0.5.0"}