> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/BintzGavin/helios/llms.txt
> Use this file to discover all available pages before exploring further.

# Timeline and frame control

> Playback controls, seeking, and frame-accurate timeline manipulation

Helios provides precise control over video playback and timeline navigation through a clean, imperative API.

## Core timeline concepts

### Time representations

Helios uses two primary time representations:

* **Frames**: Integer frame numbers (0, 1, 2, ...)
* **Seconds**: Floating-point time values (0.0, 0.033, 0.067, ...)

The relationship is always: `time = frame / fps`

```typescript theme={null}
const helios = new Helios({ duration: 10, fps: 30 });

// Frame 90 = 3 seconds (90 / 30)
helios.seek(90);
console.log(helios.currentTime.value); // 3.0

// 5 seconds = frame 150 (5 * 30)
helios.seekToTime(5);
console.log(helios.currentFrame.value); // 150
```

### Reactive state

All timeline state is exposed as **signals** for reactive programming:

```typescript theme={null}
import { Helios } from '@helios-project/core';

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

// Signals update automatically
helios.currentFrame.subscribe(frame => {
  console.log('Current frame:', frame);
});

helios.currentTime.subscribe(time => {
  console.log('Current time:', time);
});

helios.seek(100); // Triggers both subscriptions
```

See `packages/core/src/Helios.ts:123` for signal definitions.

## Playback controls

### Play and pause

```typescript theme={null}
const helios = new Helios({ duration: 10, fps: 30 });

// Start playback
helios.play();

// Pause playback
helios.pause();

// Check playback state
if (helios.isPlaying.value) {
  console.log('Video is playing');
}
```

Playback implementation at `Helios.ts:885`:

```typescript theme={null}
public play() {
  if (this._isPlaying.peek()) return;
  this._isPlaying.value = true;

  // Sync driver immediately
  this.driver.update((this._currentFrame.peek() / this._fps.value) * 1000, {
    isPlaying: true,
    playbackRate: this._playbackRate.peek(),
    volume: this._volume.peek(),
    muted: this._muted.peek(),
    audioTracks: this._audioTracks.peek()
  });

  this.ticker.start(this.onTick);
}
```

### Seeking

<Tabs>
  <Tab title="Seek by frame">
    ```typescript theme={null}
    // Seek to frame 150
    helios.seek(150);

    // Seek to end
    const totalFrames = helios.duration.value * helios.fps.value;
    helios.seek(totalFrames);

    // Relative seeking
    const current = helios.currentFrame.value;
    helios.seek(current + 30); // Forward 1 second at 30fps
    helios.seek(current - 30); // Back 1 second
    ```

    Frames are automatically clamped to `[0, duration * fps]`.
  </Tab>

  <Tab title="Seek by time">
    ```typescript theme={null}
    // Seek to 5 seconds
    helios.seekToTime(5);

    // Seek to midpoint
    helios.seekToTime(helios.duration.value / 2);

    // Relative seeking
    const current = helios.currentTime.value;
    helios.seekToTime(current + 1.5); // Forward 1.5 seconds
    ```

    Time is converted to frames internally: `frame = seconds * fps`
  </Tab>

  <Tab title="Seek to marker">
    ```typescript theme={null}
    const helios = new Helios({
      duration: 10,
      fps: 30,
      markers: [
        { id: 'intro', time: 0, label: 'Introduction' },
        { id: 'main', time: 3, label: 'Main Content' },
        { id: 'outro', time: 8, label: 'Conclusion' }
      ]
    });

    // Seek to marker by ID
    helios.seekToMarker('main'); // Jumps to 3 seconds
    ```

    See `Helios.ts:809` for marker seeking implementation.
  </Tab>
</Tabs>

### Looping

```typescript theme={null}
const helios = new Helios({
  duration: 10,
  fps: 30,
  loop: true  // Enable looping
});

helios.play();
// Playback will restart from frame 0 when reaching the end

// Toggle looping dynamically
helios.setLoop(false);
```

Loop logic at `Helios.ts:1229`:

```typescript theme={null}
if (shouldLoop && rangeDuration > 0) {
  if (playbackRate > 0 && nextFrame >= endFrame) {
    // Wrap around
    const overflow = nextFrame - startFrame;
    this._currentFrame.value = startFrame + (overflow % rangeDuration);
  }
  // ...
}
```

## Playback rate

Control playback speed:

