JAL-3446 unused imports removed
[jalview.git] / src / swingjs / api / js / HTML5Video.java
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         public static double getDuration(HTML5Video v) {
118                 return /** @j2sNative v.duration || */
119                 0;
120         }
121
122         public static double setCurrentTime(HTML5Video v, double time) {
123                 return /** @j2sNative v.currentTime = time|| */
124                 0;
125         }
126
127         public static double getCurrentTime(HTML5Video v) {
128                 return /** @j2sNative v.currentTime|| */
129                 0;
130         }
131
132         public static Dimension getSize(HTML5Video v) {
133                 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         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);
154                 }
155                 HTML5Canvas.setImageNode(v, image);
156                 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         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 */
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         @SuppressWarnings("unused")
189         public static Object getProperty(HTML5Video jsvideo, String key) {
190                 Object val = (/** @j2sNative 1? jsvideo[key] : */
191                 null);
192                 if (val == null)
193                         return null;
194                 switch (/** @j2sNative typeof val || */
195                 "") {
196                 case "number":
197                         return Double.valueOf(/** @j2sNative val || */
198                                         0);
199                 case "boolean":
200                         return Boolean.valueOf(/** @j2sNative val || */
201                                         false);
202                 default:
203                         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         public static Object[] addActionListener(HTML5Video jsvideo, ActionListener listener, String... events) {
219                 if (events == null || events.length == 0)
220                         events = eventTypes;
221                 @SuppressWarnings("unused")
222                 Function<Object, Void> f = new Function<Object, Void>() {
223
224                         @Override
225                         public Void apply(Object jsevent) {
226                                 String name = (/** @j2sNative jsevent.type || */
227                                 "?");
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);
232                                 return null;
233                         }
234                 };
235                 ArrayList<Object> listeners = new ArrayList<>();
236                 for (int i = 0; i < events.length; i++) {
237                         Object func = /**
238                                                          * @j2sNative function(event){f.apply$O.apply(f, [event])} ||
239                                                          */
240                                         null;
241                         listeners.add(events[i]);
242                         listeners.add(func);
243                         if (jsvideo != null)
244                                 jsvideo.addEventListener(events[i], func);
245
246                 }
247                 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         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]);
261                         }
262                 }
263                 
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);
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         public static ImageIcon createIcon(Object source) {
278                 try {
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()));
285                         } else {
286                                 return new ImageIcon(Files.readAllBytes(new File(source.toString()).toPath()));
287                         }
288                 } catch (Throwable t) {
289                         return null;
290                 }
291         }
292
293         /**
294          * Create a label that, when shown, displays the video.
295          * 
296          * @param source
297          * @return
298          */
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));
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         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);
319                 p.add(label);
320                 label.setVisible(false);
321                 p.add(getControls(label));
322                 dialog.setModal(false);
323                 dialog.pack();
324                 dialog.setVisible(true);
325                 dialog.setVisible(false);
326                 HTML5Video jsvideo = (HTML5Video) label.getClientProperty("jsvideo");
327                 HTML5Video.addActionListener(jsvideo, new ActionListener() {
328
329                         @Override
330                         public void actionPerformed(ActionEvent e) {
331                                 if (label.getClientProperty("jsvideo.size") != null)
332                                         return;
333                                 Dimension dim = HTML5Video.getSize(jsvideo);
334                                 while (dim.width > maxWidth) {
335                                         dim.width /= 2;
336                                         dim.height /= 2;
337                                 }
338                                 label.putClientProperty("jsvideo.size", dim);
339                                 label.setPreferredSize(dim);
340                                 label.setVisible(true);
341 //                              label.invalidate();
342                                 dialog.pack();
343 //                              dialog.setVisible(false);
344                                 if (whenReady != null)
345                                         whenReady.apply(jsvideo);
346                         }
347                         
348                 }, "canplaythrough");
349                 HTML5Video.setCurrentTime(jsvideo,  0);
350                 return dialog;
351         }
352
353         static JPanel getControls(JLabel label) {
354
355                 JPanel controls = new JPanel();
356                 controls.setAlignmentX(0.5f);
357                 JButton btn = new JButton("play");
358                 btn.addActionListener(new ActionListener() {
359
360                         @Override
361                         public void actionPerformed(ActionEvent e) {
362                                 try {
363                                         ((HTML5Video) label.getClientProperty("jsvideo")).play();
364                                 } catch (Throwable e1) {
365                                         e1.printStackTrace();
366                                 }
367                         }
368
369                 });
370                 controls.add(btn);
371
372                 btn = new JButton("pause");
373                 btn.addActionListener(new ActionListener() {
374
375                         @Override
376                         public void actionPerformed(ActionEvent e) {
377                                 try {
378                                         ((HTML5Video) label.getClientProperty("jsvideo")).pause();
379                                 } catch (Throwable e1) {
380                                         e1.printStackTrace();
381                                 }
382                         }
383
384                 });
385                 controls.add(btn);
386                 
387                 btn = new JButton("reset");
388                 btn.addActionListener(new ActionListener() {
389
390                         @Override
391                         public void actionPerformed(ActionEvent e) {
392                                 HTML5Video.setCurrentTime((HTML5Video) label.getClientProperty("jsvideo"), 0);
393                         }
394
395                 });
396                 controls.add(btn);
397
398                 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         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));
413                 }
414                 try {                   
415                         if (canSeek) {
416                                 jsvideo.seekToNextFrame();
417                         } else {
418                                 HTML5Video.setCurrentTime(jsvideo, HTML5Video.getCurrentTime(jsvideo) + dt);                                            
419                         }
420                 } catch (Throwable e1) {
421                 }
422                 return canSeek.booleanValue();
423         }
424
425         public static int getFrameCount(HTML5Video jsvideo) {
426                 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 }