Clover icon

Coverage Report

  1. Project Clover database Thu Dec 4 2025 14:43:25 GMT
  2. Package swingjs.api.js

File HTML5Video.java

 

Coverage histogram

../../../img/srcFileCovDistChart0.png
60% of files have more coverage

Code metrics

44
113
20
2
472
257
51
0.45
5.65
10
2.55

Classes

Class Line # Actions
HTML5Video 50 113 51
0.00%
HTML5Video.Promise 52 0 0
-1.0 -
 

Contributing tests

No tests hitting this source file were found.

Source view

1    package swingjs.api.js;
2   
3    import java.awt.Container;
4    import java.awt.Dimension;
5    import java.awt.Frame;
6    import java.awt.event.ActionEvent;
7    import java.awt.event.ActionListener;
8    import java.awt.image.BufferedImage;
9    import java.io.File;
10    import java.net.URL;
11    import java.nio.file.Files;
12    import java.util.ArrayList;
13    import java.util.function.Function;
14   
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;
21   
22    import swingjs.api.JSUtilI;
23   
24    /**
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.
28    *
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.
32    *
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.
37    *
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".
43    *
44    * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement for
45    * details.
46    *
47    * @author hansonr
48    *
49    */
 
50    public interface HTML5Video extends DOMNode {
51   
 
52    public interface Promise {
53   
54    }
55   
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
67    // reload it.
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
74    // data.
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
80    // forthcoming.
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
85    };
86   
87    // direct methods
88   
89    public void addTextTrack() throws Throwable;
90   
91    public Object captureStream() throws Throwable;
92   
93    public String canPlayType(String mediaType) throws Throwable;
94   
95    public void fastSeek(double time) throws Throwable;
96   
97    public void load() throws Throwable;
98   
99    public void mozCaptureStream() throws Throwable;
100   
101    public void mozCaptureStreamUntilEnded() throws Throwable;
102   
103    public void mozGetMetadata() throws Throwable;
104   
105    public void pause() throws Throwable;
106   
107    public Promise play() throws Throwable;
108   
109    public Promise seekToNextFrame() throws Throwable;
110   
111    public Promise setMediaKeys(Object mediaKeys) throws Throwable;
112   
113    public Promise setSinkId(String id) throws Throwable;
114   
115    // convenience methods
116   
 
117  0 toggle public static double getDuration(HTML5Video v) {
118  0 return /** @j2sNative v.duration || */
119    0;
120    }
121   
 
122  0 toggle public static double setCurrentTime(HTML5Video v, double time) {
123  0 return /** @j2sNative v.currentTime = time|| */
124    0;
125    }
126   
 
127  0 toggle public static double getCurrentTime(HTML5Video v) {
128  0 return /** @j2sNative v.currentTime|| */
129    0;
130    }
131   
 
132  0 toggle public static Dimension getSize(HTML5Video v) {
133  0 return new Dimension(/** @j2sNative v.videoWidth || */
134    0, /** @j2sNative v.videoHeight|| */
135    0);
136    }
137   
138    /**
139    *
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
142    * images.
143    *
144    * @param v
145    * @param imageType if Integer.MIN_VALUE, swingjs.api.JSUtilI.TYPE_4BYTE_HTML5
146    * @return
147    */
 
148  0 toggle public static BufferedImage getImage(HTML5Video v, int imageType) {
149  0 Dimension d = HTML5Video.getSize(v);
150  0 BufferedImage image = (BufferedImage) HTML5Video.getProperty(v, "_image");
151  0 if (image == null || image.getWidth() != d.width || image.getHeight() != d.height) {
152  0 image = new BufferedImage(d.width, d.height, imageType == Integer.MIN_VALUE ? JSUtilI.TYPE_4BYTE_HTML5 : imageType);
153  0 HTML5Video.setProperty(v, "_image", image);
154    }
155  0 HTML5Canvas.setImageNode(v, image);
156  0 return image;
157    }
158   
159    // property setting and getting
160   
161    /**
162    * Set a property of the the HTML5 video element using jsvideo[key] = value.
163    * Numbers and Booleans will be unboxed.
164    *
165    * @param jsvideo the HTML5 video element
166    * @param key
167    * @param value
168    */
 
169  0 toggle public static void setProperty(HTML5Video jsvideo, String key, Object value) {
170  0 if (value instanceof Number) {
171    /** @j2sNative jsvideo[key] = +value; */
172  0 } else if (value instanceof Boolean) {
173    /** @j2sNative jsvideo[key] = !!+value */
174    } else {
175    /** @j2sNative jsvideo[key] = value; */
176    }
177    }
178   
179    /**
180    * Get a property using jsvideo[key], boxing number as Double and boolean as
181    * Boolean.
182    *
183    * @param jsvideo the HTML5 video element
184    *
185    * @param key
186    * @return value or value boxed as Double or Boolean
187    */
 
