X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fswingjs%2Fapi%2Fjs%2FHTML5Video.java;fp=src%2Fswingjs%2Fapi%2Fjs%2FHTML5Video.java;h=073f1ed1b3fe32f49ddab2c7f96bdf6b7e597740;hb=586ade46bdcd05ff028a1cff82c3c527326d28ec;hp=0000000000000000000000000000000000000000;hpb=adcef27f5747b4e70e89a56c3735bc3afb8ce9bf;p=jalview.git diff --git a/src/swingjs/api/js/HTML5Video.java b/src/swingjs/api/js/HTML5Video.java new file mode 100644 index 0000000..073f1ed --- /dev/null +++ b/src/swingjs/api/js/HTML5Video.java @@ -0,0 +1,472 @@ +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 f = new Function() { + + @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 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 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 + +}