1 package swingjs.api.js;
3 import java.awt.Container;
4 import java.awt.Dimension;
6 import java.awt.event.ActionEvent;
7 import java.awt.event.ActionListener;
8 import java.awt.image.BufferedImage;
11 import java.nio.file.Files;
12 import java.util.ArrayList;
13 import java.util.function.Function;
15 import javax.swing.BoxLayout;
16 import javax.swing.ImageIcon;
17 import javax.swing.JButton;
18 import javax.swing.JDialog;
19 import javax.swing.JLabel;
20 import javax.swing.JPanel;
22 import swingjs.api.JSUtilI;
25 * A full-service interface for HTML5 video element interaction. Allows setting
26 * and getting HTML5 video element properties. ActionListeners can be set to
27 * listen for JavaScript events associated with a video element.
29 * Video is added using a JavaScript-only two-parameter constructor for
30 * ImageIcon with "jsvideo" as the description, allowing for video construction
31 * from byte[], File, or URL.
33 * After adding the ImageIcon to a JLabel, calling
34 * jlabel.getClientProperty("jsvideo") returns an HTML5 object of type
35 * HTML5Video (the <video> tag), which has the full suite of HTML5 video
36 * element properties, methods, and events.
38 * Access to event listeners is via the method addActionListener, below, which
39 * return an ActionEvent that has as its source both the video element source as
40 * well as the original JavaScript event as an Object[] { jsvideo, event }. The
41 * id of this ActionEvent is 12345, and its command is the name of the event,
42 * for example, "canplay" or "canplaythrough".
44 * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement for
50 public interface HTML5Video extends DOMNode {
52 public interface Promise {
56 final static String[] eventTypes = new String[] { "audioprocess", // The input buffer of a ScriptProcessorNode is
57 // ready to be processed.
58 "canplay", // The browser can play the media, but estimates that not enough data has been
59 // loaded to play the media up to its end without having to stop for further
60 // buffering of content.
61 "canplaythrough", // The browser estimates it can play the media up to its end without stopping
62 // for content buffering.
63 "complete", // The rendering of an OfflineAudioContext is terminated.
64 "durationchange", // The duration attribute has been updated.
65 "emptied", // The media has become empty; for example, this event is sent if the media has
66 // already been loaded (or partially loaded), and the load() method is called to
68 "ended", // Playback has stopped because the end of the media was reached.
69 "loadeddata", // The first frame of the media has finished loading.
70 "loadedmetadata", // The metadata has been loaded.
71 "pause", // Playback has been paused.
72 "play", // Playback has begun.
73 "playing", // Playback is ready to start after having been paused or delayed due to lack of
75 "progress", // Fired periodically as the browser loads a resource.
76 "ratechange", // The playback rate has changed.
77 "seeked", // A seek operation completed.
78 "seeking", // A seek operation began.
79 "stalled", // The user agent is trying to fetch media data, but data is unexpectedly not
81 "suspend", // Media data loading has been suspended.
82 "timeupdate", // The time indicated by the currentTimeattribute has been updated.
83 "volumechange", // The volume has changed.
84 "waiting", // Playback has stopped because of a temporary lack of data
89 public void addTextTrack() throws Throwable;
91 public Object captureStream() throws Throwable;
93 public String canPlayType(String mediaType) throws Throwable;
95 public void fastSeek(double time) throws Throwable;
97 public void load() throws Throwable;
99 public void mozCaptureStream() throws Throwable;
101 public void mozCaptureStreamUntilEnded() throws Throwable;
103 public void mozGetMetadata() throws Throwable;
105 public void pause() throws Throwable;
107 public Promise play() throws Throwable;
109 public Promise seekToNextFrame() throws Throwable;
111 public Promise setMediaKeys(Object mediaKeys) throws Throwable;
113 public Promise setSinkId(String id) throws Throwable;
115 // convenience methods
117 public static double getDuration(HTML5Video v) {
118 return /** @j2sNative v.duration || */
122 public static double setCurrentTime(HTML5Video v, double time) {
123 return /** @j2sNative v.currentTime = time|| */
127 public static double getCurrentTime(HTML5Video v) {
128 return /** @j2sNative v.currentTime|| */
132 public static Dimension getSize(HTML5Video v) {
133 return new Dimension(/** @j2sNative v.videoWidth || */
134 0, /** @j2sNative v.videoHeight|| */
140 * Create a BufferedIfmage from the current frame. The image will be of type
141 * swingjs.api.JSUtilI.TYPE_4BYTE_HTML5, matching the data buffer of HTML5
145 * @param imageType if Integer.MIN_VALUE, swingjs.api.JSUtilI.TYPE_4BYTE_HTML5
148 public static BufferedImage getImage(HTML5Video v, int imageType) {
149 Dimension d = HTML5Video.getSize(v);
150 BufferedImage image = (BufferedImage) HTML5Video.getProperty(v, "_image");
151 if (image == null || image.getWidth() != d.width || image.getHeight() != d.height) {
152 image = new BufferedImage(d.width, d.height, imageType == Integer.MIN_VALUE ? JSUtilI.TYPE_4BYTE_HTML5 : imageType);
153 HTML5Video.setProperty(v, "_image", image);
155 HTML5Canvas.setImageNode(v, image);
159 // property setting and getting
162 * Set a property of the the HTML5 video element using jsvideo[key] = value.
163 * Numbers and Booleans will be unboxed.
165 * @param jsvideo the HTML5 video element
169 public static void setProperty(HTML5Video jsvideo, String key, Object value) {
170 if (value instanceof Number) {
171 /** @j2sNative jsvideo[key] = +value; */
172 } else if (value instanceof Boolean) {
173 /** @j2sNative jsvideo[key] = !!+value */
175 /** @j2sNative jsvideo[key] = value; */
180 * Get a property using jsvideo[key], boxing number as Double and boolean as
183 * @param jsvideo the HTML5 video element
186 * @return value or value boxed as Double or Boolean
188 @SuppressWarnings("unused")
189 public static Object getProperty(HTML5Video jsvideo, String key) {
190 Object val = (/** @j2sNative 1? jsvideo[key] : */
194 switch (/** @j2sNative typeof val || */
197 return Double.valueOf(/** @j2sNative val || */
200 return Boolean.valueOf(/** @j2sNative val || */
210 * Add an ActionListener for the designated events. When an event is fired,
212 * @param jsvideo the HTML5 video element
214 * @param events array of events to listen to or null to listen on all video
215 * element event types
216 * @return an array of event/listener pairs that can be used for removal.
218 public static Object[] addActionListener(HTML5Video jsvideo, ActionListener listener, String... events) {
219 if (events == null || events.length == 0)
221 @SuppressWarnings("unused")
222 Function<Object, Void> f = new Function<Object, Void>() {
225 public Void apply(Object jsevent) {
226 String name = (/** @j2sNative jsevent.type || */
228 System.out.println("HTML5Video " + name);
229 ActionEvent e = new ActionEvent(new Object[] { jsvideo, jsevent }, 12345, name,
230 System.currentTimeMillis(), 0);
231 listener.actionPerformed(e);
235 ArrayList<Object> listeners = new ArrayList<>();
236 for (int i = 0; i < events.length; i++) {
238 * @j2sNative function(event){f.apply$O.apply(f, [event])} ||
241 listeners.add(events[i]);
244 jsvideo.addEventListener(events[i], func);
247 return listeners.toArray(new Object[listeners.size()]);
251 * Remove action listener
253 * @param jsvideo the HTML5 video element
254 * @param listeners an array of event/listener pairs created by
257 public static void removeActionListener(HTML5Video jsvideo, Object[] listeners) {
258 if (listeners == null) {
259 for (int i = 0; i < eventTypes.length; i++) {
260 jsvideo.removeEventListener(eventTypes[i]);
264 for (int i = 0; i < listeners.length; i += 2) {
265 String event = (String) listeners[i];
266 Object listener = listeners[i + 1];
267 jsvideo.removeEventListener(event, listener);
272 * Create an ImageIcon which, when placed in a JLabel, displays the video.
277 public static ImageIcon createIcon(Object source) {
279 if (source instanceof URL) {
280 return new ImageIcon((URL) source, "jsvideo");
281 } else if (source instanceof byte[]) {
282 return new ImageIcon((byte[]) source, "jsvideo");
283 } else if (source instanceof File) {
284 return new ImageIcon(Files.readAllBytes(((File) source).toPath()));
286 return new ImageIcon(Files.readAllBytes(new File(source.toString()).toPath()));
288 } catch (Throwable t) {
294 * Create a label that, when shown, displays the video.
299 public static JLabel createLabel(Object source) {
300 ImageIcon icon = (source instanceof ImageIcon ? (ImageIcon) source : createIcon(source));
301 return (icon == null ? null : new JLabel(icon));
305 * Create a dialog that includes rudimentary controls. Optional maxWidth allows image downscaling by factors of two.
312 public static JDialog createDialog(Frame parent, Object source, int maxWidth, Function<HTML5Video, Void> whenReady) {
313 JDialog dialog = new JDialog(parent);
314 Container p = dialog.getContentPane();
315 p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
316 JLabel label = (source instanceof JLabel ? (JLabel) source : createLabel(source));
317 label.setAlignmentX(0.5f);
318 // not in Java! dialog.putClientProperty("jsvideo", label);
320 label.setVisible(false);
321 p.add(getControls(label));
322 dialog.setModal(false);
324 dialog.setVisible(true);
325 dialog.setVisible(false);
326 HTML5Video jsvideo = (HTML5Video) label.getClientProperty("jsvideo");
327 HTML5Video.addActionListener(jsvideo, new ActionListener() {
330 public void actionPerformed(ActionEvent e) {
331 if (label.getClientProperty("jsvideo.size") != null)
333 Dimension dim = HTML5Video.getSize(jsvideo);
334 while (dim.width > maxWidth) {
338 label.putClientProperty("jsvideo.size", dim);
339 label.setPreferredSize(dim);
340 label.setVisible(true);
341 // label.invalidate();
343 // dialog.setVisible(false);
344 if (whenReady != null)
345 whenReady.apply(jsvideo);
348 }, "canplaythrough");
349 HTML5Video.setCurrentTime(jsvideo, 0);
353 static JPanel getControls(JLabel label) {
355 JPanel controls = new JPanel();
356 controls.setAlignmentX(0.5f);
357 JButton btn = new JButton("play");
358 btn.addActionListener(new ActionListener() {
361 public void actionPerformed(ActionEvent e) {
363 ((HTML5Video) label.getClientProperty("jsvideo")).play();
364 } catch (Throwable e1) {
365 e1.printStackTrace();
372 btn = new JButton("pause");
373 btn.addActionListener(new ActionListener() {
376 public void actionPerformed(ActionEvent e) {
378 ((HTML5Video) label.getClientProperty("jsvideo")).pause();
379 } catch (Throwable e1) {
380 e1.printStackTrace();
387 btn = new JButton("reset");
388 btn.addActionListener(new ActionListener() {
391 public void actionPerformed(ActionEvent e) {
392 HTML5Video.setCurrentTime((HTML5Video) label.getClientProperty("jsvideo"), 0);
402 * Advance to the next frame, using seekToNextFrame() if available, or using the time difference supplied.
405 * @param dt seconds to advance if seekToNextFrame() is not available
406 * @return true if can use seekToNextFrame()
409 public static boolean nextFrame(HTML5Video jsvideo, double dt) {
410 Boolean canSeek = (Boolean) getProperty(jsvideo,"_canseek");
411 if (canSeek == null) {
412 setProperty(jsvideo, "_canseek", canSeek = Boolean.valueOf(getProperty(jsvideo, "seekToNextFrame") != null));
416 jsvideo.seekToNextFrame();
418 HTML5Video.setCurrentTime(jsvideo, HTML5Video.getCurrentTime(jsvideo) + dt);
420 } catch (Throwable e1) {
422 return canSeek.booleanValue();
425 public static int getFrameCount(HTML5Video jsvideo) {
426 return (int) (getDuration(jsvideo) / 0.033334);
429 // HTMLMediaElement properties
433 // buffered Read only
436 // controlsList Read only
438 // currentSrc Read only
441 // defaultPlaybackRate
442 // disableRemotePlayback
443 // duration Read only
448 // mediaKeys Read only
449 // mozAudioCaptured Read only
451 // mozFrameBufferLength
452 // mozSampleRate Read only
454 // networkState Read only
460 // readyState Read only
461 // seekable Read only
466 // textTracks Read only
467 // videoTracks Read only
469 // initialTime Read only
470 // mozChannels Read only