```typescript theme={null}
const helios = new Helios({ duration: 10, fps: 30 });

// Normal speed
helios.setPlaybackRate(1.0);

// Half speed (slow motion)
helios.setPlaybackRate(0.5);

// Double speed
helios.setPlaybackRate(2.0);

// Reverse playback
helios.setPlaybackRate(-1.0);

// Access current rate
console.log(helios.playbackRate.value);
```

Playback rate affects:

* Frame advancement during `play()`
* Audio/video element playback (via drivers)
* Animation speed (when using time-based animations)

<Note>
  Playback rate does **not** affect seeking. `seek(100)` always jumps to frame 100 regardless of playback rate.
</Note>

## Playback range

Limit playback to a specific frame range:

```typescript theme={null}
const helios = new Helios({
  duration: 10,
  fps: 30,
  playbackRange: [60, 180]  // Frames 60-180 (2-6 seconds)
});

// Or set dynamically
helios.setPlaybackRange(90, 150); // Frames 90-150 (3-5 seconds)

// Clear range to use full duration
helios.clearPlaybackRange();
```

Playback range affects:

* Where `play()` starts (at range start)
* Where playback stops (at range end)
* Loop boundaries (wraps within range)

```typescript theme={null}
const helios = new Helios({
  duration: 10,
  fps: 30,
  loop: true,
  playbackRange: [60, 120]
});

helios.play();
// Starts at frame 60, loops back to 60 when reaching 120
```

See `Helios.ts:832` for playback range API.

## Timeline binding

Helios can be driven by different time sources:

### Standalone playback (default)

```typescript theme={null}
const helios = new Helios({ duration: 10, fps: 30 });

helios.play(); // Drives its own playback loop
```

Uses a ticker (RAF or setTimeout) to advance frames.

### Bound to document timeline

```typescript theme={null}
const helios = new Helios({ duration: 10, fps: 30 });

helios.bindToDocumentTimeline();
// Now reads from document.timeline.currentTime or __HELIOS_VIRTUAL_TIME__
```

**Critical for server-side rendering**: The renderer sets `__HELIOS_VIRTUAL_TIME__` via CDP, and bound instances automatically sync to it.

See `Helios.ts:1085` for binding implementation.

<Info>
  When bound to document timeline, calling `play()` has no effect—time is controlled externally.
</Info>

### Bound to another Helios instance

```typescript theme={null}
const master = new Helios({ duration: 10, fps: 30 });
const slave = new Helios({ duration: 10, fps: 30 });

slave.bindTo(master);
// slave now mirrors master's playback state

master.play();
// Both instances play in sync
```

See `Helios.ts:994` for instance binding.

### Unbinding

```typescript theme={null}
helios.unbind(); // Stop syncing to external time source
helios.unbindFromDocumentTimeline(); // Specific unbind
```

## Frame-accurate rendering

For deterministic rendering, Helios provides a `RenderSession` API:

```typescript theme={null}
import { RenderSession } from '@helios-project/core';

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

const session = new RenderSession(helios, {
  startFrame: 0,
  endFrame: 299  // Render 300 frames (10 seconds at 30fps)
});

for await (const frame of session) {
  console.log('Rendering frame', frame);
  await helios.waitUntilStable();
  // Capture frame here
}
```

See `packages/core/src/render-session.ts:9` for implementation.

### Stability waiting

Before capturing each frame, wait for all async operations to complete:

```typescript theme={null}
helios.seek(150);
await helios.waitUntilStable();
// Now safe to capture—fonts loaded, images decoded, media seeked
```

Stability checks at `Helios.ts:943`:

```typescript theme={null}
public async waitUntilStable(): Promise<void> {
  const driverPromise = this.driver.waitUntilStable();
  const checks = Array.from(this._stabilityChecks);
  const checkPromises = checks.map(check => check());
  
  await Promise.all([driverPromise, ...checkPromises, virtualTimePromise]);
}
```

This waits for:

1. Driver stability (fonts, images, media)
2. Custom registered checks
3. Virtual time sync (if bound to document timeline)

## State subscription

Subscribe to all state changes at once:

```typescript theme={null}
const unsubscribe = helios.subscribe(state => {
  console.log('Current frame:', state.currentFrame);
  console.log('Current time:', state.currentTime);
  console.log('Is playing:', state.isPlaying);
  console.log('Duration:', state.duration);
  console.log('FPS:', state.fps);
  console.log('Active clips:', state.activeClips);
  console.log('Active captions:', state.activeCaptions);
});

// Unsubscribe when done
unsubscribe();
```

