Add pause/resume (Space, P, and media Play/Pause keys)

Pausing stops the audio line and blocks both playback threads; the A/V
clock is re-anchored on resume so video and audio continue exactly where
they left off without fast-forwarding.
This commit is contained in:
2026-09-15 23:38:20 -04:00
parent d38d7f59aa
commit 7d601dd1fd
@@ -55,6 +55,11 @@ public final class PlayerApp extends Application {
private volatile long clockStartPts = -1;
private volatile long clockStartNanos = -1;
private final Object pauseLock = new Object();
private volatile boolean paused = false;
private volatile long clockOffsetNanos = 0;
private long pauseStartNanos = 0;
@Override
public void start(Stage stage) {
// Gradle's --args splits on whitespace; rejoin so paths with spaces work.
@@ -99,6 +104,14 @@ public final class PlayerApp extends Application {
}
private void handleKey(KeyCode code) {
switch (code) {
case SPACE, P, PLAY, PAUSE -> {
togglePause();
return;
}
default -> { }
}
Controller c = controller;
if (c == null) {
return;
@@ -190,7 +203,8 @@ public final class PlayerApp extends Application {
frame.vPlane(), frame.vStride());
blitVideo(argb, frame.width(), frame.height());
long targetNanos = baseNanos + (frame.pts() - basePts) * 1_000_000_000L / PlaybackEngine.MPEG_TIMESTAMP;
checkPaused();
long targetNanos = baseNanos + clockOffsetNanos + (frame.pts() - basePts) * 1_000_000_000L / PlaybackEngine.MPEG_TIMESTAMP;
sleepUntil(targetNanos);
}
@@ -218,7 +232,8 @@ public final class PlayerApp extends Application {
clockStartPts = chunk.pts();
clockStartNanos = System.nanoTime();
}
long targetNanos = clockStartNanos
checkPaused(line);
long targetNanos = clockStartNanos + clockOffsetNanos
+ (chunk.pts() - clockStartPts) * 1_000_000_000L / PlaybackEngine.MPEG_TIMESTAMP;
sleepUntil(targetNanos);
line.write(chunk.pcm(), 0, chunk.pcm().length);
@@ -267,6 +282,54 @@ public final class PlayerApp extends Application {
}
}
private void togglePause() {
synchronized (pauseLock) {
if (!paused) {
paused = true;
pauseStartNanos = System.nanoTime();
} else {
clockOffsetNanos += System.nanoTime() - pauseStartNanos;
paused = false;
pauseLock.notifyAll();
}
}
}
/** Blocks while playback is paused. */
private void checkPaused() {
synchronized (pauseLock) {
while (paused) {
try {
pauseLock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
/** Blocks while playback is paused, stopping/starting the audio line. */
private void checkPaused(SourceDataLine line) {
synchronized (pauseLock) {
boolean wasPaused = paused;
while (paused) {
if (line != null && line.isRunning()) {
line.stop();
}
try {
pauseLock.wait(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (wasPaused && line != null && !line.isRunning()) {
line.start();
}
}
}
private void blitVideo(int[] argb, int width, int height) {
WritableImage image = new WritableImage(width, height);
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), argb, 0, width);