简体   繁体   English

有没有办法将 JavaFX animation 导出到图像帧?

[英]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)制作 JavaFX animation 最终将在 video.mp4 (或任何其他扩展名)中进行协调

One way to do it:一种方法:
During the animation plays, export 30fps (or 60fps) as still images (ie png images).在 animation 播放期间,将 30fps(或 60fps)导出为静止图像(即 png 图像)。 After that process the images with some tool and create video.之后使用一些工具处理图像并创建视频。

So how to create the frames of a JavaFX window?那么如何创建 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.以下解决方案仅作为概念证明提供,没有附带解释,没有评论支持,不保证它会为您工作,不保证它不会出现错误(它可能有一些小错误),并且没有promise 适用于任何用途。

Solution Strategy解决方案策略

Capture uses techniques suggested in comments by James and mipa: Capture 使用 James 和 mipa 评论中建议的技术:

Create a Scene which is not displayed in a window containing your animation.创建一个未显示在包含 animation 的 window 中的场景。 Don't play() the animation, but repeatedly call jumpTo(...) on the animation to create each frame, snapshot the result, and write to a file.不要 play() animation,而是重复调用 animation 上的 jumpTo(...) 来创建每一帧,对结果进行快照,然后写入文件。

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). JPEG 是一种有损编码方法,此示例的 output 有点模糊(可能是由于可以调整的默认 ImageIO 设置)。

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.如果需要,可以将每帧 output 转换为无损格式(如 png)的单独文件,而不是 mjpeg,然后通过第三方处理软件运行以创建另一种视频格式,如 mp4。

MjpegCaptureAndPlayApp.java 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 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 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视频流.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视频流输出.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模块信息.java

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

    exports com.example.mjpeg;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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