java - 有没有办法将 JavaFX 动画导出到图像帧?

标签 java javafx video ffmpeg video-capture

我想要完成的事情:
制作最终会在 video.mp4(或任何其他扩展名)中协调的 JavaFX 动画
一种方法:
在动画播放期间,将 30fps(或 60fps)导出为静止图像(即 png 图像)。之后使用一些工具处理图像并创建视频。
那么如何创建 JavaFX 窗口的框架呢?

最佳答案

免责声明
以下解决方案仅作为概念证明提供,不附带解释,不支持评论,不保证它会为您工作,不保证它会没有错误(它可能有一些小错误),并且没有 promise 它将适用于任何目的。
解决方案策略

  • 创建动画。
  • 将其捕获到 mjpeg 文件中。
  • Play back the mjpeg file .

  • Capture 使用 James 和 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 是一种有损编码方法,此示例的输出有点模糊(可能是由于可以调整的默认 ImageIO 设置)。
    如果需要,可以将每一帧以无损格式(如 png)输出到单独的文件,而不是 mjpeg,然后通过第三方处理软件运行以创建另一种视频格式,如 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;
        }
    }
    
    视频流.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();
        }
    }
    
    视频流输出.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();
        }
    }
    
    模块信息.java
    module com.example.mjpeg {
        requires javafx.controls;
        requires javafx.swing;
        requires java.desktop;
    
        exports com.example.mjpeg;
    }
    

    关于java - 有没有办法将 JavaFX 动画导出到图像帧?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70152779/

    相关文章:

    HTML5 视频色差 Chrome 和 Internet Explorer

    c# - C#Windows Phone-结合音频和视频文件

    java - Thymeleaf 无法检测到 spring-boot 项目中的模板

    java - Spring循环依赖,

    Java FX 线程安全暂停/hibernate

    regex - 处理 JavaFX 数值 TextField 中的无穷大

    java - 从视频的音频流生成波形图像

    java - Trove jar THashMap 类未找到异常

    java - wait()方法对java同步块(synchronized block)中线程的影响

    JAVA - 如何确保在执行器开始阻塞 UI 线程之前隐藏节点