简体   繁体   中英

Is there any way to export a JavaFX animation to image frames?

What I want to accomplish:
Make a JavaFX animation that would eventually concertize in a video.mp4 (or any other extension)

One way to do it:
During the animation plays, export 30fps (or 60fps) as still images (ie png images). After that process the images with some tool and create video.

So how to create the frames of a JavaFX window?

Disclaimer

The following solution is just offered as a proof of concept, with no accompanying explanation, no support in comments, no warranty that it will work for you, no guarantee that it will be bug free (it probably has some small errors), and no promise that it will be fit for any purpose.

Solution Strategy

Capture uses techniques suggested in comments by James and mipa:

Create a Scene which is not displayed in a window containing your animation. Don't play() the animation, but repeatedly call jumpTo(...) on the animation to create each frame, snapshot the result, and write to a file.

JPEG is a lossy encoding method and the output of this example is a bit fuzzy (likely due to default ImageIO settings which could be tweaked).

If desired, instead of mjpeg, each frame could be output to a separate file in a lossless format (like png), and then run through third party processing software to create another video format such as mp4.

MjpegCaptureAndPlayApp.java

package com.example.mjpeg;

import javafx.animation.Animation;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.nio.file.Files;

public class MjpegCaptureAndPlayApp extends Application {
    final private double W = 100, H = 100;

    private MjpegPlayer player;

    @Override
    public void start(Stage stage) throws Exception {
        String movieFile = Files.createTempFile("mjpeg-capture", "mjpeg").toString();

        CaptureAnimation captureAnimation = createCaptureAnimation();
        MjpegCapture mjpegCapture = new MjpegCapture(
                movieFile,
                captureAnimation.root(),
                captureAnimation.animation()
        );
        mjpegCapture.capture();

        player = new MjpegPlayer(movieFile);
        StackPane viewer = new StackPane(player.getViewer());
        viewer.setPrefSize(W, H);

        VBox layout = new VBox(20);
        layout.setStyle("-fx-background-color: cornsilk;");
        layout.setPadding(new Insets(10));
        layout.setAlignment(Pos.CENTER);

        layout.getChildren().setAll(
                viewer,
                player.getControls()
        );

        stage.setScene(new Scene(layout));
        stage.show();

        player.getTimeline().playFromStart();
    }

    @Override
    public void stop() throws Exception {
        if (player != null) {
            player.dispose();
        }
    }

    record CaptureAnimation(Parent root, Animation animation) {}

    private CaptureAnimation createCaptureAnimation() {
        Pane root = new Pane();
        root.setMinSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
        root.setPrefSize(W, H);
        root.setMaxSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);

        Circle circle = new Circle(W / 10, Color.PURPLE);
        root.getChildren().add(circle);

        TranslateTransition translateTransition = new TranslateTransition(
                Duration.seconds(5),
                circle
        );
        translateTransition.setFromX(0);
        translateTransition.setToX(W);
        translateTransition.setFromY(H/2);
        translateTransition.setToY(H/2);
        translateTransition.setAutoReverse(true);
        translateTransition.setCycleCount(2);

        // move to start pos.
        circle.setTranslateX(0);
        circle.setTranslateY(H/2);

        return new CaptureAnimation(root, translateTransition);
    }

    public static void main(String[] args) {
        launch(args);
    }
}

MjpegCapture.java

package com.example.mjpeg;

import javafx.animation.Animation;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.util.Duration;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class MjpegCapture {

    private final Duration SECS_PER_FRAME = Duration.seconds(1.0 / 24);

    private final String videoFilename;
    private final Parent root;
    private final Animation animation;

    public MjpegCapture(String videoFilename, Parent root, Animation animation) {
        this.videoFilename = videoFilename;
        this.root = root;
        this.animation = animation;
    }

    public void capture() throws IOException {
        VideoStreamOutput videoStreamOutput = new VideoStreamOutput(videoFilename);

        animation.playFromStart();
        Duration curPos = Duration.ZERO;

        SnapshotParameters snapshotParameters = new SnapshotParameters();
        // transparent fill not supported by jpeg I believe so not enabled.
        //snapshotParameters.setFill(Color.TRANSPARENT);

        Scene scene = new Scene(root);

        // force a layout pass so that we can measure the height and width of the root node.
        scene.snapshot(null);
        int w = (int) scene.getWidth();
        int h = (int) scene.getHeight();
        WritableImage fxImage = new WritableImage(w, h);

        boolean complete;
        ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();
        do {
            animation.jumpTo(curPos);
            root.snapshot(snapshotParameters, fxImage);

            // Get buffered image:
            // uses ugly, inefficient workaround from:
            //   https://stackoverflow.com/a/19605733/1155209
            BufferedImage image = SwingFXUtils.fromFXImage(fxImage, null);

            // Remove alpha-channel from buffered image:
            BufferedImage imageRGB = new BufferedImage(
                    image.getWidth(),
                    image.getHeight(),
                    BufferedImage.OPAQUE);

            Graphics2D graphics = imageRGB.createGraphics();
            graphics.drawImage(image, 0, 0, null);
            ImageIO.write(imageRGB, "jpg", outputBuffer);

            videoStreamOutput.writeNextFrame(outputBuffer.toByteArray());
            outputBuffer.reset();

            complete = curPos.greaterThanOrEqualTo(animation.getTotalDuration());

            if (curPos.add(SECS_PER_FRAME).greaterThan(animation.getTotalDuration())) {
                curPos = animation.getTotalDuration();
            } else {
                curPos = curPos.add(SECS_PER_FRAME);
            }
        } while(!complete);

        videoStreamOutput.close();
    }
}

