Pythonで外部のプログラムを実行する際には、subprocessモジュールの利用が一般的です。これは外部プロセスを立ち上げて、処理を実行するため、適切にプロセスの終了処理を記述する必要があります。外部プロセスが適切に終了しない場合、メモリリークやシステムパフォーマンスの低下といった問題を招く可能性があるためです。
OpenCVを用いて取得したビデオフレームをFFmpegを通じてH.264形式に圧縮して保存する処理を実装していたのですが、waitメソッドを使ってプロセスを終了できずデッドロックが発生しました。
この記事では、デッドロックを発生させずに適切にプロセスを終了させる方法を記載します。
サンプルコード
以下のコードはOpenCVを使用してビデオファイル (input.mp4) からフレームを読み込み、それらをFFmpegを介してH.264形式で圧縮して別のファイル (output.mp4) に保存する処理を行っています。
私のWindows環境でこの処理を実行するとデッドロックによって固まってしまいました。
from subprocess import DEVNULL, PIPE, Popen
import cv2
ffmpeg_path = "ffmpeg/ffmpeg.exe"
cap = cv2.VideoCapture('input.mp4')
fps = cap.get(cv2.CAP_PROP_FPS)
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
ffmpeg = Popen([
ffmpeg_path,
'-y',
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24',
'-s', f"{frame_width}x{frame_height}",
'-r', str(fps),
'-i', '-',
'-an',
'-c:v', 'h264',
'-b:v', '1000k',
f'output.mp4'
], stdin=PIPE, stdout=DEVNULL, stderr=PIPE)
cap_total_frame = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
for i in range(cap_total_frame):
ret, frame = cap.read()
if frame is not None:
try:
ffmpeg.stdin.write(frame.tobytes())
except BrokenPipeError:
# FFmpegプロセスからのエラーメッセージを取得
ffmpeg_error = ffmpeg.stderr.read().decode('utf-8')
print("FFmpeg Error:\n", ffmpeg_error)
raise
ffmpeg.stdin.close()
ffmpeg.wait()
cap.release()
問題の説明と対処方法
上記のコードの問題の箇所は、stderrをPIPEに設定してエラーメッセージをキャプチャしている部分です。Pythonのsubprocessモジュールのドキュメントには、waitメソッドの説明の中で以下のような記述があります。
stdout=PIPEやstderr=PIPEを使用しており、子プロセスがOSのパイプバッファーをブロックするほど多くのデータを出力した場合、デッドロックが発生することがあります。これを避けるためには、Popen.communicate()を使用してください。
つまり、stderrをPIPEに設定した状態で子プロセス(この場合はFFmpeg)が多量のエラーメッセージを出力すると、プロセスがパイプバッファーを満たし、それ以上の出力ができなくなり、結果的にデッドロックを引き起こす可能性があります。
エラーメッセージが不要な場合は、stderr=PIPEをstderr=DEVNULLに変更することで、このデッドロックの問題を回避し、waitメソッドを使用してプロセスを適切に終了させることができます。
ffmpeg = Popen([
ffmpeg_path,
'-y',
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24',
'-s', f"{frame_width}x{frame_height}",
'-r', str(fps),
'-i', '-',
'-an',
'-c:v', 'h264',
'-b:v', '1000k',
f'output.mp4'
], stdin=PIPE, stdout=DEVNULL, stderr=DEVNULL)
エラーメッセージがどうしても必要な場合はパイプバッファーを満杯にしない処理を追加する必要があります。
例えば、stderrの読み取りを行うスレッドを立てることでバッファーを満杯にせずデッドロックも発生させないでプロセスを終了させることができます。ここではprintで出力していますが、ログに残すこともできます。
import threading
from subprocess import DEVNULL, PIPE, Popen
import cv2
# stderrの読み取りを行う関数
def read_stderr(process):
for line in iter(process.stderr.readline, b''):
print("FFmpeg message:\n", line.decode('utf-8'))
ffmpeg_path = "ffmpeg/ffmpeg.exe"
cap = cv2.VideoCapture('input.mp4')
fps = cap.get(cv2.CAP_PROP_FPS)
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
ffmpeg = Popen([
ffmpeg_path,
'-y',
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24',
'-s', f"{frame_width}x{frame_height}",
'-r', str(fps),
'-i', '-',
'-an',
'-c:v', 'h264',
'-b:v', '1000k',
f'output.mp4'
], stdin=PIPE, stdout=DEVNULL, stderr=PIPE)
# stderr読み取り用のスレッドを起動
stderr_thread = threading.Thread(target=read_stderr, args=(ffmpeg,))
stderr_thread.start()
cap_total_frame = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
for i in range(cap_total_frame):
ret, frame = cap.read()
if frame is not None:
try:
ffmpeg.stdin.write(frame.tobytes())
except BrokenPipeError:
# FFmpegプロセスからのエラーメッセージを取得
ffmpeg_error = ffmpeg.stderr.read().decode('utf-8')
print("FFmpeg Error:\n", ffmpeg_error)
raise
ffmpeg.stdin.close()
ffmpeg.wait()
stderr_thread.join() # スレッドの終了を待つ
cap.release()
まとめ
Pythonのsubprocessモジュールを使用して外部プロセスを扱う際、特にFFmpegのようなリソースを多く使用するプロセスを管理する場合、プロセスの終了を適切に管理することが重要です。stderr=PIPEの設定がデッドロックを引き起こす可能性があるため、エラーメッセージが不要な場合にはstderr=DEVNULLに設定することで、プロセスの終了を安全に、かつ効率的に行うことができます。この方法により、システムのパフォーマンスを維持しつつ、リソースの無駄遣いを防ぐことが可能となります。