1

I am developing an Electron application. In this application, I am spawning a Python process with a file's path as an argument, and the file itself is then passed to ffmpeg (through the ffmpeg-python module) and then goes through some Tensorflow functions.

I am trying to handle the case in which the user closes the Electron app while the whole background process is going. From my tests though, it seems like ffmpeg's process stays up no matter what. I'm on Windows and I'm looking at the task manager and I'm not sure what's going on: when closing the Electron app's window, sometimes ffmpeg.exe will be a single process, some other times it will stay in an Electron processes group.

I have noticed that if I kill Electron's process through closing the window, the python process will also close once ffmpeg has done its work, so I guess this is half-working. The problem is, ffmpeg is doing intensive stuff and if the user needs to close the window, then the ffmpeg process also NEEDS to be killed. But I can't achieve that in any way.

I have tried a couple things, so I'll paste some code:

main.js

// retrieve video data
ipcMain.handle('get-games', async (event, arg) => {
    const spawn = require('child_process').spawn;
    const pythonProcess = spawn('python', ["./backend/predict_games.py", arg]);

    // sets pythonProcess as a global variable to be accessed when quitting the app
    global.childProcess = pythonProcess;

    return new Promise((resolve, reject) => {
        let result = "";

        pythonProcess.stdout.on('data', async (data) => {
            data = String(data);

            if (data.startsWith("{"))
                result = JSON.parse(data);
        });

        pythonProcess.on('close', () => {
            resolve(result);
        })

        pythonProcess.on('error', (err) => {
            reject(err);
        });
    })
});

app.on('before-quit', function () {
    global.childProcess.kill('SIGINT');
});

predict_games.py (the ffmpeg part)

def convert_video_to_frames(fps, input_file):
    # a few useful directories
    local_dir = os.path.dirname(os.path.abspath(__file__))
    snapshots_dir = fr"{local_dir}/snapshots/{input_file.stem}"

    # creates snapshots folder if it doesn't exist
    Path(snapshots_dir).mkdir(parents=True, exist_ok=True)

print(f"Processing: {Path(fr'{input_file}')}")
try:
    (
        ffmpeg.input(Path(input_file))
        .filter("fps", fps=fps)
        .output(f"{snapshots_dir}/%d.jpg", s="426x240", start_number=0)
        .run(capture_stdout=True, capture_stderr=True)
    )
except ffmpeg.Error as e:
    print("stdout:", e.stdout.decode("utf8"))
    print("stderr:", e.stderr.decode("utf8"))

Does anyone have any clue?

Impasse
  • 85
  • 9
  • What you seem to need is a mechanism to kill the python process from electron. ffmpeg process should get killed along with it in the process. If not, you need to get the Fmpeg Popen instance and kill it when python receives the termination request from js. Finally why are you writing jpg file instead of getting data directly via stdout pipe? – kesh Apr 02 '22 at 12:02
  • @kesh Thanks for your reply, I'm not familiar at all with IPC so I'm not sure I'm getting you. So what you're saying is that I should somehow get the popen ffmpeg instance from the python script's side and then somehow kill the instance once Python receives the SIGINT/SIGTERM (not sure there's a difference) signal, is this correct? Do you have any resources as to how I could accomplish that? I'm writing jpg simply because that's the extension I need ffmpeg to give me, unless I'm missing something. – Impasse Apr 02 '22 at 20:20
  • 1
    The first thing I'd try is to call `pythonProcess.kill()` when user closes the window (browser-window's `close` event maybe? I haven't used electron in a while). This should also kill FFmpeg subprocess as well. If this doesn't work (`pythonProcess` dead but FFmpeg process alive) only then look into getting `Popen` back from `ffmpeg-python` (you need to look at its documentation on how to get the running process back) – kesh Apr 02 '22 at 20:57
  • re: jpg. well, yeah, I meant why aren't you getting frame data directly via stdout pipe, convert it to numpy array and feed it to Tensorflow. This should be a lot faster than writing frames to a disk. (this obviously isn't related to your question, I was just curious.) – kesh Apr 02 '22 at 21:00
  • 1
    oh, my bad, I'm blind as usual. Just now I saw your `before-quit` callback. Once you figure out how to get `Popen` object, see [this post](https://stackoverflow.com/q/1112343/4516027) to kill the FFmpeg process when Python exits. – kesh Apr 02 '22 at 21:25
  • I've tried the `pythonProcess.kill()` approach but sadly it doesn't work. I've tried a couple things and I realized that the process I'm killing has a different PID than ffmpeg's (comparing the .pid attribute with the pid on the task manager). When I close the Electron app I'm just killing the Python process, but the ffmpeg one just stays alive. Also I can't understand the pattern, but either the Electron process group stays open (with just ffmpeg.exe inside of it) or ffmpeg.exe appears as a process on its own (with the same PID as before). I honestly don't get it. – Impasse Apr 02 '22 at 21:32
  • @kesh No worries, honestly thanks a bunch for helping me out with this. I'll give your link a look when I wake up tomorrow, thank you again and have a great day / night! – Impasse Apr 02 '22 at 21:33
  • @kesh I finally found a solution and I put it as an answer! Also, to answer re: jpg. Honestly I hadn't thought of that, I'm also a beginner to TF so I guess that's somehow what I thought was the best solution at the time haha. Will modify it according to your suggestion in the near future, so thank you very much for the tip! – Impasse Apr 03 '22 at 08:51

1 Answers1

0

Alright, I was finally able to fix this! Since ffmpeg-python is just a collection of bindings for good old ffmpeg, the executable itself is still at the core of the module. This also means that when ffmpeg is running, a screen similar to this:

... 

    Metadata:
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 159 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
Stream mapping:
  Stream #0:0 (h264) -> fps:default
  fps:default -> Stream #0:0 (mjpeg)

...

Press [q] to stop, [?] for help

is what's going on behind the scenes. Once I realized this, all I had to do was find a way to send a 'q' to ffmpeg's stdin. And I did it by adding this snippet on the window-all-closed event:

app.on('window-all-closed', () => {
    // writes a 'q' in ffmpeg's terminal to quit ffmpeg's process
    // reminder: python gets closed when the Electron app is closed
    global.childProcess.stdin.write("q\n");

    if (process.platform !== 'darwin') app.quit()
})

The Python script itself is untouched compared to the snippet in the question, and this is the only thing I ended up modifying. Now everytime I quit my Electron app, ffmpeg gets sent a 'q'. The Python process doesn't need to be manually killed because Electron already does that for you.

So problem solved. :)

Impasse
  • 85
  • 9