package org.openautonomousconnection.luascript.fx;
import javafx.application.Platform;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import org.openautonomousconnection.luascript.hosts.AudioHost;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
/**
* JavaFX MediaPlayer based AudioHost.
*
*
Note: JavaFX Media does not reliably use {@link java.net.URLStreamHandler} for custom schemes.
* Therefore, for {@code web://} this host resolves via {@link URLConnection} (your installed handler),
* spools to a temporary file and plays that file via {@code file://}.
*/
public final class FxAudioHost implements AudioHost {
private static final Set AUDIO_EXTENSIONS = Set.of(
"mp3", "wav", "ogg", "m4a", "opus", "web"
);
private volatile MediaPlayer player;
private volatile boolean loop;
private volatile double volume = 1.0;
private volatile Path lastTempFile;
@Override
public void playFile(File file) {
Objects.requireNonNull(file, "file");
if (!file.isFile()) {
throw new IllegalArgumentException("Audio file not found: " + file.getAbsolutePath());
}
String ext = MediaExtensions.extensionOf(file.getName());
ensureAllowedAudioExtension(ext, file.getName());
playUri(file.toURI());
}
@Override
public void playUrl(String url) {
Objects.requireNonNull(url, "url");
String u = url.trim();
if (u.isEmpty()) throw new IllegalArgumentException("URL is empty");
URI uri = URI.create(u);
String scheme = (uri.getScheme() == null) ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
String ext = MediaExtensions.extensionOf(uri.getPath());
ensureAllowedAudioExtension(ext, uri.getPath());
if ("http".equals(scheme) || "https".equals(scheme) || "file".equals(scheme)) {
playUri(uri);
return;
}
if ("web".equals(scheme)) {
Path tmp = null;
try {
tmp = downloadToTempFile(u, ext);
rememberTemp(tmp);
playUri(tmp.toUri());
return;
} catch (IOException e) {
safeDelete(tmp);
throw new RuntimeException("Failed to load web audio: " + e.getMessage(), e);
}
}
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
}
@Override
public void pause() {
Platform.runLater(() -> {
MediaPlayer p = player;
if (p != null) p.pause();
});
}
@Override
public void stop() {
Platform.runLater(this::stopInternal);
}
@Override
public void setVolume(double volume) {
double v = clamp01(volume);
this.volume = v;
Platform.runLater(() -> {
MediaPlayer p = player;
if (p != null) p.setVolume(v);
});
}
@Override
public void setLoop(boolean loop) {
this.loop = loop;
Platform.runLater(() -> {
MediaPlayer p = player;
if (p != null) p.setCycleCount(loop ? MediaPlayer.INDEFINITE : 1);
});
}
private void playUri(URI uri) {
Platform.runLater(() -> {
stopInternal();
Media media = new Media(uri.toString());
MediaPlayer p = new MediaPlayer(media);
p.setCycleCount(loop ? MediaPlayer.INDEFINITE : 1);
p.setVolume(volume);
p.setOnEndOfMedia(() -> {
if (!loop) {
try {
p.dispose();
} catch (Exception ignored) {
// ignore
}
}
});
this.player = p;
p.play();
});
}
private void stopInternal() {
MediaPlayer p = this.player;
this.player = null;
if (p != null) {
try {
p.stop();
} catch (Exception ignored) {
// ignore
}
try {
p.dispose();
} catch (Exception ignored) {
// ignore
}
}
Path tmp = this.lastTempFile;
this.lastTempFile = null;
safeDelete(tmp);
}
private static Path downloadToTempFile(String url, String ext) throws IOException {
URL u = new URL(url);
URLConnection con = u.openConnection();
con.setUseCaches(false);
String safeExt = (ext == null || ext.isBlank()) ? "bin" : ext.toLowerCase(Locale.ROOT);
Path tmp = Files.createTempFile("oac-audio-", "." + safeExt);
try (InputStream in = con.getInputStream()) {
Files.copy(in, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
safeDelete(tmp);
throw e;
}
Files.write(tmp, new byte[0], StandardOpenOption.APPEND);
return tmp;
}
private void rememberTemp(Path tmp) {
Path prev = this.lastTempFile;
this.lastTempFile = tmp;
safeDelete(prev);
}
private static void safeDelete(Path p) {
if (p == null) return;
try {
Files.deleteIfExists(p);
} catch (IOException ignored) {
// best-effort
}
}
private static double clamp01(double v) {
if (v < 0.0) return 0.0;
if (v > 1.0) return 1.0;
return v;
}
private static void ensureAllowedAudioExtension(String ext, String source) {
String e = (ext == null) ? "" : ext.toLowerCase(Locale.ROOT);
if (e.isEmpty() || !AUDIO_EXTENSIONS.contains(e)) {
throw new IllegalArgumentException("Unsupported audio format '" + e + "' for: " + source);
}
}
}