188  0 toggle @SuppressWarnings("unused")
189    public static Object getProperty(HTML5Video jsvideo, String key) {
190  0 Object val = (/** @j2sNative 1? jsvideo[key] : */
191    null);
192  0 if (val == null)
193  0 return null;
194  0 switch (/** @j2sNative typeof val || */
195    "") {
196  0 case "number":
197  0 return Double.valueOf(/** @j2sNative val || */
198    0);
199  0 case "boolean":
200  0 return Boolean.valueOf(/** @j2sNative val || */
201    false);
202  0 default:
203  0 return val;
204    }
205    }
206   
207    // event action
208   
209    /**
210    * Add an ActionListener for the designated events. When an event is fired,
211    *
212    * @param jsvideo the HTML5 video element
213    * @param listener
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.
217    */
 
218  0 toggle public static Object[] addActionListener(HTML5Video jsvideo, ActionListener listener, String... events) {
219  0 if (events == null || events.length == 0)
220  0 events = eventTypes;
221  0 @SuppressWarnings("unused")
222    Function<Object, Void> f = new Function<Object, Void>() {
223   
 
224  0 toggle @Override
225    public Void apply(Object jsevent) {
226  0 String name = (/** @j2sNative jsevent.type || */
227    "?");
228  0 System.out.println("HTML5Video " + name);
229  0 ActionEvent e = new ActionEvent(new Object[] { jsvideo, jsevent }, 12345, name,
230    System.currentTimeMillis(), 0);
231  0 listener.actionPerformed(e);
232  0 return null;
233    }
234    };
235  0 ArrayList<Object> listeners = new ArrayList<>();
236  0 for (int i = 0; i < events.length; i++) {
237  0 Object func = /**
238    * @j2sNative function(event){f.apply$O.apply(f, [event])} ||
239    */
240    null;
241  0 listeners.add(events[i]);
242  0 listeners.add(func);
243  0 if (jsvideo != null)
244  0 jsvideo.addEventListener(events[i], func);
245   
246    }
247  0 return listeners.toArray(new Object[listeners.size()]);
248    }
249   
250    /**
251    * Remove action listener
252    *
253    * @param jsvideo the HTML5 video element
254    * @param listeners an array of event/listener pairs created by
255    * addActionListener
256    */
 
257  0 toggle public static void removeActionListener(HTML5Video jsvideo, Object[] listeners) {
258  0 if (listeners == null) {
259  0 for (int i = 0; i < eventTypes.length; i++) {
260  0 jsvideo.removeEventListener(eventTypes[i]);
261    }
262    }
263   
264  0 for (int i = 0; i < listeners.length; i += 2) {
265  0 String event = (String) listeners[i];
266  0 Object listener = listeners[i + 1];
267  0 jsvideo.removeEventListener(event, listener);
268    }
269    }
270   
271    /**
272    * Create an ImageIcon which, when placed in a JLabel, displays the video.
273    *
274    * @param source
275    * @return
276    */
 
277  0 toggle public static ImageIcon createIcon(Object source) {
278  0 try {
279  0 if (source instanceof URL) {
280  0 return new ImageIcon((URL) source, "jsvideo");
281  0 } else if (source instanceof byte[]) {
282  0 return new ImageIcon((byte[]) source, "jsvideo");
283  0 } else if (source instanceof File) {
284  0 return new ImageIcon(Files.readAllBytes(((File) source).toPath()));
285    } else {
286  0 return new ImageIcon(Files.readAllBytes(new File(source.toString()).toPath()));
287    }
288    } catch (Throwable t) {
289  0 return null;
290    }
291    }
292   
293    /**
294    * Create a label that, when shown, displays the video.
295    *
296    * @param source
297    * @return
298    */
 
299  0 toggle public static JLabel createLabel(Object source) {
300  0 ImageIcon icon = (source instanceof ImageIcon ? (ImageIcon) source : createIcon(source));
301  0 return (icon == null ? null : new JLabel(icon));
302    }
303   
304    /**
305    * Create a dialog that includes rudimentary controls. Optional maxWidth allows image downscaling by factors of two.
306    *
307    * @param parent
308    * @param source
309    * @param maxWidth
310    * @return
311    */
 
