How to multithread ffmpeg-python process?

Question:

I’m working on a cancel button that cancels the ffmpeg video & audio merging process when pressed.
But while loop doesn’t keep looping once ffmpeg starts execution, while loop continues to loop after ffmpeg finished the process. I couldn’t really figure it out, sorry if it’s duplicate.

I know the code looks really silly but I’m kinda doomed, any help will be greatly appreciated. Thanks in advance.

from tkinter import *
import ffmpeg
import threading

def start_ffmpeg_thread(audio_part, video_part, path):
    threading.Thread(target=start_ffmpeg, args=(audio_part, video_part, path)).start()

def start_ffmpeg(audio_part, video_part, path):
    while True:
        if is_cancelled:
            break
        threading.Thread(target=ffmpeg_func, args=(audio_part, video_part, path)).start()

def ffmpeg_func(audio_part, video_part, path):
    ffmpeg.output(audio_part, video_part, path).run(overwrite_output=True)

def cancel_ffmpeg():
    global is_cancelled
    is_cancelled = True


is_cancelled = False

root = Tk()

video_part = ffmpeg.input("<path_video_part>")
audio_part = ffmpeg.input("<path_audio_part>")
path = "<path>"

button_1 = Button(root, text="Start", command=lambda: start_ffmpeg_thread(audio_part, video_part, path))
button_1.pack(pady=30, padx=30)

button_2 = Button(root, text="Stop", command=cancel_ffmpeg)
button_2.pack(pady=30, padx=30)

root.mainloop()
Asked By: duruburak

||

Answers:

For gracefully closing FFmpeg sub-process, we may write 'q' to stdin pipe of FFmpeg sub-process as described in my following answer.

In order to have it possible to write 'q', we may execute FFmpeg sub-process as follows:

ffmpeg_process = ffmpeg.output(audio_part, video_part, path).overwrite_output().run_async(pipe_stdin=True)

ffmpeg_process allows us to access the sub-process and terminate it.
For keeping the code simple, we may declare ffmpeg_process as global variable.

Updated ffmpeg_func method:

def ffmpeg_func(audio_part, video_part, path):
    global ffmpeg_process
    ffmpeg_process = ffmpeg.output(audio_part, video_part, path).overwrite_output().run_async(pipe_stdin=True)

Updated start_ffmpeg method:

def start_ffmpeg(audio_part, video_part, path):
    global ffmpeg_process

    # Allow only one instance of FFmpeg to be executed - ffmpeg_process = None in the first time, and ffmpeg_process.poll() is not None when FFmpeg is not running
    if (ffmpeg_process is None) or ffmpeg_process.poll():
        threading.Thread(target=ffmpeg_func, args=(audio_part, video_part, path)).start()

The above code allows executing FFmpeg only if it’s not running.


Updated cancel_ffmpeg method:

def cancel_ffmpeg():
    global ffmpeg_process

    #Check if FFmpeg sub-process is running
    if (ffmpeg_process is not None) and (ffmpeg_process.poll() is None):
        # Terminate FFmpeg gracefully
        ffmpeg_process.stdin.write('q'.encode("GBK"))  # Simulate user pressing 'q' key
        ffmpeg_process.communicate()
        ffmpeg_process.wait()
        ffmpeg_process = None

Note:

Your implementation of start_ffmpeg executes threading.Thread in a loop, and that executes FFmpeg sub-process in a loop.

def start_ffmpeg(audio_part, video_part, path):
    while True:
        if is_cancelled:
            break
        threading.Thread(target=ffmpeg_func, args=(audio_part, video_part, path)).start()

We have to remove the while True from the above method.


Updated code sample:

from tkinter import *
import ffmpeg
import threading

def start_ffmpeg_thread(audio_part, video_part, path):
    threading.Thread(target=start_ffmpeg, args=(audio_part, video_part, path)).start()

def start_ffmpeg(audio_part, video_part, path):
    #while True:
    #    if is_cancelled:
    #        break
    global ffmpeg_process

    # Allow only one instance of FFmpeg to be executed - ffmpeg_process = None in the first time, and ffmpeg_process.poll() is not None when FFmpeg is not running
    if (ffmpeg_process is None) or ffmpeg_process.poll():
        threading.Thread(target=ffmpeg_func, args=(audio_part, video_part, path)).start()

def ffmpeg_func(audio_part, video_part, path):
    global ffmpeg_process
    #ffmpeg.output(audio_part, video_part, path).run(overwrite_output=True)
    ffmpeg_process = ffmpeg.output(audio_part, video_part, path).overwrite_output().run_async(pipe_stdin=True)
    

def cancel_ffmpeg():
    #global is_cancelled
    #is_cancelled = True
    global ffmpeg_process

    #Check if FFmpeg sub-process is running
    if (ffmpeg_process is not None) and (ffmpeg_process.poll() is None):
        # Terminate FFmpeg gracefully
        ffmpeg_process.stdin.write('q'.encode("GBK"))  # Simulate user pressing 'q' key
        ffmpeg_process.communicate()
        ffmpeg_process.wait()
        ffmpeg_process = None

#is_cancelled = False
ffmpeg_process = None

root = Tk()

video_part = ffmpeg.input("bunny_1080p_60fps.mp4")
audio_part = ffmpeg.input("bunny_1080p_60fps.mp4")
path = "output.mp4"

button_1 = Button(root, text="Start", command=lambda: start_ffmpeg_thread(audio_part, video_part, path))
button_1.pack(pady=30, padx=30)

button_2 = Button(root, text="Stop", command=cancel_ffmpeg)
button_2.pack(pady=30, padx=30)

root.mainloop()

In case you are planning to allow multiple instances of FFmpeg sub-processes, you may insert the ffmpeg_process into a list, for keeping track over all the sub-processes.
For closing all the sub-processes at once, just iterate the list.

Answered By: Rotem