Full state type at `Helios.ts:9`:

```typescript theme={null}
export type HeliosState<TInputProps = Record<string, any>> = {
  width: number;
  height: number;
  duration: number;
  fps: number;
  currentFrame: number;
  loop: boolean;
  isPlaying: boolean;
  inputProps: TInputProps;
  playbackRate: number;
  volume: number;
  muted: boolean;
  audioTracks: Record<string, AudioTrackState>;
  availableAudioTracks: AudioTrackMetadata[];
  captions: CaptionCue[];
  activeCaptions: CaptionCue[];
  activeClips: HeliosClip[];
  markers: Marker[];
  playbackRange: [number, number] | null;
  currentTime: number;
};
```

## Dynamic configuration

Most configuration can be changed at runtime:

### Duration and FPS

```typescript theme={null}
const helios = new Helios({ duration: 10, fps: 30 });

// Change duration (preserves current frame if possible)
helios.setDuration(15);

// Change FPS (preserves current time in seconds)
helios.setFps(60);
```

<Note>
  Changing FPS recalculates the current frame to maintain the same time position. For example, if you're at 3 seconds (frame 90 at 30fps), changing to 60fps moves you to frame 180.
</Note>

### Dimensions

```typescript theme={null}
helios.setSize(1280, 720); // Change to 720p
```

Useful for responsive compositions or multi-resolution rendering.

## Advanced timeline patterns

### Scrubbing with momentum

```typescript theme={null}
let isDragging = false;
let velocity = 0;

timeline.addEventListener('pointerdown', () => {
  isDragging = true;
  velocity = 0;
});

timeline.addEventListener('pointermove', (e) => {
  if (!isDragging) return;
  
  const delta = e.movementX;
  velocity = delta * 2; // Pixels to frames
  
  const current = helios.currentFrame.value;
  helios.seek(current + velocity);
});

timeline.addEventListener('pointerup', () => {
  isDragging = false;
  
  // Momentum scrolling
  const decelerate = () => {
    if (Math.abs(velocity) < 0.5) return;
    
    const current = helios.currentFrame.value;
    helios.seek(current + velocity);
    
    velocity *= 0.9; // Friction
    requestAnimationFrame(decelerate);
  };
  
  decelerate();
});
```

### Time remapping

```typescript theme={null}
import { interpolate } from '@helios-project/core';

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

helios.subscribe(({ currentFrame }) => {
  // Slow-motion in middle third
  const remapped = interpolate(
    currentFrame,
    [0, 100, 200, 300],
    [0, 100, 150, 300]  // Slow section: frames 100-200 map to 100-150
  );
  
  // Use remapped time for animations
  element.style.transform = `translateX(${remapped}px)`;
});
```

### Frame-by-frame control

```typescript theme={null}
const helios = new Helios({ duration: 10, fps: 30 });

document.addEventListener('keydown', (e) => {
  const current = helios.currentFrame.value;
  
  if (e.key === 'ArrowRight') {
    helios.seek(current + 1); // Next frame
  } else if (e.key === 'ArrowLeft') {
    helios.seek(current - 1); // Previous frame
  } else if (e.key === 'Space') {
    if (helios.isPlaying.value) {
      helios.pause();
    } else {
      helios.play();
    }
  }
});
```

## Performance considerations

### Seeking performance

Seeking triggers:

1. Signal updates (fast)
2. Driver sync (may trigger layout/paint)
3. Subscriber callbacks (depends on your code)

For high-frequency scrubbing, debounce expensive operations:

```typescript theme={null}
import { debounce } from 'lodash';

const expensiveUpdate = debounce(() => {
  // Heavy DOM manipulation
}, 16); // ~60fps

helios.currentFrame.subscribe(() => {
  expensiveUpdate();
});
```

### Batch updates

Multiple state changes can be batched:

```typescript theme={null}
// Instead of multiple updates:
helios.seek(100);
helios.setPlaybackRate(2);
helios.setLoop(true);

// Consider batching in a single transaction if you implement it,
// or just accept that signals batch automatically
```

Helios signals already batch updates efficiently—subscribers only run once per transaction.

## Next steps

* Learn about the [animation system](/concepts/animations) for motion control
* Explore [drivers](/concepts/drivers) to understand time sources
* See [compositions](/concepts/compositions) for configuration details
