Classroom Playback State Machine in OpenMAIC: How `lib/playback` Manages Live Interaction
The OpenMAIC classroom playback system uses a deterministic PlaybackEngine state machine with four engine modes (idle, playing, paused, live) and three topic states to coordinate seamless transitions between lecture playback and live discussions.
The OpenMAIC project implements a sophisticated state management layer in lib/playback that governs how AI-powered classroom sessions alternate between scripted content and real-time student interaction. This article examines the PlaybackEngine implementation, its state transitions, and the callback-driven architecture that keeps the UI synchronized with engine state.
Core Engine Modes and State Machine Design
The PlaybackEngine class implements a compact state machine defined in [lib/playback/engine.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/engine.ts) (lines 7–23). The engine operates in one of four engine modes, represented by the EngineMode type declared in [lib/playback/types.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/types.ts) at line 18:
| Mode | Purpose |
|---|---|
idle |
Initial state—no playback active, no discussion in progress |
playing |
Lecture playback running with audio, visuals, and timed actions |
paused |
Playback halted, timers and TTS suspended |
live |
Live discussion active—student chat or proactive Q&A session |
The state machine diagram embedded in the source code shows how these modes interconnect:
idle ──► playing ──► paused
▲ ▲ │
│ │ │
│ │ resume() │
│ └──────────┘
│
│ handleEndDiscussion()
│ confirmDiscussion()
│ / handleUserInterrupt()
│ │
│ ▼ pause()
└─────── live ──► paused
▲ │
│ resume / user msg
└────────┘
State Transitions and Trigger Methods
All state transitions are deterministic and invoked through explicit methods on PlaybackEngine. Here are the critical transition paths:
Standard Playback Control
-
start()—idle → playing: Initializes playback from the first scene, begins audio and action scheduling. -
pause()—playing → paused: Halts timers, pauses browser-native TTS, freezes the lecture cursor. -
resume()—paused → playing: Restores timers and audio, continues from the saved cursor position.
Live Discussion Integration
-
confirmDiscussion()—playing → live: Triggered when the user accepts a proactive Q&A prompt. Saves the current lecture cursor, consumes the discussion trigger, and switches to live mode. -
skipDiscussion()—playing → playing: Discards a pending discussion trigger and proceeds with lecture playback. -
handleEndDiscussion()—live → idle: Closes the active discussion, restores the saved lecture state, and returns to idle (ready forresume()to restart playback). -
handleUserInterrupt()—playing/paused → live: Captures an unsolicited student message, immediately saves the interrupted cursor position, aborts playback, and opens live chat.
Error Handling and Cleanup
-
handleDiscussionError()—* → idle: Emergency reset when a live discussion fails (network error, agent unavailability). -
stop()—* → idle: Nuclear option—cancels all work, clears state, returns to initial idle condition.
Topic State Tracking
Separate from engine mode, the system tracks topic state for live discussions using the TopicState type defined in [types.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/types.ts) at line 21:
| State | Meaning |
|---|---|
active |
Discussion currently open and interactive |
pending |
Discussion paused but not closed—can be resumed |
closed |
Discussion terminated, no further interaction possible |
This distinction enables sophisticated behaviors: when pause() is called during live mode, the topic becomes pending rather than closing entirely. Subsequent resume() calls detect this state and return to live rather than playing.
Callback Architecture
The PlaybackEngine accepts a PlaybackEngineCallbacks object that decouples state logic from UI rendering. Key callbacks include:
onModeChange(mode: EngineMode)— Fires on every state transitiononProactiveShow(trigger)— Signals a discussion opportunity is availableonDiscussionConfirmed(topic, prompt, agentId)— Confirms user entered live modeonUserInterrupt()— Indicates spontaneous chat initiationonProgress(snapshot)— Delivers playback progress for progress bars
This design enables deterministic unit testing and clean separation between engine logic and React/Vue components.
Practical Code Examples
Basic Playback Cycle
import { PlaybackEngine } from '@/lib/playback/engine';
import { ActionEngine } from '@/lib/action/engine';
import { AudioPlayer } from '@/lib/utils/audio-player';
const engine = new PlaybackEngine(
scenes, // Array of Stage scenes
new ActionEngine(),
new AudioPlayer(),
{
onModeChange: (mode) => console.log('Engine mode →', mode),
onProgress: (snapshot) => console.log('Progress:', snapshot),
},
);
engine.start(); // idle → playing
engine.pause(); // playing → paused
engine.resume(); // paused → playing
Proactive Q&A Flow
const engine = new PlaybackEngine(scenes, actionEngine, audioPlayer, {
onProactiveShow: (trigger) => {
// Render discussion card in UI
console.log('Proactive question:', trigger.question);
},
onDiscussionConfirmed: (topic, prompt, agentId) => {
console.log('Confirmed discussion on:', topic);
},
});
// User clicks "Join" on the card
engine.confirmDiscussion(); // playing → live
Handling Chat Interruptions
// Student types during lecture playback
engine.handleUserInterrupt('Can you explain the transformer architecture?');
// playing → live (cursor saved automatically)
// Later, end discussion and return to lecture
engine.handleEndDiscussion(); // live → idle
Key Implementation Files
| File | Responsibility |
|---|---|
[lib/playback/engine.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/engine.ts) |
PlaybackEngine class, state machine diagram, all transition methods |
[lib/playback/types.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/types.ts) |
EngineMode, TopicState, PlaybackEngineCallbacks definitions |
[lib/playback/derived-state.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/derived-state.ts) |
Stage state consolidation for UI consumption |
[tests/playback/action-navigation.test.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/tests/playback/action-navigation.test.ts) |
Unit tests verifying navigation and transitions |
Summary
-
The classroom playback state machine centers on a four-mode
PlaybackEnginewith deterministic transitions betweenidle,playing,paused, andlive. -
Live interaction integrates seamlessly through
confirmDiscussion()andhandleUserInterrupt(), both preserving lecture cursor position for clean resumption. -
Topic state (
active/pending/closed) extends the engine mode to handle paused discussions without data loss. -
The callback-driven architecture in
PlaybackEngineCallbacksmaintains strict separation between state logic and UI components. -
All source references originate from
lib/playbackin the THU-MAIC/OpenMAIC repository, with the state machine diagram explicitly documented inengine.tslines 7–23.
Frequently Asked Questions
How does the engine handle unexpected chat messages during playback?
The handleUserInterrupt() method captures unsolicited student input by immediately saving the current lecture cursor, aborting active playback timers and TTS, and transitioning playing or paused → live. The lecture position remains stored for restoration via handleEndDiscussion().
What happens if a user pauses during a live discussion?
Calling pause() while in live mode transitions to paused but leaves the topic in pending state. A subsequent resume() detects this pending status and returns to live mode rather than resuming lecture playback, preserving the discussion context.
Where is the state machine visually documented in the source code?
The complete state machine diagram appears as an ASCII comment block in [lib/playback/engine.ts](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/engine.ts) starting at line 7. This diagram illustrates all valid transitions between idle, playing, paused, and live modes, including the confirmDiscussion() and handleUserInterrupt() entry points into live discussion.
How does the engine recover from failed live discussions?
The handleDiscussionError() method provides a universal reset path from any state back to idle. This handles network failures, agent timeouts, or other live discussion errors by clearing all transient state and returning the engine to a known-good initial condition.
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 →