Skip to main content

TimeDriver

The TimeDriver interface defines the contract for time synchronization drivers. Drivers manage media elements, animations, and ensure frame-accurate playback.
interface TimeDriver {
  init(scope: unknown): void;
  update(timeInMs: number, options?: {
    isPlaying: boolean;
    playbackRate: number;
    volume?: number;
    muted?: boolean;
    audioTracks?: Record<string, { volume: number; muted: boolean }>;
  }): void;
  waitUntilStable(): Promise<void>;
  dispose?(): void;
  subscribeToMetadata?(callback: (meta: DriverMetadata) => void): () => void;
  getAudioContext?(): Promise<unknown>;
  getAudioSourceNode?(trackId: string): Promise<unknown>;
}

Required methods

init
(scope: unknown) => void
Initialize the driver with an animation scope. For browser drivers, this is typically the document object.
update
(timeInMs: number, options?) => void
Update the driver to a specific time in milliseconds with playback options.Parameters:
  • timeInMs: Target time in milliseconds
  • options.isPlaying: Whether playback is active
  • options.playbackRate: Speed multiplier
  • options.volume: Master volume (0.0 to 1.0)
  • options.muted: Master mute state
  • options.audioTracks: Per-track audio state
waitUntilStable
() => Promise<void>
Returns a promise that resolves when all asynchronous operations (media seeking, image loading, etc.) are complete. Critical for deterministic rendering.

Optional methods

dispose
() => void
Clean up resources when the driver is no longer needed.
subscribeToMetadata
(callback: (meta: DriverMetadata) => void) => () => void
Subscribe to metadata updates (e.g., discovered audio tracks). Returns an unsubscribe function.
getAudioContext
() => Promise<unknown>
Get the Web Audio API AudioContext for custom audio processing.
getAudioSourceNode
(trackId: string) => Promise<unknown>
Get a MediaElementAudioSourceNode for a specific audio track, useful for visualization.

DriverMetadata

Metadata reported by drivers about available resources.
interface DriverMetadata {
  audioTracks?: AudioTrackMetadata[];
}
audioTracks
AudioTrackMetadata[]
Audio tracks discovered by the driver from the DOM.

AudioTrackMetadata

Detailed metadata for an audio track.
interface AudioTrackMetadata {
  id: string;
  src: string;
  startTime: number;
  duration: number;
  fadeInDuration?: number;
  fadeOutDuration?: number;
  fadeEasing?: string;
}
id
string
Unique track identifier.
src
string
Audio source URL or path.
startTime
number
Track start time in the composition timeline.
duration
number
Track duration in seconds.
fadeInDuration
number
Optional fade-in duration in seconds.
fadeOutDuration
number
Optional fade-out duration in seconds.
fadeEasing
string
Optional easing function name for fade transitions.

Ticker

The Ticker interface defines the contract for playback loop implementations.
interface Ticker {
  start(callback: TickCallback): void;
  stop(): void;
}
start
(callback: TickCallback) => void
Start the ticker loop, calling the callback on each tick with the delta time since the last tick.
stop
() => void
Stop the ticker loop.

TickCallback

Callback type for ticker implementations.
type TickCallback = (deltaTime: number) => void;
deltaTime
number
Time elapsed since the last tick in milliseconds.

Built-in implementations

Helios provides several built-in driver and ticker implementations:

Drivers

  • DomDriver: Synchronizes WAAPI animations and media elements with Helios playback
  • NoopDriver: Minimal driver with no synchronization (default when autoSyncAnimations is false)

Tickers

  • RafTicker: Uses requestAnimationFrame for smooth browser-based playback (default in browsers)
  • TimeoutTicker: Uses setTimeout for Node.js environments (default in Node.js)
  • ManualTicker: Manual tick control for testing or custom playback loops

Usage examples

Custom time driver

import { TimeDriver, Helios } from '@helios/core';

class CustomDriver implements TimeDriver {
  private mediaElements: HTMLMediaElement[] = [];

  init(scope: unknown): void {
    if (scope instanceof Document) {
      this.mediaElements = Array.from(
        scope.querySelectorAll('audio, video')
      );
    }
  }

  update(timeInMs: number, options?: {
    isPlaying: boolean;
    playbackRate: number;
    volume?: number;
    muted?: boolean;
  }): void {
    const timeSec = timeInMs / 1000;

    this.mediaElements.forEach(el => {
      el.currentTime = timeSec;
      el.playbackRate = options?.playbackRate ?? 1;
      
      if (options?.volume !== undefined) {
        el.volume = options.volume;
      }
      
      if (options?.muted !== undefined) {
        el.muted = options.muted;
      }

      if (options?.isPlaying) {
        el.play();
      } else {
        el.pause();
      }
    });
  }

  async waitUntilStable(): Promise<void> {
    // Wait for all media to finish seeking
    await Promise.all(
      this.mediaElements.map(el => new Promise<void>(resolve => {
        if (el.seeking) {
          el.addEventListener('seeked', () => resolve(), { once: true });
        } else {
          resolve();
        }
      }))
    );
  }

  dispose(): void {
    this.mediaElements = [];
  }
}

// Use the custom driver
const helios = new Helios({
  duration: 10,
  fps: 30,
  driver: new CustomDriver()
});

Custom ticker

import { Ticker, TickCallback, Helios } from '@helios/core';

class CustomTicker implements Ticker {
  private intervalId: number | null = null;
  private lastTime = 0;

  start(callback: TickCallback): void {
    this.lastTime = Date.now();
    
    this.intervalId = setInterval(() => {
      const now = Date.now();
      const delta = now - this.lastTime;
      this.lastTime = now;
      
      callback(delta);
    }, 1000 / 60) as unknown as number; // 60 FPS
  }

  stop(): void {
    if (this.intervalId !== null) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }
}

// Use the custom ticker
const helios = new Helios({
  duration: 10,
  fps: 30,
  ticker: new CustomTicker()
});

Accessing audio context

import { Helios } from '@helios/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  autoSyncAnimations: true // Uses DomDriver
});

// Get the audio context for custom processing
const audioContext = await helios.getAudioContext();

if (audioContext instanceof AudioContext) {
  // Create an analyser for visualization
  const analyser = audioContext.createAnalyser();
  
  // Get source node for a specific track
  const sourceNode = await helios.getAudioSourceNode('background-music');
  
  if (sourceNode instanceof MediaElementAudioSourceNode) {
    sourceNode.connect(analyser);
    analyser.connect(audioContext.destination);
    
    // Now you can visualize audio
    const dataArray = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(dataArray);
  }
}

Subscribing to metadata

import { Helios } from '@helios/core';

const helios = new Helios({
  duration: 10,
  fps: 30,
  autoSyncAnimations: true
});

// Subscribe to available audio tracks
const unsubscribe = helios.availableAudioTracks.subscribe(tracks => {
  console.log('Available audio tracks:', tracks);
  
  tracks.forEach(track => {
    console.log(`Track ${track.id}:`, track.src);
    console.log(`  Duration: ${track.duration}s`);
    console.log(`  Start: ${track.startTime}s`);
  });
});

// Clean up when done
unsubscribe();