Search code examples
pythonffmpegpyqtpyqt5qthread

PyQt-thread. Get dynamically output


I use PyQt-thread for parallel conversion of mp3 files to aac via ffmpeg. Here is my code:

class SubprocessThread(QThread):
    signal = pyqtSignal('PyQt_PyObject')

    def __init__(self, command, args):
        QThread.__init__(self)
        self.command = command
        self.args = args

    def __del__(self):
        self.wait()

    def run(self):
        output = subprocess.check_output('{0} {1}'.format(self.command, self.args), shell=True).split()
        self.signal.emit(output)

And here is example of usage:

threads = []

for part in parts.keys():
    args = "-i \'{0}.mp3\' -c:a aac -b:a {1}k \'{2}.m4a\'".format(
        os.path.join(tmp_dir, str(part)),
        int(self.bitrate_cbx.currentText()),
        os.path.join(tmp_dir, str(part)))
    print(args)  # debug
    ffmpeg_thread = SubprocessThread('ffmpeg', args)
    ffmpeg_thread.signal.connect(self.on_data_ready)
    threads.append(ffmpeg_thread)
    ffmpeg_thread.start()
    self.threads_count += 1

I want to make progress bar, based on conversion, but ffmpeg always updates last string in his output (when conversion in progress). Here is an example of ffmpeg output while files are converting:

user@host$ ffmpeg -i '/home/user/001.mp3' -c:a aac -b:a 128k -vn '/home/user/test.m4a' 
ffmpeg version n4.2.1 Copyright (c) 2000-2019 the FFmpeg developers
  built with gcc 9.2.0 (GCC)
  configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3
  libavutil      56. 31.100 / 56. 31.100
  libavcodec     58. 54.100 / 58. 54.100
  libavformat    58. 29.100 / 58. 29.100
  libavdevice    58.  8.100 / 58.  8.100
  libavfilter     7. 57.100 /  7. 57.100
  libswscale      5.  5.100 /  5.  5.100
  libswresample   3.  5.100 /  3.  5.100
  libpostproc    55.  5.100 / 55.  5.100
Input #0, mp3, from '/home/user/001.mp3':
  Metadata:
    encoder         : Lavf57.41.100
    title           : test
    artist          : test
    album_artist    : test
    album           : test
    composer        : test
    genre           : test
    date            : 2018
  Duration: 00:12:38.02, start: 0.025056, bitrate: 192 kb/s
    Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s
    Metadata:
      encoder         : Lavc57.48
    Stream #0:1: Video: mjpeg (Baseline), yuvj420p(pc, bt470bg/unknown/unknown), 500x500 [SAR 1:1 DAR 1:1], 90k tbr, 90k tbn, 90k tbc (attached pic)
    Metadata:
      comment         : Cover (front)
Stream mapping:
  Stream #0:0 -> #0:0 (mp3 (mp3float) -> aac (native))
Press [q] to stop, [?] for help
Output #0, ipod, to '/home/user/test.m4a':
  Metadata:
    date            : test
    title           : test
    artist          : test
    album_artist    : test
    album           : test
    composer        : test
    genre           : test
    encoder         : Lavf58.29.100
    Stream #0:0: Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s
    Metadata:
      encoder         : Lavc58.54.100 aac
size=   12107kB time=00:12:38.01 bitrate= 130.8kbits/s speed=79.2x 

How can I receive this data (string, that begins from "size=...") from my parallel QThreads to calculate overall progress?


Solution

  • You do not have to use subprocess.check_output() + QThread since as you see you only get the log when the execution is finished. Instead, use QProcess that notifies the log while the conversion is still running.

    class Converter(QObject):
        dataChanged = pyqtSignal(object)
    
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self._process = QProcess()
    
            self._process.setProcessChannelMode(QProcess.MergedChannels)
            self._process.readyReadStandardOutput.connect(self.on_readyReadStandardOutput)
    
            self._process.setProgram("ffmpeg")
    
        def convert(self, source, destination, bitrate):
            # remove if destination exist
            QFile.remove(destination)
            args = ["-i", source, "-c:a", "aac", "-b:a", bitrate, "-vn", destination]
            self._process.setArguments(args)
            self._process.start()
    
        @pyqtSlot()
        def on_readyReadStandardOutput(self):
            keys = ("size", "time", "bitrate", "speed")
            data = self._process.readAllStandardOutput()
            msg = data.data().decode()
            for line in msg.splitlines():
                line = line.replace(" ", "").replace("=", "")
                if all(key in line for key in keys):
                    values = []
                    for left, right in zip(keys[:-1], keys[1:]):
                        # https://stackoverflow.com/a/51456576/6622587
                        value = line[line.index(left) + len(left) : line.index(right)]
                        values.append(value)
                    value = line[line.index(keys[-1]) + len(keys[-1]) :]
                    values.append(value)
                    d = dict(zip(keys, values))
                    self.dataChanged.emit(d)
    

    And here is example of usage:

        # ...
        converters = []
    
        for part in parts.keys():
            converter = Converter()
            converter.dataChanged.connect(self.on_data_ready)
            converter.convert(
                "{}.mp3".format(part),
                "{}.m4a".format(part),
                "{}k".format(self.bitrate_cbx.currentText()),
            )
            converters.append(converter)
            # ...
    # ...
    
    @pyqtSlot(object)
    def on_data_ready(self, data):
        print(data)
    

    Output:

    {'size': '256kB', 'time': '00:00:17.94', 'bitrate': '116.9kbits/s', 'speed': '35.9x'}
    {'size': '512kB', 'time': '00:00:44.62', 'bitrate': '94.0kbits/s', 'speed': '44.6x'}
    {'size': '1024kB', 'time': '00:01:11.21', 'bitrate': '117.8kbits/s', 'speed': '47.5x'}
    {'size': '1280kB', 'time': '00:01:37.66', 'bitrate': '107.4kbits/s', 'speed': '48.8x'}
    {'size': '1941kB', 'time': '00:02:02.46', 'bitrate': '129.8kbits/s', 'speed': '49.6x'}