Timelinx

Playback

PlayheadController, PlaybackEngine, the Clock abstraction, and KeyboardHandler

Playback in Timelinx is split across two classes with a clear separation of concerns:

  • PlayheadController - manages the playhead position. Knows about frame rate, loop regions, and playback rate. Never calls dispatch().
  • PlaybackEngine - coordinates between PlayheadController events and the media pipeline. Requests decoded frames at the current playhead position.

TimelineEngine owns both, wired together via the pipeline constructor option.


PlayheadController

import { PlayheadController } from '@timelinx/core';

const controller = new PlayheadController(
  { durationFrames: toFrame(9000), fps: frameRate(30) },
  browserClock, // optional  - defaults to nodeClock in non-browser
);

Methods

MethodSignatureNotes
play()() => voidStarts advancing the playhead on each clock tick.
pause()() => voidStops advancement without seeking.
stop()() => voidPauses and seeks to frame 0.
seek(frame)(frame: TimelineFrame) => voidJump to a specific frame. Fires a 'seek' event.
setPlaybackRate(rate)(rate: PlaybackRate) => void0.25 | 0.5 | 1.0 | 1.5 | 2.0
setQuality(q)(q: PlaybackQuality) => void'full' | 'draft'
setLoopRegion(r)(r: LoopRegion | null) => void{ inFrame, outFrame }
getState()() => PlayheadStateCurrent playhead state snapshot.
on(listener)(listener: PlayheadListener) => PlayheadUnsubscribeSubscribe to playhead events.

PlayheadState

type PlayheadState = {
  readonly currentFrame: TimelineFrame;
  readonly isPlaying: boolean;
  readonly playbackRate: PlaybackRate;
  readonly quality: PlaybackQuality;
  readonly durationFrames: TimelineFrame;
  readonly fps: FrameRate;
  readonly loopRegion: LoopRegion | null;
  readonly prerollFrames: number;
  readonly postrollFrames: number;
};

PlayheadEvent

type PlayheadEvent = {
  readonly type: PlayheadEventType;  // 'play' | 'pause' | 'seek' | 'frame' | 'end'
  readonly state: PlayheadState;
};

Subscribe:

const unsubscribe = controller.on((event) => {
  if (event.type === 'frame') {
    renderFrame(event.state.currentFrame);
  }
  if (event.type === 'end') {
    controller.seek(toFrame(0));
  }
});

// Later:
unsubscribe();

PlaybackEngine

PlaybackEngine sits between PlayheadController and the media pipeline. On each 'frame' event from the controller, it calls resolveFrame() to find active clips, then calls pipeline.videoDecoder for each, then pipeline.compositor to produce the output frame.

import { PlaybackEngine, browserClock } from '@timelinx/core';

const playback = new PlaybackEngine(
  state,
  pipeline,          // PipelineConfig
  { width: 1920, height: 1080 },
  browserClock,
);

PlaybackEngine Methods

MethodNotes
updateState(state)Call after each accepted dispatch to keep the compositor in sync.
play()Delegates to the internal PlayheadController.
pause()
seek(frame)
on(listener)Subscribe to playback-driven state changes (fired after each composited frame).

TimelineEngine wraps PlaybackEngine and exposes play(), pause(), and setPlayheadFrame() directly. You only interact with PlaybackEngine directly in headless integrations.


The Clock Abstraction

PlayheadController accepts an injected Clock:

type Clock = {
  requestFrame(callback: (timestamp: number) => void): number;
  cancelFrame(id: number): void;
  now(): number;
};
ClockUse case
browserClockrequestAnimationFrame-based. Default for browser apps.
nodeClocksetInterval-based. For server-side use.
createTestClock()Manually controlled. Advance by calling tick(ms).

Test Clock Example

import { PlayheadController, createTestClock, toFrame, frameRate } from '@timelinx/core';

const clock = createTestClock();
const controller = new PlayheadController(
  { durationFrames: toFrame(300), fps: frameRate(30) },
  clock,
);

controller.play();
clock.tick(1000); // advance 1 second
expect(controller.getState().currentFrame).toBe(30); // 30fps × 1s

KeyboardHandler

KeyboardHandler implements J/K/L jog-shuttle and I/O mark-in/out keyboard controls:

import { KeyboardHandler } from '@timelinx/core';

const keyboard = new KeyboardHandler(playbackEngine, {
  onMarkIn: () => {
    engine.dispatch({ ... /* SET_IN_POINT operation */ });
  },
  onMarkOut: () => {
    engine.dispatch({ ... /* SET_OUT_POINT operation */ });
  },
  getTimelineState: () => currentState,
});

// Wire to DOM
document.addEventListener('keydown', (e) => keyboard.handleKeyDown(e));
document.addEventListener('keyup',   (e) => keyboard.handleKeyUp(e));

Default Bindings

KeyAction
JPlay backward (hold: increase reverse speed)
KPause
LPlay forward (hold: increase speed)
SpacePlay / Pause toggle
ISet in-point at current frame
OSet out-point at current frame
Step back 1 frame
Step forward 1 frame
Shift+←Step back 10 frames
Shift+→Step forward 10 frames
HomeSeek to frame 0
EndSeek to last frame

TimelineEngine wires KeyboardHandler internally. In React apps, call engine.handleKeyDown(event, modifiers) from a useEffect event listener rather than instantiating KeyboardHandler directly.

On this page