1 package swingjs.api.js;
3 import java.awt.Container;
4 import java.awt.Dimension;
6 import java.awt.Graphics;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.image.BufferedImage;
12 import java.nio.file.Files;
13 import java.util.ArrayList;
14 import java.util.function.Function;
16 import javax.swing.BoxLayout;
17 import javax.swing.ImageIcon;
18 import javax.swing.JButton;
19 import javax.swing.JDialog;
20 import javax.swing.JLabel;
21 import javax.swing.JPanel;
23 import swingjs.api.JSUtilI;
26 * A full-service interface for HTML5 video element interaction. Allows setting
27 * and getting HTML5 video element properties. ActionListeners can be set to
28 * listen for JavaScript events associated with a video element.
30 * Video is added using a JavaScript-only two-parameter constructor for
31 * ImageIcon with "jsvideo" as the description, allowing for video construction
32 * from byte[], File, or URL.
34 * After adding the ImageIcon to a JLabel, calling
35 * jlabel.getClientProperty("jsvideo") returns an HTML5 object of type
36 * HTML5Video (the <video> tag), which has the full suite of HTML5 video
37 * element properties, methods, and events.
39 * Access to event listeners is via the method addActionListener, below, which
40 * return an ActionEvent that has as its source both the video element source as
41 * well as the original JavaScript event as an Object[] { jsvideo, event }. The
42 * id of this ActionEvent is 12345, and its command is the name of the event,
43 * for example, "canplay" or "canplaythrough".
45 * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement for
51 public interface HTML5Video extends DOMNode {
53 public interface Promise {
57 final static String[] eventTypes = new String[] { "audioprocess", // The input buffer of a ScriptProcessorNode is
58 // ready to be processed.
59 "canplay", // The browser can play the media, but estimates that not enough data has been
60 // loaded to play the media up to its end without having to stop for further
61 // buffering of content.
62 "canplaythrough", // The browser estimates it can play the media up to its end without stopping
63 // for content buffering.
64 "complete", // The rendering of an OfflineAudioContext is terminated.
65 "durationchange", // The duration attribute has been updated.
66 "emptied", // The media has become empty; for example, this event is sent if the media has
67 // already been loaded (or partially loaded), and the load() method is called to
69 "ended", // Playback has stopped because the end of the media was reached.
70 "loadeddata", // The first frame of the media has finished loading.
71 "loadedmetadata", // The metadata has been loaded.
72 "pause", // Playback has been paused.
73 "play", // Playback has begun.
74 "playing", // Playback is ready to start after having been paused or delayed due to lack of
76 "progress", // Fired periodically as the browser loads a resource.
77 "ratechange", // The playback rate has changed.
78 "seeked", // A seek operation completed.
79 "seeking", // A seek operation began.
80 "stalled", // The user agent is trying to fetch media data, but data is unexpectedly not
82 "suspend", // Media data loading has been suspended.
83 "timeupdate", // The time indicated by the currentTimeattribute has been updated.
84 "volumechange", // The volume has changed.
85 "waiting", // Playback has stopped because of a temporary lack of data
90 public void addTextTrack() throws Throwable;
92 public Object captureStream() throws Throwable;
94 public String canPlayType(String mediaType) throws Throwable;
96 public void fastSeek(double time) throws Throwable;
98 public void load() throws Throwable;
100 public void mozCaptureStream() throws Throwable;
102 public void mozCaptureStreamUntilEnded() throws Throwable;
104 public void mozGetMetadata() throws Throwable;
106 public void pause() throws Throwable;
108 public Promise play() throws Throwable;
110 public Promise seekToNextFrame() throws Throwable;
112 public Promise setMediaKeys(Object mediaKeys) throws Throwable;
114 public Promise setSinkId(String id) throws Throwable;
116 // convenience methods
118 public static double getDuration(HTML5Video v) {
119 return /** @j2sNative v.duration || */
123 public static double setCurrentTime(HTML5Video v, double time) {
124 return /** @j2sNative v.currentTime = time|| */
128 public static double getCurrentTime(HTML5Video v) {
129 return /** @j2sNative v.currentTime|| */
133 public static Dimension getSize(HTML5Video v) {
134 return new Dimension(/** @j2sNative v.videoWidth || */
135 0, /** @j2sNative v.videoHeight|| */
141 * Create a BufferedIfmage from the current frame. The image will be of type
142 * swingjs.api.JSUtilI.TYPE_4BYTE_HTML5, matching the data buffer of HTML5
146 * @param imageType if Integer.MIN_VALUE, swingjs.api.JSUtilI.TYPE_4BYTE_HTML5
149 public static BufferedImage getImage(HTML5Video v, int imageType) {
150 Dimension d = HTML5Video.getSize(v);
151 BufferedImage image = (BufferedImage) HTML5Video.getProperty(v, "_image");
152 if (image == null || image.getWidth() != d.width || image.getHeight() != d.height) {
153 image = new BufferedImage(d.width, d.height, imageType == Integer.MIN_VALUE ? JSUtilI.TYPE_4BYTE_HTML5 : imageType);
154 HTML5Video.setProperty(v, "_image", image);
156 HTML5Canvas.setImageNode(v, image);
160 // property setting and getting
163 * Set a property of the the HTML5 video element using jsvideo[key] = value.
164 * Numbers and Booleans will be unboxed.
166 * @param jsvideo the HTML5 video element
170 public static void setProperty(HTML5Video jsvideo, String key, Object value) {
171 if (value instanceof Number) {
172 /** @j2sNative jsvideo[key] = +value; */
173 } else if (value instanceof Boolean) {
174 /** @j2sNative jsvideo[key] = !!+value */
176 /** @j2sNative jsvideo[key] = value; */
181 * Get a property using jsvideo[key], boxing number as Double and boolean as
184 * @param jsvideo the HTML5 video element
187 * @return value or value boxed as Double or Boolean
189 @SuppressWarnings("unused")
190 public static Object getProperty(HTML5Video jsvideo, String key) {
191 Object val = (/** @j2sNative 1? jsvideo[key] : */
195 switch (/** @j2sNative typeof val || */
198 return Double.valueOf(/** @j2sNative val || */
201 return Boolean.valueOf(/** @j2sNative val || */
211 * Add an ActionListener for the designated events. When an event is fired,
213 * @param jsvideo the HTML5 video element
215 * @param events array of events to listen to or null to listen on all video
216 * element event types
217 * @return an array of event/listener pairs that can be used for removal.
219 public static Object[] addActionListener(HTML5Video jsvideo, ActionListener listener, String... events) {
220 if (events == null || events.length == 0)
222 @SuppressWarnings("unused")
223 Function<Object, Void> f = new Function<Object, Void>() {
226 public Void apply(Object jsevent) {
227 String name = (/** @j2sNative jsevent.type || */
229 System.out.println("HTML5Video " + name);
230 ActionEvent e = new ActionEvent(new Object[] { jsvideo, jsevent }, 12345, name,
231 System.currentTimeMillis(), 0);
232 listener.actionPerformed(e);
236 ArrayList<Object> listeners = new ArrayList<>();
237 for (int i = 0; i < events.length; i++) {
239 * @j2sNative function(event){f.apply$O.apply(f, [event])} ||
242 listeners.add(events[i]);
245 jsvideo.addEventListener(events[i], func);
248 return listeners.toArray(new Object[listeners.size()]);
252 * Remove action listener
254 * @param jsvideo the HTML5 video element
255 * @param listeners an array of event/listener pairs created by
258 public static void removeActionListener(HTML5Video jsvideo, Object[] listeners) {
259 if (listeners == null) {
260 for (int i = 0; i < eventTypes.length; i++) {
261 jsvideo.removeEventListener(eventTypes[i]);
265 for (int i = 0; i < listeners.length; i += 2) {
266 String event = (String) listeners[i];
267 Object listener = listeners[i + 1];
268 jsvideo.removeEventListener(event, listener);
273 * Create an ImageIcon which, when placed in a JLabel, displays the video.
278 public static ImageIcon createIcon(Object source) {
280 if (source instanceof URL) {
281 return new ImageIcon((URL) source, "jsvideo");
282 } else if (source instanceof byte[]) {
283 return new ImageIcon((byte[]) source, "jsvideo");
284 } else if (source instanceof File) {
285 return new ImageIcon(Files.readAllBytes(((File) source).toPath()));
287 return new ImageIcon(Files.readAllBytes(new File(source.toString()).toPath()));
289 } catch (Throwable t) {
295 * Create a label that, when shown, displays the video.
300 public static JLabel createLabel(Object source) {
301 ImageIcon icon = (source instanceof ImageIcon ? (ImageIcon) source : createIcon(source));
302 return (icon == null ? null : new JLabel(icon));
306 * Create a dialog that includes rudimentary controls. Optional maxWidth allows image downscaling by factors of two.
313 public static JDialog createDialog(Frame parent, Object source, int maxWidth, Function<HTML5Video, Void> whenReady) {
314 JDialog dialog = new JDialog(parent);
315 Container p = dialog.getContentPane();
316 p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
317 JLabel label = (source instanceof JLabel ? (JLabel) source : createLabel(source));
318 label.setAlignmentX(0.5f);
319 // not in Java! dialog.putClientProperty("jsvideo", label);
321 label.setVisible(false);
322 p.add(getControls(label));
323 dialog.setModal(false);
325 dialog.setVisible(true);
326 dialog.setVisible(false);
327 HTML5Video jsvideo = (HTML5Video) label.getClientProperty("jsvideo");
328 HTML5Video.addActionListener(jsvideo, new ActionListener() {
331 public void actionPerformed(ActionEvent e) {
332 if (label.getClientProperty("jsvideo.size") != null)
334 Dimension dim = HTML5Video.getSize(jsvideo);
335 while (dim.width > maxWidth) {
339 label.putClientProperty("jsvideo.size", dim);
340 label.setPreferredSize(dim);
341 label.setVisible(true);
342 // label.invalidate();
344 // dialog.setVisible(false);
345 if (whenReady != null)
346 whenReady.apply(jsvideo);
349 }, "canplaythrough");
350 HTML5Video.setCurrentTime(jsvideo, 0);
354 static JPanel getControls(JLabel label) {
356 JPanel controls = new JPanel();
357 controls.setAlignmentX(0.5f);
358 JButton btn = new JButton("play");
359 btn.addActionListener(new ActionListener() {
362 public void actionPerformed(ActionEvent e) {
364 ((HTML5Video) label.getClientProperty("jsvideo")).play();
365 } catch (Throwable e1) {
366 e1.printStackTrace();
373 btn = new JButton("pause");
374 btn.addActionListener(new ActionListener() {
377 public void actionPerformed(ActionEvent e) {
379 ((HTML5Video) label.getClientProperty("jsvideo")).pause();
380 } catch (Throwable e1) {
381 e1.printStackTrace();
388 btn = new JButton("reset");
389 btn.addActionListener(new ActionListener() {
392 public void actionPerformed(ActionEvent e) {
393 HTML5Video.setCurrentTime((HTML5Video) label.getClientProperty("jsvideo"), 0);
403 * Advance to the next frame, using seekToNextFrame() if available, or using the time difference supplied.
406 * @param dt seconds to advance if seekToNextFrame() is not available
407 * @return true if can use seekToNextFrame()
410 public static boolean nextFrame(HTML5Video jsvideo, double dt) {
411 Boolean canSeek = (Boolean) getProperty(jsvideo,"_canseek");
412 if (canSeek == null) {
413 setProperty(jsvideo, "_canseek", canSeek = Boolean.valueOf(getProperty(jsvideo, "seekToNextFrame") != null));
417 jsvideo.seekToNextFrame();
419 HTML5Video.setCurrentTime(jsvideo, HTML5Video.getCurrentTime(jsvideo) + dt);
421 } catch (Throwable e1) {
423 return canSeek.booleanValue();
426 public static int getFrameCount(HTML5Video jsvideo) {
427 return (int) (getDuration(jsvideo) / 0.033334);
430 // HTMLMediaElement properties
434 // buffered Read only
437 // controlsList Read only
439 // currentSrc Read only
442 // defaultPlaybackRate
443 // disableRemotePlayback
444 // duration Read only
449 // mediaKeys Read only
450 // mozAudioCaptured Read only
452 // mozFrameBufferLength
453 // mozSampleRate Read only
455 // networkState Read only
461 // readyState Read only
462 // seekable Read only
467 // textTracks Read only
468 // videoTracks Read only
470 // initialTime Read only
471 // mozChannels Read only