MjpegPlayer.java

package com.example.mjpeg;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.util.Duration;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;

public class MjpegPlayer {

    private final String videoFilename;
    private final Timeline timeline;
    private final ImageView viewer = new ImageView();
    private final HBox controls;
    private VideoStream videoStream;

    public MjpegPlayer(String filename) throws FileNotFoundException {
        videoFilename = filename;
        videoStream = new VideoStream(filename);
        timeline = createTimeline(viewer);
        controls = createControls(timeline);
    }

    private Timeline createTimeline(ImageView viewer) {
        final Timeline timeline = new Timeline();
        final byte[] buf = new byte[15000];

        timeline.getKeyFrames().setAll(
                new KeyFrame(Duration.ZERO, event -> {
                    try {
                        int len = videoStream.readNextFrame(buf);
                        if (len == -1) {
                            timeline.stop();
                            return;
                        }
                        viewer.setImage(
                                new Image(
                                        new ByteArrayInputStream(
                                                Arrays.copyOf(buf, len)
                                        )
                                )
                        );
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }),
                new KeyFrame(Duration.seconds(1.0 / 24))
        );
        timeline.setCycleCount(Timeline.INDEFINITE);

        return timeline;
    }

    private HBox createControls(final Timeline timeline) {
        Button play = new Button("Play");
        play.setOnAction(event -> timeline.play());

        Button pause = new Button("Pause");
        pause.setOnAction(event -> timeline.pause());

        Button restart = new Button("Restart");
        restart.setOnAction(event -> {
            try {
                timeline.stop();
                videoStream = new VideoStream(videoFilename);
                timeline.playFromStart();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        HBox controls = new HBox(10);
        controls.setAlignment(Pos.CENTER);
        controls.getChildren().setAll(
                play,
                pause,
                restart
        );

        return controls;
    }

    public void dispose() throws IOException {
        videoStream.close();
    }

    public String getVideoFilename() {
        return videoFilename;
    }

    public Timeline getTimeline() {
        return timeline;
    }

    public ImageView getViewer() {
        return viewer;
    }

    public HBox getControls() {
        return controls;
    }
}

VideoStream.java

package com.example.mjpeg;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

class VideoStream {

    private final FileInputStream fis; //video file

    VideoStream(String filename) throws FileNotFoundException {
        fis = new FileInputStream(filename);
    }

    int readNextFrame(byte[] frame) throws Exception {
        int length;
        String lengthAsString;
        byte[] lengthAsBytes = new byte[5];

        //read current frame length
        fis.read(lengthAsBytes, 0, 5);

        //transform lengthAsBytes to integer
        lengthAsString = new String(lengthAsBytes);
        try {
            length = Integer.parseInt(lengthAsString);
        } catch (Exception e) {
            return -1;
        }

        return (fis.read(frame, 0, length));
    }

    void close() throws IOException {
        fis.close();
    }
}

VideoStreamOutput.java

package com.example.mjpeg;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

class VideoStreamOutput {
    private FileOutputStream fos; //video file
    private int frameNum; //current frame nb

    public VideoStreamOutput(String filename) throws FileNotFoundException {
        fos = new FileOutputStream(filename);
        frameNum = 0;
    }

    public void writeNextFrame(byte[] frame) throws IOException {
        frameNum++;

        String lengthAsString = String.format("%05d", frame.length);
        byte[] lengthAsBytes = lengthAsString.getBytes(StandardCharsets.US_ASCII);

        fos.write(lengthAsBytes);
        fos.write(frame);

        System.out.println(frameNum + ": " + lengthAsString);
    }

    public void close() throws IOException {
        fos.flush();
        fos.close();
    }
}

module-info.java

module com.example.mjpeg {
    requires javafx.controls;
    requires javafx.swing;
    requires java.desktop;

    exports com.example.mjpeg;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM