212 lines
6.0 KiB
Java
212 lines
6.0 KiB
Java
|
|
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.
|
||
|
|
*
|
||
|
|
* <p>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://}.</p>
|
||
|
|
*/
|
||
|
|
public final class FxAudioHost implements AudioHost {
|
||
|
|
|
||
|
|
private static final Set<String> 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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|