You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程并发执行ffmpeg子进程时Python程序无法正常终止的问题求助

多线程并发执行ffmpeg子进程时Python程序无法正常终止的问题求助

我基于ffmpeg的ffplay封装了一个Python GUI媒体播放器,其中用到了回放增益(replay gain)来统一音乐音量。但因为没找到能计算回放增益的共享库,我只能通过subprocess启动ffmpeg子进程,从它的stderr输出中获取回放增益数据。

我给每个音乐文件单独开一个线程来执行ffmpeg子进程——如果在GUI主线程里直接跑这些子进程,会导致播放时出现短暂卡顿(播放器本身也在独立线程中运行)。但我发现当同时运行2个及以上的ffmpeg子进程线程时,Python GUI无法正常终止。

我先解释下“无法正常终止”的具体表现:我用Geany作为IDE,它会在终端窗口中运行程序。正常终止时,终端会显示(program exited with code: 0) Press return to continue.,按Enter就能直接关闭窗口;但出问题时,同样的提示会出现,但按Enter窗口不会关闭,必须点击左上角的X或者按Ctrl+C才能关掉。

我用定时器来创建并启动线程,在我的系统上,单个线程大概2秒就能完成。如果把定时器间隔设为3秒或更长,程序就能正常终止;但如果把间隔缩短到2秒以内(我现在设的是100毫秒,而且因为各种原因不想用更长的间隔),就会出现上述问题。

我把GUI精简成了最小示例,这个例子没有播放器,只是一个会启动一系列线程(每个线程都运行ffmpeg子进程)的小部件。我当前的环境是Python 3.12、PyQt 5.15、Linux Mint 22.1,ffmpeg版本是7.1。

以下是示例代码:

import sys, subprocess, queue, threading
from PyQt5 import QtWidgets, QtGui, QtCore

class AudioPlayerDialog(QtWidgets.QDialog):
    def __init__(self, mediaList):
        super().__init__()
        vbox = QtWidgets.QVBoxLayout()
        vbox.addWidget(QtWidgets.QLabel('WIDGET'))
        self.setLayout(vbox)
        self.calcGainTimer = None
        self.gdCalcThreads = None
        self.finished.connect(self.closeEvent)

        self.mediaList = mediaList
        self.mediaListLen = len(mediaList)
        self.normGainList = [-1. for x in mediaList]
        self.buildNormGainList()

    def closeEvent(self, event):
        print('**** close event')
        if self.calcGainTimer is not None:
            print('**** stopping calcGainTimer')
            self.calcGainTimer.stop()
        if self.gdCalcThreads is not None:
            print('**** checking gain calc threads')
            for idx, t in self.gdCalcThreads.copy().items():
                if t.is_alive():
                    print('**** waiting for thread {} to finish'.format(idx))
                    t.join()
        print('**** closing ...')

    def buildNormGainList(self):
        self.calcGainQueue = queue.LifoQueue(maxsize=self.mediaListLen + 10)
        for i in range(self.mediaListLen-1, -1, -1):
            self.calcGainQueue.put_nowait(i)
        # setup gain/dur calculation thread dictionary
        self.gdCalcThreads = dict()
        # For each expiration of the following timer, a thread will be
        # started to calculate the replay gain for a particular file
        # and a check will determine if all files have been processed.
        print('@@@@ starting updt timer')
        self.calcGainTimer = QtCore.QTimer()
        self.calcGainTimer.timeout.connect(self.stopGainDurCalcWhenDone)
        self.calcGainTimer.setInterval(100) # interval in msec.
        self.calcGainTimer.start()

    def stopGainDurCalcWhenDone(self):
        # this is the target of the updt timer
        try:
            # start a thread if the queue is not empty
            idx = self.calcGainQueue.get_nowait()
            if idx not in self.gdCalcThreads:
                print('@@@@ creating thread for', idx)
                thread = threading.Thread(None, self.requestGain, args=(idx,))
                self.gdCalcThreads[idx] = thread
                thread.start()
        except queue.Empty:
            pass
        except Exception as err:
            print('**** ERROR: get request from calcGainQueue failed.\n\t'\
                                                        '{}'.format(err))
        # check if any thread finished but updates not yet finished
        for idx,t in self.gdCalcThreads.items():
            if not t.is_alive() and self.normGainList[idx]  < 0.:
                # Thread finished; update not yet completed. Invert the
                # sign of the gain, which indicates update completed.
                self.normGainList[idx] = -self.normGainList[idx]
                print('@@@@ update finished for', idx)

        # check if all files have been processed; stop updt timer if so
        done = True     # assume all calc done
        for i in range(self.mediaListLen):
            if self.normGainList[i] < 0.:
                done = False
                break
        if done:
            print('@@@@ all calc are done -----------------------')
            # all files have been processed
            print('@@@@ stopping updt timer')
            self.calcGainTimer.stop()
            self.calcGainTimer = None
            self.gdCalcThreads = None

    TEST = False  # set True to pass dummy filename of ffmpeg

    def requestGain(self, idx):
        print('@@@@ start subprocess for', idx)
        if self.TEST:
            fileName = 'XXXX'
        else:
            fileName = self.mediaList[idx]
        cmd = ['ffmpeg', '-i', '{}'.format(fileName), '-hide_banner',
               '-af', 'replaygain', '-f', 'null', '-']
        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        try:
            _,err = proc.communicate(timeout=30)
            text = err.decode('utf8')
        except subprocess.TimeoutExpired:
            print('**** Wait for report from subprocess expired')
            proc.kill()
            text = ''
        except Exception as err:
           print('**** Unable to get report from subprocess: {}'.format(err))
           proc.kill()
           text = ''
        # actual program parses gain from text
        gain = 1.
        self.normGainList[idx] = -gain  # save negative gain


if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)

    # put full-qualified filenames of media in list
    mediaList = [
        ]

    apd = AudioPlayerDialog(mediaList)
    apd.exec()

    print('\nFINI')
    sys.exit()

我尝试过不给子进程设置PIPE,问题依然存在。另外,我把requestGain方法里的TEST设为True时,会给ffmpeg传一个无效的文件名XXXX,这样线程会快速结束,不会出现并发的情况,程序就能正常终止——这也验证了问题确实出在多个ffmpeg子进程线程并发运行的场景下。

我还做了这些排查:

  • 用top监控进程:所有ffmpeg子进程都能正常结束,最后只剩Python进程和终端窗口。但程序退出后,终端回到命令行提示符却无法响应键盘输入,只能按Ctrl+C刷新提示符,或者点击窗口X关闭。
  • 把subprocess调用换成time.sleep(5),即使多线程并发,程序也能正常终止——这说明问题确实和ffmpeg子进程的调用有关,而不是多线程本身的问题。
  • 尝试用concurrent.futures.ThreadPoolExecutor替换手动创建线程,问题依旧。
  • 调整子进程的超时逻辑(比如给communicate加timeout、捕获TimeoutExpired异常),也没能解决问题。

现在我想知道:

  1. 为什么多个ffmpeg子进程线程并发时,程序无法正常终止?
  2. 有什么办法能进一步排查出“残留”的资源或进程?
  3. 如何在保持100毫秒定时器间隔的前提下,解决这个终止问题?

备注:内容来源于stack exchange,提问作者dave

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 19:08:02