--- /dev/null
+package swingjs.api.js;
+
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Frame;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.function.Function;
+
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import swingjs.api.JSUtilI;
+
+/**
+ * A full-service interface for HTML5 video element interaction. Allows setting
+ * and getting HTML5 video element properties. ActionListeners can be set to
+ * listen for JavaScript events associated with a video element.
+ *
+ * Video is added using a JavaScript-only two-parameter constructor for
+ * ImageIcon with "jsvideo" as the description, allowing for video construction
+ * from byte[], File, or URL.
+ *
+ * After adding the ImageIcon to a JLabel, calling
+ * jlabel.getClientProperty("jsvideo") returns an HTML5 object of type
+ * HTML5Video (the <video> tag), which has the full suite of HTML5 video
+ * element properties, methods, and events.
+ *
+ * Access to event listeners is via the method addActionListener, below, which
+ * return an ActionEvent that has as its source both the video element source as
+ * well as the original JavaScript event as an Object[] { jsvideo, event }. The
+ * id of this ActionEvent is 12345, and its command is the name of the event,
+ * for example, "canplay" or "canplaythrough".
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement for
+ * details.
+ *
+ * @author hansonr
+ *
+ */
+public interface HTML5Video extends DOMNode {
+
+ public interface Promise {
+
+ }
+
+ final static String[] eventTypes = new String[] { "audioprocess", // The input buffer of a ScriptProcessorNode is
+ // ready to be processed.
+ "canplay", // The browser can play the media, but estimates that not enough data has been
+ // loaded to play the media up to its end without having to stop for further
+ // buffering of content.
+ "canplaythrough", // The browser estimates it can play the media up to its end without stopping
+ // for content buffering.
+ "complete", // The rendering of an OfflineAudioContext is terminated.
+ "durationchange", // The duration attribute has been updated.
+ "emptied", // The media has become empty; for example, this event is sent if the media has
+ // already been loaded (or partially loaded), and the load() method is called to
+ // reload it.
+ "ended", // Playback has stopped because the end of the media was reached.
+ "loadeddata", // The first frame of the media has finished loading.
+ "loadedmetadata", // The metadata has been loaded.
+ "pause", // Playback has been paused.
+ "play", // Playback has begun.
+ "playing", // Playback is ready to start after having been paused or delayed due to lack of
+ // data.
+ "progress", // Fired periodically as the browser loads a resource.
+ "ratechange", // The playback rate has changed.
+ "seeked", // A seek operation completed.
+ "seeking", // A seek operation began.
+ "stalled", // The user agent is trying to fetch media data, but data is unexpectedly not
+ // forthcoming.
+ "suspend", // Media data loading has been suspended.
+ "timeupdate", // The time indicated by the currentTimeattribute has been updated.
+ "volumechange", // The volume has changed.
+ "waiting", // Playback has stopped because of a temporary lack of data
+ };
+
+ // direct methods
+
+ public void addTextTrack() throws Throwable;
+
+ public Object captureStream() throws Throwable;
+
+ public String canPlayType(String mediaType) throws Throwable;
+
+ public void fastSeek(double time) throws Throwable;
+
+ public void load() throws Throwable;
+
+ public void mozCaptureStream() throws Throwable;
+
+ public void mozCaptureStreamUntilEnded() throws Throwable;
+
+ public void mozGetMetadata() throws Throwable;
+
+ public void pause() throws Throwable;
+
+ public Promise play() throws Throwable;
+
+ public Promise seekToNextFrame() throws Throwable;
+
+ public Promise setMediaKeys(Object mediaKeys) throws Throwable;
+
+ public Promise setSinkId(String id) throws Throwable;
+
+ // convenience methods
+
+ public static double getDuration(HTML5Video v) {
+ return /** @j2sNative v.duration || */
+ 0;
+ }
+
+ public static double setCurrentTime(HTML5Video v, double time) {
+ return /** @j2sNative v.currentTime = time|| */
+ 0;
+ }
+
+ public static double getCurrentTime(HTML5Video v) {
+ return /** @j2sNative v.currentTime|| */
+ 0;
+ }
+
+ public static Dimension getSize(HTML5Video v) {
+ return new Dimension(/** @j2sNative v.videoWidth || */
+ 0, /** @j2sNative v.videoHeight|| */
+ 0);
+ }
+
+ /**
+ *
+ * Create a BufferedIfmage from the current frame. The image will be of type
+ * swingjs.api.JSUtilI.TYPE_4BYTE_HTML5, matching the data buffer of HTML5
+ * images.
+ *
+ * @param v
+ * @param imageType if Integer.MIN_VALUE, swingjs.api.JSUtilI.TYPE_4BYTE_HTML5
+ * @return
+ */
+ public static BufferedImage getImage(HTML5Video v, int imageType) {
+ Dimension d = HTML5Video.getSize(v);
+ BufferedImage image = (BufferedImage) HTML5Video.getProperty(v, "_image");
+ if (image == null || image.getWidth() != d.width || image.getHeight() != d.height) {
+ image = new BufferedImage(d.width, d.height, imageType == Integer.MIN_VALUE ? JSUtilI.TYPE_4BYTE_HTML5 : imageType);
+ HTML5Video.setProperty(v, "_image", image);
+ }
+ HTML5Canvas.setImageNode(v, image);
+ return image;
+ }
+
+ // property setting and getting
+
+ /**
+ * Set a property of the the HTML5 video element using jsvideo[key] = value.
+ * Numbers and Booleans will be unboxed.
+ *
+ * @param jsvideo the HTML5 video element
+ * @param key
+ * @param value
+ */
+ public static void setProperty(HTML5Video jsvideo, String key, Object value) {
+ if (value instanceof Number) {
+ /** @j2sNative jsvideo[key] = +value; */
+ } else if (value instanceof Boolean) {
+ /** @j2sNative jsvideo[key] = !!+value */
+ } else {
+ /** @j2sNative jsvideo[key] = value; */
+ }
+ }
+
+ /**
+ * Get a property using jsvideo[key], boxing number as Double and boolean as
+ * Boolean.
+ *
+ * @param jsvideo the HTML5 video element
+ *
+ * @param key
+ * @return value or value boxed as Double or Boolean
+ */
+ @SuppressWarnings("unused")
+ public static Object getProperty(HTML5Video jsvideo, String key) {
+ Object val = (/** @j2sNative 1? jsvideo[key] : */
+ null);
+ if (val == null)
+ return null;
+ switch (/** @j2sNative typeof val || */
+ "") {
+ case "number":
+ return Double.valueOf(/** @j2sNative val || */
+ 0);
+ case "boolean":
+ return Boolean.valueOf(/** @j2sNative val || */
+ false);
+ default:
+ return val;
+ }
+ }
+
+ // event action
+
+ /**
+ * Add an ActionListener for the designated events. When an event is fired,
+ *
+ * @param jsvideo the HTML5 video element
+ * @param listener
+ * @param events array of events to listen to or null to listen on all video
+ * element event types
+ * @return an array of event/listener pairs that can be used for removal.
+ */
+ public static Object[] addActionListener(HTML5Video jsvideo, ActionListener listener, String... events) {
+ if (events == null || events.length == 0)
+ events = eventTypes;
+ @SuppressWarnings("unused")
+ Function<Object, Void> f = new Function<Object, Void>() {
+
+ @Override
+ public Void apply(Object jsevent) {
+ String name = (/** @j2sNative jsevent.type || */
+ "?");
+ System.out.println("HTML5Video " + name);
+ ActionEvent e = new ActionEvent(new Object[] { jsvideo, jsevent }, 12345, name,
+ System.currentTimeMillis(), 0);
+ listener.actionPerformed(e);
+ return null;
+ }
+ };
+ ArrayList<Object> listeners = new ArrayList<>();
+ for (int i = 0; i < events.length; i++) {
+ Object func = /**
+ * @j2sNative function(event){f.apply$O.apply(f, [event])} ||
+ */
+ null;
+ listeners.add(events[i]);
+ listeners.add(func);
+ if (jsvideo != null)
+ jsvideo.addEventListener(events[i], func);
+
+ }
+ return listeners.toArray(new Object[listeners.size()]);
+ }
+
+ /**
+ * Remove action listener
+ *
+ * @param jsvideo the HTML5 video element
+ * @param listeners an array of event/listener pairs created by
+ * addActionListener
+ */
+ public static void removeActionListener(HTML5Video jsvideo, Object[] listeners) {
+ if (listeners == null) {
+ for (int i = 0; i < eventTypes.length; i++) {
+ jsvideo.removeEventListener(eventTypes[i]);
+ }
+ }
+
+ for (int i = 0; i < listeners.length; i += 2) {
+ String event = (String) listeners[i];
+ Object listener = listeners[i + 1];
+ jsvideo.removeEventListener(event, listener);
+ }
+ }
+
+ /**
+ * Create an ImageIcon which, when placed in a JLabel, displays the video.
+ *
+ * @param source
+ * @return
+ */
+ public static ImageIcon createIcon(Object source) {
+ try {
+ if (source instanceof URL) {
+ return new ImageIcon((URL) source, "jsvideo");
+ } else if (source instanceof byte[]) {
+ return new ImageIcon((byte[]) source, "jsvideo");
+ } else if (source instanceof File) {
+ return new ImageIcon(Files.readAllBytes(((File) source).toPath()));
+ } else {
+ return new ImageIcon(Files.readAllBytes(new File(source.toString()).toPath()));
+ }
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ /**
+ * Create a label that, when shown, displays the video.
+ *
+ * @param source
+ * @return
+ */
+ public static JLabel createLabel(Object source) {
+ ImageIcon icon = (source instanceof ImageIcon ? (ImageIcon) source : createIcon(source));
+ return (icon == null ? null : new JLabel(icon));
+ }
+
+ /**
+ * Create a dialog that includes rudimentary controls. Optional maxWidth allows image downscaling by factors of two.
+ *
+ * @param parent
+ * @param source
+ * @param maxWidth
+ * @return
+ */
+ public static JDialog createDialog(Frame parent, Object source, int maxWidth, Function<HTML5Video, Void> whenReady) {
+ JDialog dialog = new JDialog(parent);
+ Container p = dialog.getContentPane();
+ p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
+ JLabel label = (source instanceof JLabel ? (JLabel) source : createLabel(source));
+ label.setAlignmentX(0.5f);
+ // not in Java! dialog.putClientProperty("jsvideo", label);
+ p.add(label);
+ label.setVisible(false);
+ p.add(getControls(label));
+ dialog.setModal(false);
+ dialog.pack();
+ dialog.setVisible(true);
+ dialog.setVisible(false);
+ HTML5Video jsvideo = (HTML5Video) label.getClientProperty("jsvideo");
+ HTML5Video.addActionListener(jsvideo, new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (label.getClientProperty("jsvideo.size") != null)
+ return;
+ Dimension dim = HTML5Video.getSize(jsvideo);
+ while (dim.width > maxWidth) {
+ dim.width /= 2;
+ dim.height /= 2;
+ }
+ label.putClientProperty("jsvideo.size", dim);
+ label.setPreferredSize(dim);
+ label.setVisible(true);
+// label.invalidate();
+ dialog.pack();
+// dialog.setVisible(false);
+ if (whenReady != null)
+ whenReady.apply(jsvideo);
+ }
+
+ }, "canplaythrough");
+ HTML5Video.setCurrentTime(jsvideo, 0);
+ return dialog;
+ }
+
+ static JPanel getControls(JLabel label) {
+
+ JPanel controls = new JPanel();
+ controls.setAlignmentX(0.5f);
+ JButton btn = new JButton("play");
+ btn.addActionListener(new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ try {
+ ((HTML5Video) label.getClientProperty("jsvideo")).play();
+ } catch (Throwable e1) {
+ e1.printStackTrace();
+ }
+ }
+
+ });
+ controls.add(btn);
+
+ btn = new JButton("pause");
+ btn.addActionListener(new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ try {
+ ((HTML5Video) label.getClientProperty("jsvideo")).pause();
+ } catch (Throwable e1) {
+ e1.printStackTrace();
+ }
+ }
+
+ });
+ controls.add(btn);
+
+ btn = new JButton("reset");
+ btn.addActionListener(new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ HTML5Video.setCurrentTime((HTML5Video) label.getClientProperty("jsvideo"), 0);
+ }
+
+ });
+ controls.add(btn);
+
+ return controls;
+ }
+
+ /**
+ * Advance to the next frame, using seekToNextFrame() if available, or using the time difference supplied.
+ *
+ * @param jsvideo
+ * @param dt seconds to advance if seekToNextFrame() is not available
+ * @return true if can use seekToNextFrame()
+ *
+ */
+ public static boolean nextFrame(HTML5Video jsvideo, double dt) {
+ Boolean canSeek = (Boolean) getProperty(jsvideo,"_canseek");
+ if (canSeek == null) {
+ setProperty(jsvideo, "_canseek", canSeek = Boolean.valueOf(getProperty(jsvideo, "seekToNextFrame") != null));
+ }
+ try {
+ if (canSeek) {
+ jsvideo.seekToNextFrame();
+ } else {
+ HTML5Video.setCurrentTime(jsvideo, HTML5Video.getCurrentTime(jsvideo) + dt);
+ }
+ } catch (Throwable e1) {
+ }
+ return canSeek.booleanValue();
+ }
+
+ public static int getFrameCount(HTML5Video jsvideo) {
+ return (int) (getDuration(jsvideo) / 0.033334);
+ }
+
+// HTMLMediaElement properties
+
+// audioTracks
+// autoplay
+// buffered Read only
+// controller
+// controls
+// controlsList Read only
+// crossOrigin
+// currentSrc Read only
+// currentTime
+// defaultMuted
+// defaultPlaybackRate
+// disableRemotePlayback
+// duration Read only
+// ended Read only
+// error Read only
+// loop
+// mediaGroup
+// mediaKeys Read only
+// mozAudioCaptured Read only
+// mozFragmentEnd
+// mozFrameBufferLength
+// mozSampleRate Read only
+// muted
+// networkState Read only
+// paused Read only
+// playbackRate
+// played Read only
+// preload
+// preservesPitch
+// readyState Read only
+// seekable Read only
+// seeking Read only
+// sinkId Read only
+// src
+// srcObject
+// textTracks Read only
+// videoTracks Read only
+// volume
+// initialTime Read only
+// mozChannels Read only
+
+}