Search code examples
javaeclipsevideoffmpegruntime

Runtime.exec() freezes on execution of ffmpeg Audio-replacing command that works in windows console


I have been building an application to merge together videos based on data I got from the web. I was lucky enough to find this great tool ffmpeg that has saved me a lot of time and effort, but unfortunately I seem to be stuck on the following issue:

Im attempting to execute a command for merging a video with audio (in the future I will also be adding hardcoded .ASS subtitles and .PNG images). My application dynamically generates me a command based on the files its suppposed to use which works just as intended, as the command that I get works inside of my windows console (cmd). But when I try to run it through my Java application, the code seems to "freeze" permanently and while the placeholder file gets created on my desktop with the filename and correct file format, it just has the default windows video icon and cant be opened (as it has not finished being created, duh). Im also using Eclipse, perhaps thats worth noting.

This is the part of the code where it freezes:

protected void runFFMPEG(Multimap<String,String> args, String outputFile, String fileFormat) throws IOException
{
    try
    {
        Runtime.getRuntime().exec(setCommand(args, outputFile, fileFormat)).waitFor();
    }
    catch (InterruptedException e)
    {
        e.printStackTrace();
    }
}

as previously said, the command itself is generated correctly, but heres the code nonetheless:

private String[] setCommand(Multimap<String, String> args, String outputFile, String fileFormat)
{
    String[] command = new String[args.size()*2+2];
    command[0] = ffmpegLocation;
    int[] i = new int[1];
    i[0] = 1;
    args.forEach((key, value) -> { 
        command[i[0]++] = key;
        command[i[0]++] = value;
    });
    command[i[0]] = outputFile + fileFormat;
    return command;
}

Note: I used a normal string previously for command, but I saw another thread saying that using an array instead fixxed their issue (which it didnt for me, else i wouldnt be here...).

This is an example command that gets generated:

ffmpeg.exe -ss 00:00:00 -to 00:00:15 -i C:\Users\Test.mp4 -i C:\Users\FINAL.mp3 -map 0 -map 1:a -c:v copy C:/Users/User/TestVideo.mp4

I also have a different command that concats the audio together into one FINAL.mp3 file, so its not like ffmpeg entirely fails to work in my application. My guess is that it has something to do with the Runtime i get with Runtime.getRunTime(), but I cant seem to find out what the problem is. Can someone tell me what I did wrong? Thanks in advance!


Solution

  • To reliably start a sub-process you need to handle the output streams or the process will block. Use ProcessBuilder to have better control of where output streams go - including redirect to File.

    If a sub-process writes to stdout or stderr and that buffer fills because it isn't read, the process simply blocks at next write. This can affect either stream (and even stdin if you supply it). Most common examples on Stackoverflow is when stdout is read before stderr - and so large message sent to stderr blocks the process, and therefore blocks the stdout reader too.

    Here is a simple example of how to handle launch which could be used in place of Runtime.exec() and sends STDOUT(/STDERR) to any stream you pass in:

    // Example: start(yourCmd, null, System.out, null);
    // or start(yourCmd, null, System.out, System.err);
    // Don't forget to close streams you pass in - if appropriate
    
    public static int start(String[] cmd, byte[] stdin, OutputStream stdout, OutputStream stderr)
            throws IOException, InterruptedException
    {
        Objects.requireNonNull(cmd);
        Objects.requireNonNull(stdout);
        System.out.println("start "+Arrays.toString(cmd));
    
        // Launch and wait:
        ProcessBuilder pb = new ProcessBuilder(cmd);
        if (stderr == null) {
            pb.redirectErrorStream(true);   // No STDERR => merge to STDOUT
        }
    
        Process p = pb.start();
    
        // Consumes STDERR at same time as STDOUT, not doing this large streams can block I/O in the sub-process
        Thread bg = null;
        if (stderr != null) {
            Runnable task = () -> {
                try(var from = p.getErrorStream()) {
                    from.transferTo(stderr);
                } catch(IOException io) {
                    throw new UncheckedIOException(io);
                }
            };
            bg = new Thread(task, "STDERR");
            bg.start();
        }
    
        // Send STDIN if required, and close STDIN stream
        // NOTE!!! a huge input stream can lock up STDOUT/STDERR readers, you may need a background thread here too
        try(OutputStream os = p.getOutputStream()) {
            if (stdin != null) os.write(stdin);
        }
    
        // Move STDOUT to the output stream
        try(var stdo = p.getInputStream()) {
            stdo.transferTo(stdout);
        }
    
        int rc = p.waitFor();
        if (bg != null) {
            bg.join();
        }
    
        System.out.println("start "+Arrays.toString(cmd));
        System.out.println("Exit "+p.pid()+" CODE "+rc +' '+(rc == 0 ? "OK":"**** ERROR ****")+" "+(stderr == null ? "STDERR>OUT":""));
        return rc;
    }