/* * Copyright (c) 2012 Michael Zucchi * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package au.notzed.fxperiment; import java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.Preferences; import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.RotateTransition; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Bounds; import javafx.geometry.Orientation; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.ScrollPane; import javafx.scene.image.ImageView; import javafx.scene.image.WritableImage; import javafx.scene.layout.FlowPane; import javafx.scene.layout.StackPane; import javafx.scene.media.Media; import javafx.scene.media.MediaException; import javafx.scene.media.MediaPlayer; import javafx.scene.media.MediaPlayer.Status; import javafx.scene.media.MediaView; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.Circle; import javafx.stage.DirectoryChooser; import javafx.stage.Stage; import javafx.util.Duration; import au.notzed.jjmpeg.AVCodecContext; import au.notzed.jjmpeg.exception.AVException; import au.notzed.jjmpeg.exception.AVIOException; import au.notzed.jjmpeg.io.JJMediaReader; import au.notzed.jjmpeg.io.JJMediaReader.JJReaderStream; /** * This is a "simple" experiment of a long list which loads it's animated * content asynchronously. * * @author notzed */ public class VideoLibrary extends Application { final static String tag = "VideoLibrary"; FlowPane flow; ScrollPane scroll; VisibilityManager vbm; @Override public void start(Stage primaryStage) { StackPane root = new StackPane(); flow = new FlowPane(Orientation.HORIZONTAL); scroll = new ScrollPane(); scroll.setContent(flow); root.getChildren().add(scroll); vbm = new VisibilityManager(256, 144); vbm.connect(scroll, flow); Scene scene = new Scene(root, 256 * 4 + 32, 144 * 4); primaryStage.setTitle("Video Library!"); primaryStage.setScene(scene); primaryStage.show(); Preferences p = Preferences.userNodeForPackage(VideoLibrary.class); String dir = p.get("video.drawer", null); if (dir == null) { DirectoryChooser dc = new DirectoryChooser(); dc.setTitle("Select root search path"); File file = dc.showDialog(null); if (file == null) { Platform.exit(); return; } dir = file.getAbsolutePath(); p.put("video.drawer", dir); } Thread th = new Thread(new DirectoryScanner(dir)); th.setDaemon(true); th.start(); } public static void main(String[] args) { launch(args); } class DirectoryScanner extends Task<Object> implements FileVisitor<Path> { String root; public DirectoryScanner(String root) { this.root = root; } @Override protected Object call() throws Exception { try { Files.walkFileTree(Paths.get(root), this); } catch (IOException ex) { Logger.getLogger(tag).log(Level.SEVERE, null, ex); } catch (Exception ex) { ex.printStackTrace(); } finally { System.out.println("complete"); } return null; } @Override public FileVisitResult preVisitDirectory(Path t, BasicFileAttributes bfa) throws IOException { if (isCancelled()) return FileVisitResult.TERMINATE; return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path t, BasicFileAttributes bfa) throws IOException { if (isCancelled()) return FileVisitResult.TERMINATE; if (bfa.isRegularFile()) { JJMediaReader mr = null; try { final String name = t.toAbsolutePath().toString(); System.out.println("Checking: " + name); // See if ffmpeg can decode it & is has a video stream mr = new JJMediaReader(name); for (JJReaderStream rs : mr.getStreams()) { if (rs.getType() == AVCodecContext.AVMEDIA_TYPE_VIDEO) { // Add this source Platform.runLater(new Runnable() { @Override public void run() { // Set mediapreviewpane if you dare ... AsyncImagePane video = new JJPreviewPane( name); // AsyncImagePane video = new // MediaPreviewPane(name); video.setPrefSize(256, 144); flow.getChildren().add(video); } }); break; } } } catch (AVException ex) { } catch (AVIOException ex) { } finally { if (mr != null) { // workaround a bug in jjmpeg which has a null error try { mr.dispose(); } catch (Exception ex) { ex.printStackTrace(); } } } } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path t, IOException ioe) throws IOException { if (isCancelled()) return FileVisitResult.TERMINATE; return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path t, IOException ioe) throws IOException { if (isCancelled()) return FileVisitResult.TERMINATE; return FileVisitResult.CONTINUE; } } class BusySpinner extends Group { public BusySpinner() { ObservableList<Node> children = getChildren(); Paint p = Color.FUCHSIA; for (int r = 0; r < 8; r++) { double R = r * Math.PI * 2 / 8; double x = Math.sin(R) * 24; double y = Math.cos(R) * 24; Circle dot = new Circle(x, y, 6, p); dot.setOpacity((9 - r) / 9.0); children.add(dot); } RotateTransition rt = new RotateTransition(Duration.seconds(1), this); rt.setFromAngle(0); rt.setToAngle(360); rt.setCycleCount(RotateTransition.INDEFINITE); rt.setInterpolator(Interpolator.LINEAR); rt.play(); } } /** * "loads" an image asynchronously, showing a busy spinner if it isn't ready */ class AsyncImagePane extends StackPane { boolean userVisible; public void clear() { getChildren().clear(); } public void startLoading() { ImageView iv = new ImageView(); iv.setFitWidth(256); iv.setFitHeight(144); // blah iv.setPreserveRatio(false); getChildren().add(iv); } public void setUserVisible(boolean userVisible) { if (this.userVisible != userVisible) { this.userVisible = userVisible; if (userVisible) { /* * ImageView iv = new ImageView(); iv.setFitWidth(256); * iv.setFitHeight(144); iv.setPreserveRatio(false); * getChildren().add(iv); */ // getChildren().add(new BusySpinner()); startVideo(); } else { stopVideo(); clear(); } } } void startVideo() { } void stopVideo() { } } class JJPreviewPane extends AsyncImagePane { VideoThread vt; String url; BusySpinner bs; public JJPreviewPane(String url) { this.url = url; } void stopVideo() { if (vt != null) { try { System.out.println("Stopping play"); vt.cancel(); bs = null; } catch (InterruptedException ex) { Logger.getLogger(VideoLibrary.class.getName()).log( Level.SEVERE, null, ex); } vt = null; } } void startVideo() { if (vt == null) { vt = new VideoThread(url) { @Override public void imageCreated(WritableImage image) { // getChildren().clear(); ImageView iv = new ImageView(image); getChildren().add(iv); iv.setOpacity(0); FadeTransition ft = new FadeTransition( Duration.seconds(0.5), iv); ft.setFromValue(0); ft.setToValue(1); ft.play(); ft.setOnFinished(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent t) { getChildren().remove(bs); bs = null; } }); } }; bs = new BusySpinner(); bs.setOpacity(0); FadeTransition ft = new FadeTransition(Duration.seconds(0.5), bs); ft.setFromValue(0); ft.setToValue(1); getChildren().add(bs); vt.start(); ft.play(); } } } /** * Uses the mediaplayer. But unfortunately that uses gst ... well enough * said. Managed to bring my whole machine to a halt in a few seconds. */ class MediaPreviewPane extends AsyncImagePane { Media media; MediaPlayer mp; String url; public MediaPreviewPane(String url) { this.url = "file://" + url; } void stopVideo() { if (mp != null) { mp.statusProperty().removeListener(mediaStatus); mp.stop(); mp = null; } } void startVideo() { if (mp == null) { try { media = new Media(url); mp = new MediaPlayer(media); mp.setMute(true); mp.statusProperty().addListener(mediaStatus); } catch (MediaException mx) { mp = null; } // getChildren().add(new BusySpinner()); } } ChangeListener<Status> mediaStatus = new ChangeListener<Status>() { @Override public void changed(ObservableValue<? extends Status> ov, Status t, Status t1) { System.out.println("media status " + t1); if (t1 == Status.READY) { getChildren().clear(); MediaView mview = new MediaView(mp); mview.setSmooth(true); mview.setFitWidth(256); mview.setFitHeight(144); mview.setPreserveRatio(true); getChildren().add(mview); mp.play(); } } }; } /** * Tracks which range of objects is visible and activates them */ class VisibilityManager implements Runnable, ChangeListener<Number>, EventHandler<ActionEvent> { private ScrollPane sp; private FlowPane fp; private final double iwidth; private final double iheight; private boolean pending = false; public VisibilityManager(double iwidth, double iheight) { this.iwidth = iwidth; this.iheight = iheight; } void connect(ScrollPane isp, FlowPane ifp) { sp = isp; fp = ifp; fp.heightProperty().addListener(this); fp.widthProperty().addListener(this); sp.viewportBoundsProperty().addListener( new ChangeListener<Bounds>() { @Override public void changed( ObservableValue<? extends Bounds> ov, Bounds t, Bounds t1) { fp.setPrefWidth(t1.getWidth()); } }); sp.vvalueProperty().addListener(this); } @Override public void run() { double top = sp.getVvalue(); double vheight = sp.getViewportBounds().getHeight(); double fheight = fp.getHeight(); double fwidth = sp.getViewportBounds().getWidth(); // Convert scroll position into cell coordinates // Number of cells visible in a row int cellw = (int) Math.floor(fwidth / iwidth); // ID of first visible cell int cell0 = (int) (Math .floor((top * (fheight - vheight)) / iheight)) * cellw; // ID of last visible cell + 1 int celln = (int) (Math.ceil((top * (fheight - vheight) + vheight) / iheight)) * cellw; ObservableList<Node> list = fp.getChildrenUnmodifiable(); int len = list.size(); for (int i = 0; i < len; i++) { AsyncImagePane pb = (AsyncImagePane) list.get(i); pb.setUserVisible(i >= cell0 && i < celln); } pending = false; } @Override public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) { if (!pending) { Platform.runLater(this); pending = true; } } /** * Invoked on transition completion - clear transition * * @param t */ @Override public void handle(ActionEvent t) { FadeTransition ft = (FadeTransition) t.getSource(); ft.getNode().setUserData(null); } } @Override public void stop() throws Exception { super.stop(); ObservableList<Node> list = flow.getChildrenUnmodifiable(); int len = list.size(); for (int i = 0; i < len; i++) { AsyncImagePane pb = (AsyncImagePane) list.get(i); pb.setUserVisible(false); } } }