CasualDemon
CasualDemon

Reputation: 6160

Pause a FFmpeg encoding in a Python Popen subprocess on Windows

I am trying to pause an encode of FFmpeg while it is in a non-shell subprocess (This is important to how it plays into a larger program). This can be done by presssing the "Pause / Break" key on the keyboard by itself, and I am trying to send that to Popen.

The command itself must be cross platform compatible, so I cannot wrap it in any way, but I can send signals or run functions that are platform specific as needed.

I looked at how to send a "Ctrl+Break" to a subprocess via pid or handler and it suggested to send a signal, but that raised a "ValueError: Unsupported signal: 21"

from subprocess import Popen, PIPE
import signal


if __name__ == '__main__':
    command = "ffmpeg.exe -y -i example_video.mkv -map 0:v -c:v libx265 -preset slow -crf 18 output.mkv"
    proc = Popen(command, stdin=PIPE, shell=False)

    try:
        proc.send_signal(signal.SIGBREAK)
    finally:
        proc.wait()

Then attempted to use GenerateConsoleCtrlEvent to create a Ctrl+Break event as described here https://learn.microsoft.com/en-us/windows/console/generateconsolectrlevent

from subprocess import Popen, PIPE
import ctypes


if __name__ == '__main__':
    command = "ffmpeg.exe -y -i example_video.mkv -map 0:v -c:v libx265 -preset slow -crf 18 output.mkv"
    proc = Popen(command, stdin=PIPE, shell=False)

    try:
        ctypes.windll.kernel32.GenerateConsoleCtrlEvent(1, proc.pid)
    finally:
        proc.wait()

I have tried psutil pause feature, but it keeps the CPU load really high even when "paused".

Even though it wouldn't work with the program overall, I have at least tried setting creationflags=CREATE_NEW_PROCESS_GROUP which makes the SIGBREAK not error, but also not pause it. For the Ctrl-Break event will entirely stop the encode instead of pausing it.

Upvotes: 6

Views: 832

Answers (1)

Roman Pavelka
Roman Pavelka

Reputation: 4171

Linux/Unix solution:

import subprocess, os, signal
# ...
# Start the task:
proc = subprocess.Popen(..., start_new_session=True)
# ...
def send_signal_to_task(pid, signal):
        #gpid = os.getpgid(pid)  # WARNING This does not work
        gpid = pid  # But this does!
        print(f"Sending {signal} to process group {gpid}...")
        os.killpg(gpid, signal)
# ...
# Pause and resume:
send_signal_to_task(proc.pid, signal.SIGSTOP)
send_signal_to_task(proc.pid, signal.SIGCONT)

Notice start_new_session=True in Popen call and using of os.killpg instead of os.kill in send_signal_to_task function. The reason for this is the same as as reason for why you have large CPU usage even in paused state as you reported. ffmpeg spawns a number of child processes. start_new_session=True will create a new process group and os.killpg sends signal to all processes in group.

Cross-platform solution is supposed to be provided by psutil module but you probably should research whether it supports process groups as the variant above:

>>> import psutil
>>> psutil.pids()
[1, 2, 3, 4, 5, 6, 7, 46, 48, 50, 51, 178, 182, 222, 223, 224, 268, 1215,
 1216, 1220, 1221, 1243, 1244, 1301, 1601, 2237, 2355, 2637, 2774, 3932,
 4176, 4177, 4185, 4187, 4189, 4225, 4243, 4245, 4263, 4282, 4306, 4311,
 4312, 4313, 4314, 4337, 4339, 4357, 4358, 4363, 4383, 4395, 4408, 4433,
 4443, 4445, 4446, 5167, 5234, 5235, 5252, 5318, 5424, 5644, 6987, 7054,
 7055, 7071]
>>> p = psutil.Process(7055)
>>> p.suspend()
>>> p.resume()

Reference: https://pypi.org/project/psutil/

Some pointers on emulating process groups in Windows: Popen waiting for child process even when the immediate child has terminated

Upvotes: 2

Related Questions