“ffmpeg-python show progress”で検索すると出てくるのがこのコード (ffmpeg-python/show_progress.py at master – GitHub)。この手法は、ffmpeg の progress 出力を stdout ではなく Unix ドメインソケットに吐き出させ、それを盗み見ます。盗み見るために gevent greenlet を使っており、エレガントかつ高速に処理できます。ただ、gevent monkey patch を patch_all で適用すると、思いもよらず広範囲にモジュールが greenlet を使うように書き換えられてしまい、誤動作につながることがままあります。そこで gevent を使わずに show progress できないか葛藤してみたので、その結果をご紹介します。
そもそもの方向性
gevent を限定的な範囲で有効にできればそれでよかったのですが、いろいろ試してみた限りではうまく動かせなかった、そして gevent を未だによく理解できていないということもあり、もっとわかりやすい構造で実現できないか、という思いがありました。具体的に言うと、Unix ドメインソケットではなく stdout をウォッチして処理できないか、というのがそもそもの方向性としてありました。
もうひとつの制限
制限というかこだわりみたいなものでしたが、このお題を頂いた大元のコードが冒頭のサイトを参考にしたこのようなものだったため、
try: ((ffmpeg .input(input_video).video .filter_('fps', fps='30') .filter_('trim', duration=trim_duration) .filter_('crop', x=str(x), y=str(y), w=str(w), h=str(h)) .filter_('scale', w=str(nw), h=str(nh)) .filter_('pad', x=str(x_offset), y=str(y_offset), width='800', height='400', color='black') .output(output_video, **{'qscale:v': 1, 'loglevel': 'quiet'}) .global_args('-progress', 'unix://{}'.format(socket_filename)) .overwrite_output() .run(capture_stdout=True, capture_stderr=True) ), except ffmpeg.Error as e: print(e.stderr, file=sys.stderr) raise e
以下のように、Linuxコマンドを呼ぶ形では書かないことにしました。あくまでも ffmpeg-python にこだわったわけです。まあ最終的には似たようなことにはなるのですが…
cmd = ['ffmpeg', '-i', 'input.mp4', 'output.mp4'] proc = subprocess.Popen(cmd)
標準出力にするには
まず Unix ドメインソケットではなく標準出力に progress が出るようにします。参考にしたのはこのサイト(FFmpegで進捗を出力して計算するには)。これによれば、”-progress -” とすればよい模様。ということで、こうすればよいのかなと察しがつく。
.global_args('-progress', '-'))
次に実際に標準出力に出してみたいわけですが、なかなか簡単には行かない。この記事 (Getting realtime output from ffmpeg to be used in progress bar (PyQt4, stdout)) によれば、Linuxコマンドの形式であればこれでいけるらしい。
cmd = "ffmpeg -i in.mp4 -y out.avi" p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) for line in p.stdout: print(line)
だから ffmpeg-python でも subprocess にできればいいんだよね… でもやり方がわからない… 1週間くらい彷徨った挙句に出会ったのが、冒頭の人の別の記事 (Ability to track progress of an ffmpeg command)。どうやら .compile() というのがあるらしいことがわかりやってみる。
p = subprocess.Popen((ffmpeg .input(input_video).video .filter_('fps', fps='30') .filter_('trim', duration=trim_duration) .filter_('crop', x=str(x), y=str(y), w=str(w), h=str(h)) .filter_('scale', w=str(nw), h=str(nh)) .filter_('pad', x=str(x_offset), y=str(y_offset), width='800', height='400', color='black') .output(output_video, **{'qscale:v': 1, 'loglevel': 'quiet'}) .global_args('-progress', '-') .overwrite_output() .compile() ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True ) for line in p.stdout: print(line)
おー、コンソール出力に progress が出てきたよー。
というわけで、
あとは標準出力に出したものを parse して progress bar に渡せばよいと。
class ProgressReporter: def __init__(self, total, max_percentage): self.total_duration = total self.max_percentage = float(max_percentage) self.last_progress = 0.0 def __enter__(self): return self def __exit__(self, exc_type, exc_value, exc_tb): pass def update(self, progress, final=False): if final: prog = self.max_percentage else: prog = math.floor(float(progress) / self.total_duration * self.max_percentage) if prog > self.last_progress: self.last_progress = prog show_progress(prog) # show progress bar total = total_duration max_percentage = 100 with ProgressReporter(total, max_percentage) as resp: p = subprocess.Popen((ffmpeg .input(input_video).video .filter_('fps', fps='30') .filter_('trim', duration=trim_duration) .filter_('crop', x=str(x), y=str(y), w=str(w), h=str(h)) .filter_('scale', w=str(nw), h=str(nh)) .filter_('pad', x=str(x_offset), y=str(y_offset), width='800', height='400', color='black') .output(output_video, **{'qscale:v': 1, 'loglevel': 'quiet'}) .global_args('-progress', '-') .overwrite_output() .compile() ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True ) for line in p.stdout: parts = line.split('=') key = parts[0] if len(parts) > 0 else None value = parts[1] if len(parts) > 1 else None if key == 'out_time_ms': time = math.floor(float(value) / 100000.) / 10. resp.update(time) elif key == 'progress' and value == 'end': resp.update(None, final=True) out, err = p.communicate()
細かいところは省略してますが、だいたいこんな感じ。max_percentage ってなんだって思うかも知れませんが、実は扱っている全システムはもっと巨大で、ffmpeg での処理は前段の準備に過ぎないので、実際には max_percentage = 20 くらいで運用しているシステムなのです。それが一つの Docker container で動作しているので、gevent は使いたくなかったというわけです。実際 ‘import gevent.monkey; gevent.monkey.patch_all()’ の位置で様々なモジュールが微妙に影響を受けたりして、動作が不定になったりしました。あまりわかっていないで使っていたのがそもそもよくなかったわけですが。
所感
ある程度時間をかけて調査したことと、システム全体の動作が安定したことから、一定の達成感はあるものの、少し動作が遅い気がしています。このあたり(python で標準出力をリアルタイムに取得するときにハマった件)の内容も気にはなっていますが、今のところ全く致命的ではない遅さのため、あとまわしにしてます。