Search code examples
pythonmultithreadingtkinterffmpegffmpeg-python

How to multithread ffmpeg-python process?


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()

Solution

  • 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.