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

# Start and control playback

> Start a replay for one or more viewers, control its timeline, and clean up correctly.

# Start and control playback

**Runtime:** player · **Entry point:** `ReversePlayer` · **Controls:** Bukkit main thread

Use this guide when your own command, menu, or game UI needs to open a saved recording. The runtime handles loading, viewer state, and the playback scene.

## Start by replay ID

Discover `ReversePlayer` and use `play(replayId, viewer)` or `play(replayId, viewers)`. The runtime loads asynchronously and schedules the actual start on the main thread.

Let's assume `api` is the discovered player provider, `viewer` is the requesting online player, and the replay ID has passed a fresh catalog access check. Inside our JavaPlugin, we can start playback and handle its result:

```java theme={null}
api.play(replayId, viewer).whenComplete((session, failure) -> {
    if (failure != null) {
        getLogger().log(Level.WARNING, "Could not start replay " + replayId, failure);
    } else if (session == null) {
        getLogger().info("No playback session was started");
    } else {
        getLogger().info("Started playback session " + session.getSessionId());
    }
});
```

This callback only logs. If we want to send messages, seek, or change the session speed after completion, we schedule that work back to the main thread and recheck the current viewer/request.

The returned future can:

* Complete with a `PlaybackSession`.
* Complete normally with null if the replay is missing or no viewer can be accepted.
* Complete exceptionally if loading or starting fails.

Handle all three. Completion callbacks have no thread guarantee. For a user-selected recording, authorize with the catalog first: `play` is not itself a catalog actor check. See the [PlaybackIntegration example](https://docs.marioded.com/examples/playback-integration.md).

## Start an already loaded replay

Call `startPlaying(loadedReplay, viewers)` on the main thread. Offline players and duplicates are dropped. A player already in another session is deliberately switched where their previous state can be restored; a failed restoration can cause that viewer to be skipped. If nobody is accepted, the result is null.

The supplied `/reverseplayer play` command applies its own “already watching” guard. Do not infer the more general service behavior only from that command UX.

## Shared sessions

Start with a list to give several people one synchronized viewing session. Its cursor, pause state, speed, and folded world state are shared. Every viewer sees the same timeline position.

If each player needs independent controls, start separate sessions. The recording's immutable event stream can be shared, while mutable session state and entity mappings remain independent.

> [!NOTE]
>
> **One session means one timeline.** Pausing, changing speed, or seeking affects everyone in that session. Start separate sessions when viewers need independent controls.

Membership is chosen at session start. There is no public “join this running session” operation. Viewers leave through the service; do not modify the returned viewer list.

For an already loaded replay, start one shared session on the main thread:

```java theme={null}
PlaybackSession session = api.getPlaybackService().startPlaying(
    loadedReplay, List.of(firstViewer, secondViewer));
```

Check for null before using `session`: neither viewer may have been accepted. To give them independent controls, call `startPlaying` separately for each viewer instead.

## Control a session

```java theme={null}
PlaybackService playback = api.getPlaybackService();
playback.pause(session.getSessionId());
playback.seekTo(session.getSessionId(), 200);
playback.setSpeed(session.getSessionId(), 0.5);
playback.resume(session.getSessionId());
```

Use the Bukkit main thread. Speed must be finite and strictly positive; pause with `pause`, not by setting speed to zero.

All timeline controls take the playback session UUID: `seekTo(sessionId, tick)`,
`seekBy(sessionId, deltaTicks)`, `pause(sessionId)`, `resume(sessionId)`,
`isPaused(sessionId)` and `setSpeed(sessionId, speed)`. Unknown-session controls do nothing;
`isPaused` returns false for an absent session.

`seekTo` uses an absolute session-relative tick. `seekBy` uses a signed relative delta. Tick zero is the start of the event timeline. The service reconstructs scene state and resynchronizes viewers. Setting `session.setCurrentTick` yourself does not do that work.

The API seek does not imply the command's extra “resume after seek” behavior. Decide whether your integration should preserve pause or explicitly resume.

## Leave one viewer or stop everyone

```java theme={null}
playback.leaveSession(session.getSessionId(), viewer); // Restore one viewer; stop if last viewer leaves.
playback.stopPlaying(session.getSessionId());         // End the entire session.
```

Use service operations instead of calling `session.removeViewer`: the service also performs restoration, listener-visible lifecycle, scene cleanup, and final-session handling. Handle disconnections through the runtime rather than retaining offline player objects indefinitely.

## Read session information

* `getReplayId()` identifies the recording; `getSessionId()` identifies this viewing instance.
* `getCurrentTick()` and `getLastTick()` describe the playable timeline.
* `getRecordedDurationMillis()` is a separate wall-clock recording duration.
* `getViewers()` returns an immutable membership snapshot.
* `getWorldState()` exposes the accumulated durable scene; treat it as runtime-owned.
* `getRecordablesAt(tick)` gives events for the requested tick, or an empty list.

Use `ReversePlayer.getSession(viewerId)` (an `Optional`) or `PlaybackService.getSessionByViewer(viewerId)` (nullable) to find a viewer's current session. `ReversePlayer.isWatching(viewerId)` and `PlaybackService.isViewerPlaying(viewerId)` test viewer membership. In contrast, `PlaybackService.getSession(sessionId)` resolves a playback session UUID and returns null if absent. These lookups require the main thread. A cached session reference is not proof that it is still active after an asynchronous operation.

For example, a main-thread pause button can look up the current session at click time:

```java theme={null}
api.getSession(viewer.getUniqueId()).ifPresent(session -> {
    api.getPlaybackService().pause(session.getSessionId());
});
```

## Entity IDs are session-specific

Recorded entity IDs cannot be sent directly back to every client. Different playback sessions may contain the same original ID, and real server entities also have IDs. Render through `PlaybackContext` and its session entity ID/UUID mappings.

Do not retain a remapped ID indefinitely across resynchronization or use one session's mapping for another session. Let entity-aware renderers and lifecycle operations maintain those mappings.

## Events and custom display

Subscribe to [playback events](https://docs.marioded.com/api/events.md) to respond to session lifecycle. Use the [layout/menu APIs](https://docs.marioded.com/api/ui-extensions.md) to add controls without replacing the player's inventory management. `playTick(PlaybackSession)` is runtime scheduling machinery and still takes the actual session object; an integration should not advance a session separately while Reverse's own tick task is already doing so. `handleViewerQuit(Player)` likewise retains a player handle for disconnect cleanup, and `leaveSession(sessionId, Player)` needs it for restoration. The UUID controls do not change these helper contracts.

The runtime owns its sidebar lifecycle too. Its implementation-level `ScoreboardService.clear(sessionId)` uses the **playback session UUID**, while layout defaults and `clearViewerState(viewerId)` use **viewer UUIDs**. Scoreboard internals are not exposed by `ReversePlayer`; ordinary integrations should stop/leave through the playback service and let it clean up displays.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.