Added image/video control and property manipulation
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user