javamultithreadingjavafxffmpegprocess

FFmpeg process finishes writing the file only after closing the program


My JavaFX application uses FFmpeg to extract the first frame from a video file in a separate thread and the process ends with a wait. The image is saved to a temporary directory (temp/video_preview.jpg) but becomes available only after the forced termination of the entire program. I need the image for display in ImageView immediately after the FFmpeg process is completed.

@FXML
private void onDragOver(DragEvent event) {
    if (event.getDragboard().hasFiles()) {
        event.acceptTransferModes(TransferMode.ANY);
    }
}

@FXML
private void onDragDropped(DragEvent event) {
    Dragboard db = event.getDragboard();
    boolean success = false;
    if (db.hasFiles()) {
        List<File> files = db.getFiles();
        File videoFile = files.get(0);
        Image image = extractFirstFrameFromVideo(videoFile.getAbsolutePath());
        mainImage.setImage(image);
        success = true;
    }
    event.setDropCompleted(success);
    event.consume();
}
private Image extractFirstFrameFromVideo(String pathToVideo) {
    try {
        ProcessBuilder processBuilder = new ProcessBuilder(
                ffmpegPath,
                "-y",
                "-ss", "00:00:00",
                "-i", pathToVideo,
                "-frames:v", "1",
                outputFilePath  // "temp/video_preview.jpg"
        );

        Process process = processBuilder.start();

        int exitCode = process.exitValue();
        if (exitCode != 0) {
            throw new IOException("ffmpeg process exited with error code: " + exitCode);
        }

        return new Image(new File(outputFilePath).toURI().toString());

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

Solution

  • Your problem is likely that you aren't 'draining' ffmpeg's output.

    All processes get 'pipes' hooked up to them. There are by default 3: 'in', 'out', and 'err'. You might think that there's simply 'keyboard input' and 'output to screen', but that's not actually how it works. Instead, if you start an application on the command line, then bash (or another terminal app on posix systems, or cmd.exe or powershell or whatever on windows) reads in what you typed, parses that into a ProcessBuilder-like execution plan, hooks up its own terminal and its own keyboard as in, out, and err to it, and fires it up.

    Your problem is that you have hooked up nothing to in, out, and err. These are 'stream' concepts - out and err are like buckets you place at the end of a pipe for water, to catch the water. What 'the bucket' does with the water (bytes sent by the process to sysout or syserr) has no further bearing on ffmpeg. It doesn't care. But it does care that the buckets are there, and are not full.

    Sysin is a bit different - that's more a thing where it is a bucket, and it's looking for the waterpipe that drips into it. You haven't installed that water pipe either.

    Without any buckets hooked up, your ffmpeg process will wait, because, it's going with: "Uhoh, the bucket is full so I have to wait before I send any more water there" (no bucket is treated as a full bucket), or possibly "Uhoh, I need some water, or at least, be ready to receive some water, and no waterline drips into my bucket yet so I shall wait".

    You need to hook up the plumbing. You do this with ProcessBuilder's various set methods. To keep it as simple as possible, you can ask PB to just hook it to your java's process's sysin, sysout, and syserr:

    // replace `pb.start()` with:
    pb.inheritIO().start();
    

    But, instead, you might want to explicitly define the targets of this stuff, that way, you can actually check what ffmpeg is reporting and write code that responds to it. To do that, remove the inheritIO stuff and go with:

    Process p = pb.start();
    OutputStream out = p.getOutputStream(); // you can send stuff to ffmpeg with this
    InputStream err = p.getErrorStream(); // errors arrive here
    InputStream in = p.getInputStream();  // normal output arrives here
    

    And you will have to read both err and in - i.e. actually invoke .read() or some variant of it, or ffmpeg will stop running because its waiting for you to read this data. Generally, this requires threads.