312  0 toggle public static JDialog createDialog(Frame parent, Object source, int maxWidth, Function<HTML5Video, Void> whenReady) {
313  0 JDialog dialog = new JDialog(parent);
314  0 Container p = dialog.getContentPane();
315  0 p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
316  0 JLabel label = (source instanceof JLabel ? (JLabel) source : createLabel(source));
317  0 label.setAlignmentX(0.5f);
318    // not in Java! dialog.putClientProperty("jsvideo", label);
319  0 p.add(label);
320  0 label.setVisible(false);
321  0 p.add(getControls(label));
322  0 dialog.setModal(false);
323  0 dialog.pack();
324  0 dialog.setVisible(true);
325  0 dialog.setVisible(false);
326  0 HTML5Video jsvideo = (HTML5Video) label.getClientProperty("jsvideo");
327  0 HTML5Video.addActionListener(jsvideo, new ActionListener() {
328   
 
329  0 toggle @Override
330    public void actionPerformed(ActionEvent e) {
331  0 if (label.getClientProperty("jsvideo.size") != null)
332  0 return;
333  0 Dimension dim = HTML5Video.getSize(jsvideo);
334  0 while (dim.width > maxWidth) {
335  0 dim.width /= 2;
336  0 dim.height /= 2;
337    }
338  0 label.putClientProperty("jsvideo.size", dim);
339  0 label.setPreferredSize(dim);
340  0 label.setVisible(true);
341    // label.invalidate();
342  0 dialog.pack();
343    // dialog.setVisible(false);
344  0 if (whenReady != null)
345  0 whenReady.apply(jsvideo);
346    }
347   
348    }, "canplaythrough");
349  0 HTML5Video.setCurrentTime(jsvideo, 0);
350  0 return dialog;
351    }
352   
 
353  0 toggle static JPanel getControls(JLabel label) {
354   
355  0 JPanel controls = new JPanel();
356  0 controls.setAlignmentX(0.5f);
357  0 JButton btn = new JButton("play");
358  0 btn.addActionListener(new ActionListener() {
359   
 
360  0 toggle @Override
361    public void actionPerformed(ActionEvent e) {
362  0 try {
363  0 ((HTML5Video) label.getClientProperty("jsvideo")).play();
364    } catch (Throwable e1) {
365  0 e1.printStackTrace();
366    }
367    }
368   
369    });
370  0 controls.add(btn);
371   
372  0 btn = new JButton("pause");
373  0 btn.addActionListener(new ActionListener() {
374   
 
375  0 toggle @Override
376    public void actionPerformed(ActionEvent e) {
377  0 try {
378  0 ((HTML5Video) label.getClientProperty("jsvideo")).pause();
379    } catch (Throwable e1) {
380  0 e1.printStackTrace();
381    }
382    }
383   
384    });
385  0 controls.add(btn);
386   
387  0 btn = new JButton("reset");
388  0 btn.addActionListener(new ActionListener() {
389   
 
390  0 toggle @Override
391    public void actionPerformed(ActionEvent e) {
392  0 HTML5Video.setCurrentTime((HTML5Video) label.getClientProperty("jsvideo"), 0);
393    }
394   
395    });
396  0 controls.add(btn);
397   
398  0 return controls;
399    }
400   
401    /**
402    * Advance to the next frame, using seekToNextFrame() if available, or using the time difference supplied.
403    *
404    * @param jsvideo
405    * @param dt seconds to advance if seekToNextFrame() is not available
406    * @return true if can use seekToNextFrame()
407    *
408    */
 
409  0 toggle public static boolean nextFrame(HTML5Video jsvideo, double dt) {
410  0 Boolean canSeek = (Boolean) getProperty(jsvideo,"_canseek");
411  0 if (canSeek == null) {
412  0 setProperty(jsvideo, "_canseek", canSeek = Boolean.valueOf(getProperty(jsvideo, "seekToNextFrame") != null));
413    }
414  0 try {
415  0 if (canSeek) {
416  0 jsvideo.seekToNextFrame();
417    } else {
418  0 HTML5Video.setCurrentTime(jsvideo, HTML5Video.getCurrentTime(jsvideo) + dt);
419    }
420    } catch (Throwable e1) {
421    }
422  0 return canSeek.booleanValue();
423    }
424   
 
425  0 toggle public static int getFrameCount(HTML5Video jsvideo) {
426  0 return (int) (getDuration(jsvideo) / 0.033334);
427    }
428   
429    // HTMLMediaElement properties
430   
431    // audioTracks
432    // autoplay
433    // buffered Read only
434    // controller
435    // controls
436    // controlsList Read only
437    // crossOrigin
438    // currentSrc Read only
439    // currentTime
440    // defaultMuted
441    // defaultPlaybackRate
442    // disableRemotePlayback
443    // duration Read only
444    // ended Read only
445    // error Read only
446    // loop
447    // mediaGroup
448    // mediaKeys Read only
449    // mozAudioCaptured Read only
450    // mozFragmentEnd
451    // mozFrameBufferLength
452    // mozSampleRate Read only
453    // muted
454    // networkState Read only
455    // paused Read only
456    // playbackRate
457    // played Read only
458    // preload
459    // preservesPitch
460    // readyState Read only
461    // seekable Read only
462    // seeking Read only
463    // sinkId Read only
464    // src
465    // srcObject
466    // textTracks Read only
467    // videoTracks Read only
468    // volume
469    // initialTime Read only
470    // mozChannels Read only
471   
472    }