What Is the Role of the Playback Engine in OpenMAIC? A Deep Dive Into the Core Orchestrator
The Playback Engine in OpenMAIC is a state-machine-driven orchestrator that unifies automated lecture playback and live discussion modes, managing audio/TTS, user interruptions, and state persistence across idle, playing, paused, and live states.
The Playback Engine sits at the heart of the OpenMAIC educational platform, bridging static lecture content with dynamic, user-driven interactions. Whether delivering pre-recorded lessons via text-to-speech or enabling real-time discussions through ProactiveCards, this component ensures seamless transitions without losing synchronization. Below is a comprehensive breakdown of its architecture, responsibilities, and API based on the source code in THU-MAIC/OpenMAIC.
Playback Engine Modes and State Machine
The Playback Engine implements a finite state machine with four distinct modes defined in lib/playback/engine.ts lines 9-22:
idle— No active playback; the engine awaits initializationplaying— Automated lecture delivery with audio/TTS executionpaused— Temporary halt preserving all timers and synthesis statelive— Interactive discussion mode triggered by user engagement
Mode transitions are strictly controlled. For example, receiving a user message during playing triggers a save of the lecture cursor, audio termination, and a switch to live engine.ts#L138-L166.
Core Responsibilities of the Playback Engine
Direct Action Execution Without Compilation
Unlike systems that compile scenes into an intermediate format, the Playback Engine consumes Scene.actions[] directly through its ActionEngine dependency engine.ts#L4-L5. This design eliminates preprocessing overhead and allows dynamic modifications to lecture content.
Cursor Management and Snapshot Persistence
The engine maintains precise playback position through scene and action indices. Two critical methods enable state durability:
getSnapshot()— SerializessceneIndex,actionIndex, consumed discussion IDs, andsceneIdrestoreFromSnapshot()— Reconstructs engine state from a saved snapshot
These are implemented in engine.ts#L32-L40, supporting features like progress recovery across browser sessions.
Audio and TTS Coordination
The Playback Engine handles three audio delivery strategies engine.ts#L86-L106:
- Pre-generated audio files — Played via
AudioPlayer - Browser-native TTS — Using the Web Speech API with voice caching
- Timed reading — Chunked synthesis for long text segments to prevent browser limitations
Voice list caching and chunked synthesis utilities appear in engine.ts#L150-L190, ensuring cross-browser compatibility for Chrome and Firefox.
Discussion Trigger Handling
Proactive discussions are surfaced through a delayed callback mechanism. When a discussion action becomes active, the engine waits 3 seconds then invokes onProactiveShow engine.ts#L98-L114. The markDiscussionConsumed method engine.ts#L48-L52 prevents re-triggering already-handled discussions.
User Interrupt Management
The handleUserInterrupt() method implements the full interruption protocol:
engine.handleUserInterrupt('What is the definition of AI?');
This executes engine.ts#L138-L166:
- Saves current lecture cursor to
lectureCursorBeforeInterrupt - Stops all audio/TTS immediately
- Switches mode to
live - Triggers
onUserInterruptcallback for chat UI integration
When discussion ends, handleEndDiscussion resets the cursor and returns to idle engine.ts#L99-L114.
Public API and Lifecycle Callbacks
The Playback Engine exposes a clean control surface engine.ts#L49-L73 that shields UI layers from internal complexity:
| Method | Purpose |
|---|---|
start() |
Begin playback from first scene/action |
continuePlayback() |
Resume after interruption using saved cursor |
pause() / resume() |
Temporary halt with full state preservation |
stop() |
Terminate playback and reset to idle |
jumpToAction(index, options) |
Navigate to specific action with optional autoplay |
All state changes emit through constructor callbacks defined in types.ts lines 31-65:
onModeChange— Engine mode transitionsonSceneChange/onActionChange— Content progressiononSpeechStart/onSpeechEnd— TTS lifecycleonEffectFire— Action effect executiononProgress— General progress updates
Safe Navigation and Jump Validation
The engine prevents illegal navigation through canJumpToAction engine.ts#L74-L80, which verifies that target actions lie within a reconstructable prefix. This ensures that jumping to arbitrary positions won't break lecture continuity or leave the engine in an inconsistent state.
The underlying logic resides in lib/playback/action-navigation.ts, with canJumpWithinReconstructablePrefix providing the mathematical guarantee.
Practical Code Examples
Engine Instantiation
import { PlaybackEngine } from '@/lib/playback/engine';
import { ActionEngine } from '@/lib/action/engine';
import { AudioPlayer } from '@/lib/utils/audio-player';
const engine = new PlaybackEngine(
scenes, // Scene[] array from lecture content
new ActionEngine(),
new AudioPlayer(),
{
onModeChange: (mode) => console.log('Mode →', mode),
onProgress: (snap) => saveProgress(snap),
onProactiveShow: (trigger) => showDiscussionCard(trigger),
onSpeechStart: (text) => console.log('Speaking:', text),
onUserInterrupt: (msg) => openChatInterface(msg),
},
);
Basic Playback Control
// Start fresh lecture
engine.start();
// Pause and resume
engine.pause();
engine.resume();
// Continue after discussion ends
engine.continuePlayback();
State Persistence Pattern
// Save before page unload
window.addEventListener('beforeunload', () => {
localStorage.setItem('playbackSnap', JSON.stringify(engine.getSnapshot()));
});
// Restore on application mount
const saved = localStorage.getItem('playbackSnap');
if (saved) {
engine.restoreFromSnapshot(JSON.parse(saved));
}
Precision Navigation
// Jump to 5th action in first scene and auto-play
engine.jumpToAction(5, { autoplay: true });
// Check validity before attempting jump
if (engine.canJumpToAction(12)) {
engine.jumpToAction(12);
}
Key Source Files in the Playback Module
| File | Purpose | Critical Exports |
|---|---|---|
lib/playback/engine.ts |
Core state machine and public API | PlaybackEngine class |
lib/playback/types.ts |
Type definitions | PlaybackSnapshot, EngineMode, PlaybackEngineCallbacks |
lib/playback/cursor.ts |
Action resolution utilities | resolvePlaybackCursor |
lib/playback/action-navigation.ts |
Safe jump validation | canJumpWithinReconstructablePrefix |
lib/playback/auto-resume.ts |
Post-discussion resumption logic | Auto-resume determination |
lib/playback/index.ts |
Module exports | Barrel re-exports |
Summary
- The Playback Engine orchestrates all lecture delivery and discussion flow in OpenMAIC through a rigorously defined four-mode state machine (
idle,playing,paused,live) - It executes actions directly without intermediate compilation, reducing latency and enabling dynamic content
- Cursor persistence via
getSnapshot()andrestoreFromSnapshot()enables seamless progress recovery across sessions - User interruption handling preserves exact lecture position, switches to discussion mode, and supports automatic resumption
- The public API (
start,pause,resume,jumpToAction, etc.) provides UI-agnostic control while lifecycle callbacks enable reactive application architecture
Frequently Asked Questions
What triggers the Playback Engine to switch to live mode?
A user-generated message or interaction triggers handleUserInterrupt(), which saves the current lecture cursor, stops all audio/TTS, and transitions the engine to live mode engine.ts#L138-L166. This ensures no content is lost when the user initiates discussion.
How does the Playback Engine handle long text that exceeds browser TTS limits?
The engine implements chunked speech synthesis engine.ts#L150-L190, breaking long text into browser-manageable segments while maintaining the illusion of continuous speech. It also caches the available voice list to avoid repeated permission prompts.
Can users jump to arbitrary positions in a lecture?
Only within validated boundaries. The jumpToAction() method checks canJumpToAction() engine.ts#L74-L80, which ensures targets lie within a reconstructable prefix—actions whose dependencies have all been satisfied. This prevents navigation to states that would break lecture logic.
What happens to playback progress if the browser crashes?
The getSnapshot() method produces a fully serializable state including scene index, action index, consumed discussion IDs, and scene identifier engine.ts#L32-L40. Applications can persist this to localStorage or a backend, then restore via restoreFromSnapshot() on reinitialization.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →