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); } } }