How the Playback Engine Manages the Classroom Lifecycle in OpenMAIC
The Playback Engine in OpenMAIC orchestrates the classroom lifecycle through a finite state machine with four distinct modes—idle, playing, paused, and live—transitioning between lecture playback and interactive discussions via methods like start(), handleUserInterrupt(), and confirmDiscussion().
OpenMAIC’s Playback Engine is the core state-machine that governs every phase of a classroom session, from initial lecture playback through live student discussions to final cleanup. Unlike complex orchestration layers that compile intermediate representations, this engine directly consumes the Scene.actions[] array and reacts immediately to user-initiated events. Understanding how this engine manages the classroom lifecycle is essential for developers extending OpenMAIC’s educational platform or debugging session continuity issues.
Core State Machine Architecture
The engine operates through four mutually exclusive modes defined in lib/playback/types.ts. Each mode represents a distinct phase of the classroom lifecycle, with strict transitions ensuring predictable behavior during lecture playback and discussion interruptions.
The Four Engine Modes
- idle — No playback activity. The engine awaits a new lecture start, holding no active timers or audio resources.
- playing — Auto-advancing through speech, spotlights, whiteboard actions, and other scripted scene elements.
- paused — Temporary halt state preserving playback position, used when users press pause or when ProactiveCards appear.
- live — Interactive discussion state with an active SSE stream processing user chat messages in real-time.
State Transition Entry and Exit Points
The state diagram sketched in lib/playback/engine.ts【/cache/repos/github.com/THU-MAIC/OpenMAIC/main/lib/playback/engine.ts#L7-L24】 defines specific methods that trigger transitions:
idle→playingoccurs viastart()orcontinuePlayback()playing→pausedoccurs viapause()playing→liveoccurs viahandleUserInterrupt()(user chat) orconfirmDiscussion()(ProactiveCard join)live→idleoccurs viahandleEndDiscussion()orhandleDiscussionError()- Any mode →
idlecan be forced viastop()
Key Lifecycle Hooks
The engine exposes explicit methods for each lifecycle phase, allowing the UI layer to control session flow while the engine manages internal state consistency.
Starting and Resuming Sessions
The start() method resets indices, clears stale generation caches, switches the mode to playing, and immediately invokes processNext() to begin action execution. For resuming after a discussion, continuePlayback() preserves the current cursor position rather than resetting it, ensuring seamless return to lecture content.
import { PlaybackEngine } from '@/lib/playback/engine';
import { ActionEngine } from '@/lib/action/engine';
import { AudioPlayer } from '@/lib/utils/audio-player';
const scenes = loadLessonScenes();
const engine = new PlaybackEngine(
scenes,
new ActionEngine(),
new AudioPlayer(),
{
onModeChange: (mode) => console.log('Engine mode →', mode),
onProgress: (snapshot) => saveSnapshot(snapshot),
onProactiveShow: (trigger) => showProactiveCard(trigger),
onDiscussionConfirmed: (q, p, a) => startLiveDiscussion(q, p, a),
},
);
// Initiate classroom session
engine.start();
Handling User Interruptions
When a student sends a chat message during playback, handleUserInterrupt(text) saves the current lecture cursor, stops ongoing audio/TTS, and transitions the engine to live mode. This method is the primary entry point for converting passive lecture viewing into active discussion.
function sendMessage(text: string) {
// Engine saves cursor and switches to live mode
playbackEngine.handleUserInterrupt(text);
sendChatToBackend(text);
}
Managing Discussion Flow
The engine provides three distinct methods for handling ProactiveCard interactions:
confirmDiscussion()— Consumes the discussion, saves the lecture cursor tosavedSceneIndex/ActionIndex, switches to live mode, and notifies UI callbacks. Called when users click "Join" on a discussion prompt.skipDiscussion()— Marks the discussion as consumed without entering live mode, immediately continuing playback.handleEndDiscussion()— Restores the saved lecture position, closes the whiteboard, firesonDiscussionEnd, and returns to idle when users finish a discussion.
function onJoinDiscussion() {
playbackEngine.confirmDiscussion(); // Moves to live mode
}
function onSkipDiscussion() {
playbackEngine.skipDiscussion(); // Continues lecture
}
Emergency Stopping and Cleanup
The stop() method forces the engine to idle, halting all audio/TTS, clearing effects, and resetting indices. This is invoked when users navigate away from the classroom or when the application tears down.
Integration with the Classroom UI
The engine exposes callbacks (onModeChange, onProgress, onProactiveShow) that allow the workbench UI layer to react instantly to state changes without polling.
Pro Playback Exit Handling
When users exit full-screen Pro playback mode, exitProPlaybackToStandalone in lib/workbench/pro-playback-exit.ts【/cache/repos/github.com/THU-MAIC/OpenMAIC/main/lib/workbench/pro-playback-exit.ts#L19-L34】 tears down the playback context and ensures the classroom UI re-mounts with a clean state.
import { exitProPlaybackToStandalone } from '@/lib/workbench/pro-playback-exit';
function leaveProPlayback(stageId: string) {
exitProPlaybackToStandalone({
stageId,
teardown: () => playbackEngine.stop(),
setMode: (mode) => playbackEngine.setMode(mode as any),
replace: (url) => router.replace(url),
});
}
Header Controls and Discussion UI
The classroom header controls in lib/workbench/classroom-header-controls.tsx invoke engine.pause() and engine.resume() based on play/pause button interactions. Meanwhile, the ProactiveCard component drives the discussion lifecycle by calling engine.confirmDiscussion() or engine.skipDiscussion(), creating a seamless "lecture → discussion → lecture" flow without position loss or visual flashes.
Persistence and Recovery
The engine supports classroom lifecycle persistence through snapshots. The getSnapshot() method exports the current sceneIndex, actionIndex, and consumed discussion IDs, which the persistence layer stores in IndexedDB. After page reloads or navigation, restoreFromSnapshot() reinjects these indices and the consumed set, allowing the engine to resume exactly where the student left off.
Timing and TTS Management
During the playing and paused modes, the engine manages audio timing through two mechanisms:
- Pre-generated audio uses
audioPlayer.play()with exact duration tracking. - Browser TTS uses the Web Speech API with
estimateSpeechDurationMsas a fallback timer.
When pause() fires, the engine saves speechTimerRemaining and, for browser TTS, stores unfinished chunks in browserTTSPausedChunks. The resume() method restores these timers and re-schedules speech, ensuring that pausing never skips content regardless of audio source.
Summary
- The Playback Engine manages the OpenMAIC classroom lifecycle through four strict modes: idle, playing, paused, and live.
- State transitions are triggered by specific methods—
start(),handleUserInterrupt(),confirmDiscussion(), andhandleEndDiscussion()—that preserve lecture position and manage audio resources. - The engine directly consumes
Scene.actions[]without intermediate compilation, making it lightweight and predictable. - Persistence is achieved through
getSnapshot()andrestoreFromSnapshot(), enabling session recovery across page reloads. - UI integration occurs through callbacks that update
classroom-header-controls.tsxand handle Pro playback exits viapro-playback-exit.ts.
Frequently Asked Questions
What triggers the transition from lecture playback to live discussion mode?
The engine transitions from playing to live mode via two paths: handleUserInterrupt(text) when a user sends a chat message during a lecture, or confirmDiscussion() when the user clicks "Join" on a ProactiveCard. Both methods save the current lecture cursor position before switching modes, ensuring the lecture can resume at the exact action where it was interrupted.
How does the Playback Engine handle classroom session recovery after a browser refresh?
The engine exports a snapshot containing sceneIndex, actionIndex, and consumed discussion IDs via getSnapshot(). The application layer stores this in IndexedDB, and upon reload calls restoreFromSnapshot() to reinject these values. This mechanism allows the engine to resume the classroom lifecycle at the precise action and mode state that existed before the refresh.
What is the difference between stop() and pause() in the Playback Engine?
pause() transitions the engine to paused mode while preserving the current sceneIndex and actionIndex, saving remaining speech timers and TTS chunks for later resumption. stop() forces an immediate return to idle, clearing all timers, resetting indices, and halting audio without preserving playback position—effectively ending the current classroom session.
How does the engine manage audio timing when users pause a TTS lecture?
When pause() executes during browser TTS playback, the engine stores the remaining duration in speechTimerRemaining and saves unfinished utterance chunks in browserTTSPausedChunks. Upon resume(), it restores these values and re-schedules the speech timer, ensuring that pausing a lecture never results in skipped content or lost audio synchronization.
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 →