Added image/video control and property manipulation
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
package org.openautonomousconnection.luascript.fx;
|
||||
|
||||
import javafx.scene.web.WebEngine;
|
||||
import org.openautonomousconnection.luascript.hosts.VideoHost;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
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.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* VideoHost implementation that renders video inside the WebView DOM using a {@code <video>} element.
|
||||
*
|
||||
* <p>No JavaFX overlay panes are used. Positioning is done by setting CSS via {@code style} attribute.</p>
|
||||
*
|
||||
* <p>Playback control is done via {@link FxDomHost#setProperty(String, String, Object)} and
|
||||
* {@link FxDomHost#call(String, String, Object...)}, i.e. HTMLMediaElement methods.</p>
|
||||
*
|
||||
* <p>For {@code web://} URLs this host downloads the resource via {@link URLConnection} (your installed handler),
|
||||
* stores it as a temporary file, then sets {@code src=file://...} because WebView won't reliably load custom schemes.</p>
|
||||
*/
|
||||
public final class FxVideoHost implements VideoHost {
|
||||
|
||||
private static final Set<String> VIDEO_EXTENSIONS = Set.of(
|
||||
"mp4", "mov", "ogg", "ogv", "gif", "gifv", "avi", "m4v"
|
||||
);
|
||||
|
||||
private static final String VIDEO_ID = "__oac_video";
|
||||
|
||||
private final WebEngine engine;
|
||||
private final FxDomHost dom;
|
||||
|
||||
private volatile boolean loop;
|
||||
private volatile double volume = 1.0;
|
||||
|
||||
private volatile double x;
|
||||
private volatile double y;
|
||||
private volatile double w = 640;
|
||||
private volatile double h = 360;
|
||||
|
||||
private volatile Path lastTempFile;
|
||||
|
||||
/**
|
||||
* Creates a new video host.
|
||||
*
|
||||
* @param engine web engine
|
||||
* @param dom fx dom host
|
||||
*/
|
||||
public FxVideoHost(WebEngine engine, FxDomHost dom) {
|
||||
this.engine = Objects.requireNonNull(engine, "engine");
|
||||
this.dom = Objects.requireNonNull(dom, "dom");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playFile(File file) {
|
||||
Objects.requireNonNull(file, "file");
|
||||
if (!file.isFile()) {
|
||||
throw new IllegalArgumentException("Video file not found: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
String ext = MediaExtensions.extensionOf(file.getName());
|
||||
ensureAllowed(ext, "video", file.getName());
|
||||
|
||||
setSource(file.toURI().toString(), null);
|
||||
resume();
|
||||
}
|
||||
|
||||
@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 ext = MediaExtensions.extensionOf(uri.getPath());
|
||||
ensureAllowed(ext, "video", uri.getPath());
|
||||
|
||||
String scheme = (uri.getScheme() == null) ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
|
||||
if ("http".equals(scheme) || "https".equals(scheme) || "file".equals(scheme)) {
|
||||
setSource(uri.toString(), null);
|
||||
resume();
|
||||
return;
|
||||
}
|
||||
|
||||
if ("web".equals(scheme)) {
|
||||
Path tmp = null;
|
||||
try {
|
||||
tmp = downloadToTempFile(u, ext, "oac-video-");
|
||||
rememberTemp(tmp);
|
||||
setSource(tmp.toUri().toString(), tmp);
|
||||
resume();
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
safeDelete(tmp);
|
||||
throw new RuntimeException("Failed to load web video: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported scheme: " + scheme);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
ensureVideoElement();
|
||||
dom.call(VIDEO_ID, "pause");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
applyMediaProps();
|
||||
v.setAttribute("style", mergeStyle(baseStyle(true), rectStyle(), visualStyle()));
|
||||
dom.call(VIDEO_ID, "play");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
dom.call(VIDEO_ID, "pause");
|
||||
dom.setProperty(VIDEO_ID, "currentTime", 0);
|
||||
v.removeAttribute("src");
|
||||
v.setAttribute("style", mergeStyle(baseStyle(false), rectStyle(), visualStyle()));
|
||||
});
|
||||
|
||||
Path tmp = this.lastTempFile;
|
||||
this.lastTempFile = null;
|
||||
safeDelete(tmp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
v.setAttribute("style", mergeStyle(baseStyle(false), rectStyle(), visualStyle()));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRect(double x, double y, double w, double h) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.w = Math.max(0.0, w);
|
||||
this.h = Math.max(0.0, h);
|
||||
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
boolean visible = "block".equalsIgnoreCase(extractDisplay(v.getAttribute("style")));
|
||||
v.setAttribute("style", mergeStyle(baseStyle(visible), rectStyle(), visualStyle()));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(double volume) {
|
||||
this.volume = clamp01(volume);
|
||||
FxThreadBridge.runAndWait(this::applyMediaProps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLoop(boolean loop) {
|
||||
this.loop = loop;
|
||||
FxThreadBridge.runAndWait(this::applyMediaProps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(double seconds) {
|
||||
double s = Math.max(0.0, seconds);
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
ensureVideoElement();
|
||||
dom.setProperty(VIDEO_ID, "currentTime", s);
|
||||
});
|
||||
}
|
||||
|
||||
private void setSource(String src, Path tempFileKept) {
|
||||
FxThreadBridge.runAndWait(() -> {
|
||||
Element v = ensureVideoElement();
|
||||
v.setAttribute("src", src);
|
||||
applyMediaProps();
|
||||
v.setAttribute("style", mergeStyle(baseStyle(true), rectStyle(), visualStyle()));
|
||||
});
|
||||
|
||||
if (tempFileKept != null) {
|
||||
rememberTemp(tempFileKept);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMediaProps() {
|
||||
ensureVideoElement();
|
||||
dom.setProperty(VIDEO_ID, "loop", loop);
|
||||
dom.setProperty(VIDEO_ID, "volume", volume);
|
||||
}
|
||||
|
||||
private Element ensureVideoElement() {
|
||||
Document doc = dom.requireDocument();
|
||||
Element v = doc.getElementById(VIDEO_ID);
|
||||
if (v != null) return v;
|
||||
|
||||
Element body = (Element) doc.getElementsByTagName("body").item(0);
|
||||
if (body == null) throw new IllegalStateException("No <body> element available");
|
||||
|
||||
v = doc.createElement("video");
|
||||
v.setAttribute("id", VIDEO_ID);
|
||||
// Default: no controls; script can enable via dom.setAttr(id,"controls","controls") if needed.
|
||||
v.setAttribute("preload", "metadata");
|
||||
|
||||
body.appendChild(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
private String baseStyle(boolean visible) {
|
||||
return "position:fixed;"
|
||||
+ "left:0;top:0;"
|
||||
+ "z-index:2147483647;"
|
||||
+ "background-color:black;"
|
||||
+ "display:" + (visible ? "block" : "none") + ";";
|
||||
}
|
||||
|
||||
private String rectStyle() {
|
||||
return "left:" + x + "px;"
|
||||
+ "top:" + y + "px;"
|
||||
+ "width:" + w + "px;"
|
||||
+ "height:" + h + "px;";
|
||||
}
|
||||
|
||||
private String visualStyle() {
|
||||
return "object-fit:contain;";
|
||||
}
|
||||
|
||||
private static String mergeStyle(String... parts) {
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
for (String p : parts) {
|
||||
if (p == null || p.isBlank()) continue;
|
||||
String s = p.trim();
|
||||
if (!s.endsWith(";")) s += ";";
|
||||
sb.append(s);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String extractDisplay(String style) {
|
||||
if (style == null || style.isBlank()) return "";
|
||||
String[] parts = style.split(";");
|
||||
for (String part : parts) {
|
||||
String p = part.trim();
|
||||
if (p.isEmpty()) continue;
|
||||
int idx = p.indexOf(':');
|
||||
if (idx <= 0) continue;
|
||||
String k = p.substring(0, idx).trim().toLowerCase(Locale.ROOT);
|
||||
if ("display".equals(k)) return p.substring(idx + 1).trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static Path downloadToTempFile(String url, String ext, String prefix) 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(prefix, "." + safeExt);
|
||||
|
||||
try (InputStream in = con.getInputStream()) {
|
||||
Files.copy(in, tmp, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
safeDelete(tmp);
|
||||
throw e;
|
||||
}
|
||||
|
||||
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 void ensureAllowed(String ext, String kind, String source) {
|
||||
String e = (ext == null) ? "" : ext.toLowerCase(Locale.ROOT);
|
||||
if (e.isEmpty() || !VIDEO_EXTENSIONS.contains(e)) {
|
||||
throw new IllegalArgumentException("Unsupported " + kind + " format '" + e + "' for: " + source);
|
||||
}
|
||||
}
|
||||
|
||||
private static double clamp01(double v) {
|
||||
if (v < 0.0) return 0.0;
|
||||
if (v > 1.0) return 1.0;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user