1+ # ffmpeg_worker.py
2+ # FFmpegのワーカープロセスと関連ヘルパー関数
3+
4+ import subprocess
5+ import os
6+ import time
7+ # import json # configは呼び出し側で準備され、worker内では直接jsonをパースしない
8+ # import queue # キューは引数として渡される
9+ # import multiprocessing # イベントは引数として渡される
10+ import traceback # 例外発生時のスタックトレース取得用
11+
12+ def check_for_cuda_fallback_error (ffmpeg_output_str ):
13+ """
14+ FFmpegの出力文字列を解析し、CUDA関連のエラーや
15+ CPUへのフォールバックを示唆する可能性のあるパターンを検出します。
16+
17+ Args:
18+ ffmpeg_output_str (str): FFmpegの標準エラー出力または標準出力の文字列。
19+
20+ Returns:
21+ bool: CUDAフォールバックが必要と判断されるエラーが含まれていればTrue、そうでなければFalse。
22+ """
23+ output_lower = ffmpeg_output_str .lower ()
24+ # CUDA初期化エラー
25+ if "hwaccel initialisation returned error" in output_lower or \
26+ "failed setup for format cuda" in output_lower :
27+ return True
28+ # 一般的なCUDAエラー
29+ if "cuda_error_" in output_lower : # Generic CUDA error string
30+ return True
31+ # 解像度関連のCUDAエラー (特定のキーワードの組み合わせ)
32+ if "not within range" in output_lower and \
33+ ("width" in output_lower or "height" in output_lower ) and \
34+ ("cuda" in output_lower or "nvdec" in output_lower or "cuvid" in output_lower ): # Resolution issues with CUDA
35+ return True
36+ # フォーマット変換関連のCUDAエラー
37+ if "impossible to convert between the formats supported by the filter" in output_lower and \
38+ ("cuda" in output_lower or "hwdownload" in output_lower or "hwupload" in output_lower ): # Format conversion issues with CUDA pipeline
39+ return True
40+ # フィルタ記述パースエラー (CUDA利用時によく見られる)
41+ if "error parsing a filter description" in output_lower or \
42+ "error parsing filterchain" in output_lower : # Often seen with CUDA issues if filters are complex
43+ return True # This might be too broad, but often indicates a problem in CUDA pipeline setup
44+ return False
45+
46+ def ffmpeg_worker_process (viewpoint_idx , viewpoint_data , config , log_queue_mp , progress_queue_mp , cancel_event_mp ):
47+ """
48+ 個別の視点に対するFFmpeg変換処理をサブプロセスとして実行します。
49+ マルチプロセッシングプールから呼び出されることを想定しています。
50+
51+ Args:
52+ viewpoint_idx (int): 処理中の視点のインデックス。
53+ viewpoint_data (dict): 視点情報 (fov, pitch, yaw)。
54+ config (dict): 変換設定 (ffmpeg_path, input_file, output_folderなど)。
55+ log_queue_mp (multiprocessing.Queue): ログメッセージをGUIプロセスに送るためのキュー。
56+ progress_queue_mp (multiprocessing.Queue): 進捗情報をGUIプロセスに送るためのキュー。
57+ cancel_event_mp (multiprocessing.Event): キャンセル指示を検知するためのイベント。
58+ """
59+ process_start_time = time .time () # 処理開始時間
60+ # configから必要な設定値を取得
61+ ffmpeg_path = config ["ffmpeg_path" ]
62+ input_file = config ["input_file" ]
63+ output_folder = config ["output_folder" ]
64+ output_width , output_height = config ["output_resolution" ]
65+ interp = config ["interp" ]
66+ threads_ffmpeg = config ["threads_ffmpeg" ] # CPU処理時のスレッド数
67+ use_cuda = config ["use_cuda" ]
68+ output_format = config ["output_format" ]
69+ frame_interval_val = config ["frame_interval" ] # PNG/JPEG時のフレーム抽出間隔
70+ video_preset = config ["video_preset" ] # 動画出力時のプリセット
71+ video_cq = config ["video_cq" ] # 動画出力時の品質値 (CQ/CRF)
72+ png_pred_option = config .get ("png_pred_option" , "3" ) # PNG予測オプション
73+ jpeg_quality = config .get ("jpeg_quality" , 90 ) # JPEG品質
74+
75+ # 視点データを展開
76+ fov , pitch , yaw = viewpoint_data ["fov" ], viewpoint_data ["pitch" ], viewpoint_data ["yaw" ]
77+ # FFmpegのyawは -180 から 180 の範囲であるため調整
78+ ffmpeg_yaw = yaw
79+ if ffmpeg_yaw > 180 :
80+ ffmpeg_yaw -= 360
81+
82+ # v360フィルタのパラメータ文字列を生成
83+ v360_filter_params = (
84+ f"e:flat:yaw={ ffmpeg_yaw :.2f} :pitch={ pitch :.2f} :h_fov={ fov :.2f} :v_fov={ fov :.2f} "
85+ f":w={ output_width } :h={ output_height } :interp={ interp } "
86+ )
87+ command = [ffmpeg_path , "-y" ] # -y: 出力ファイルを無条件に上書き
88+ filter_complex_parts = [] # -vf または -filter_complex に渡すフィルタのリスト
89+
90+ # CUDAを使用する場合のオプション追加
91+ if use_cuda :
92+ command .extend (["-hwaccel" , "cuda" , "-hwaccel_output_format" , "cuda" ])
93+ command .extend (["-i" , input_file ]) # 入力ファイル指定
94+
95+ # 静止画シーケンスでフレームレート指定がある場合
96+ if output_format in ["png" , "jpeg" ] and frame_interval_val > 0 :
97+ filter_complex_parts .append (f"fps=fps=1/{ frame_interval_val :.3f} " )
98+
99+ # フィルタチェーンの構築
100+ if use_cuda :
101+ # CUDAパイプライン: hwdownloadでGPUメモリからシステムメモリへ -> v360 -> hwuploadでシステムメモリからGPUメモリへ
102+ filter_complex_parts .extend (["hwdownload" , "format=nv12" , f"v360={ v360_filter_params } " ])
103+ if output_format == "png" :
104+ filter_complex_parts .append ("format=rgb24" ) # PNGはrgb24が必要
105+ elif output_format == "jpeg" :
106+ filter_complex_parts .append ("format=yuvj420p" ) # JPEGはyuvj420p
107+ elif output_format == "video" : # 動画(hevc_nvenc)
108+ filter_complex_parts .extend (["format=nv12" , "hwupload_cuda" ]) # hevc_nvencはnv12入力を期待
109+ else : # CPU処理
110+ filter_complex_parts .append (f"v360={ v360_filter_params } " )
111+ if output_format == "png" :
112+ filter_complex_parts .append ("format=rgb24" )
113+ elif output_format == "jpeg" :
114+ filter_complex_parts .append ("format=yuvj420p" )
115+ elif output_format == "video" : # 動画(libx265)
116+ filter_complex_parts .append ("format=yuv420p" ) # libx265はyuv420p
117+
118+
119+ # 出力ファイル名/フォルダ名の生成
120+ base_input_name = os .path .splitext (os .path .basename (input_file ))[0 ]
121+ pitch_folder_str = f"{ int (pitch ):03d} " .replace ("-" , "m" ) # ピッチ角をフォルダ名用に整形 (例: -30 -> m030)
122+
123+ if output_format in ["png" , "jpeg" ]:
124+ # 静止画シーケンスの場合、視点ごとにフォルダを作成
125+ view_folder_name = f"{ base_input_name } _p{ pitch_folder_str } _y{ int (yaw ):03d} { '_jpeg' if output_format == 'jpeg' else '' } "
126+ output_dir_for_viewpoint = os .path .join (output_folder , view_folder_name )
127+ try :
128+ os .makedirs (output_dir_for_viewpoint , exist_ok = True )
129+ except OSError as e :
130+ log_queue_mp .put ({"type" : "log" , "level" : "ERROR" , "message" : f"出力フォルダ作成失敗({ view_folder_name } ): { e } " })
131+ progress_queue_mp .put ({
132+ "type" : "task_result" , "viewpoint_index" : viewpoint_idx , "success" : False ,
133+ "error_message" : f"出力フォルダ作成失敗: { e } " , "duration" : time .time () - process_start_time
134+ })
135+ return
136+ file_ext = "jpg" if output_format == "jpeg" else "png"
137+ output_filename_pattern = os .path .join (output_dir_for_viewpoint , f"{ view_folder_name } _%05d.{ file_ext } " )
138+ command .extend (["-vf" , "," .join (filter_complex_parts )])
139+ if not use_cuda : # CPU処理時のみスレッド数を指定
140+ command .extend (["-threads" , str (threads_ffmpeg )])
141+ if output_format == "png" :
142+ command .extend (["-pred" , png_pred_option ]) # PNG予測オプション
143+ elif output_format == "jpeg" :
144+ # JPEG品質をFFmpegのqscale:vに変換 (1-100 -> 1-31, 1が高い)
145+ q_val = max (1 , min (31 , int (round (1 + (100 - jpeg_quality ) * 30 / 99 ))))
146+ command .extend (["-qscale:v" , str (q_val )])
147+ command .append (output_filename_pattern )
148+ elif output_format == "video" :
149+ command .extend (["-vf" , "," .join (filter_complex_parts )])
150+ if use_cuda :
151+ command .extend (["-c:v" , "hevc_nvenc" ]) # CUDAエンコーダ
152+ else :
153+ command .extend (["-c:v" , "libx265" , "-threads" , str (threads_ffmpeg )]) # CPUエンコーダ
154+ view_file_name = f"{ base_input_name } _p{ pitch_folder_str } _y{ int (yaw ):03d} .mp4"
155+ output_file = os .path .join (output_folder , view_file_name )
156+ command .extend (["-preset" , video_preset , "-cq" if use_cuda else "-crf" , str (video_cq ), "-an" , output_file ]) # -an: 音声なし
157+
158+ # FFmpegコマンドをログに出力 (デバッグ用)
159+ log_queue_mp .put ({"type" : "log" , "level" : "DEBUG" , "message" : f"Worker { viewpoint_idx + 1 } (CUDA: { use_cuda } ) command: { ' ' .join (command )} " })
160+ ffmpeg_process = None
161+ try :
162+ # Windowsでコンソールウィンドウを非表示にするための設定
163+ startupinfo = None
164+ if os .name == 'nt' :
165+ startupinfo = subprocess .STARTUPINFO ()
166+ startupinfo .dwFlags |= subprocess .STARTF_USESHOWWINDOW
167+ startupinfo .wShowWindow = subprocess .SW_HIDE
168+
169+ # FFmpegプロセスを開始
170+ ffmpeg_process = subprocess .Popen (
171+ command , stdout = subprocess .PIPE , stderr = subprocess .STDOUT , # 標準出力と標準エラーをパイプで取得
172+ universal_newlines = True , startupinfo = startupinfo ,
173+ encoding = 'utf-8' , errors = 'replace' # エンコーディング指定
174+ )
175+
176+ # FFmpegの出力をリアルタイムで読み取り、ログキューに送信
177+ if ffmpeg_process .stdout : # stdoutがNoneでないことを確認
178+ for line in iter (ffmpeg_process .stdout .readline , '' ):
179+ if cancel_event_mp .is_set (): # キャンセルが要求されたかチェック
180+ log_queue_mp .put ({"type" : "log" , "level" : "INFO" , "message" : f"Worker { viewpoint_idx + 1 } (P{ pitch :.1f} Y{ yaw :.1f} ) cancelled." })
181+ ffmpeg_process .terminate () # プロセスを終了
182+ try :
183+ ffmpeg_process .wait (timeout = 2 ) # 終了待機 (タイムアウト付き)
184+ except subprocess .TimeoutExpired :
185+ ffmpeg_process .kill () # 強制終了
186+ break
187+ log_queue_mp .put ({"type" : "ffmpeg_raw" , "line" : line .strip (), "viewpoint_index" : viewpoint_idx })
188+ ffmpeg_process .wait () # プロセスの終了を待つ
189+
190+ if cancel_event_mp .is_set ():
191+ progress_queue_mp .put ({
192+ "type" : "task_result" , "viewpoint_index" : viewpoint_idx , "success" : False ,
193+ "cancelled" : True , "duration" : time .time () - process_start_time
194+ })
195+ return
196+
197+ # FFmpegの終了コードをチェック
198+ if ffmpeg_process .returncode == 0 :
199+ progress_queue_mp .put ({
200+ "type" : "task_result" , "viewpoint_index" : viewpoint_idx , "success" : True ,
201+ "duration" : time .time () - process_start_time
202+ })
203+ else :
204+ log_queue_mp .put ({"type" : "log" , "level" : "ERROR" , "message" : f"FFmpeg error (Worker { viewpoint_idx + 1 } , P{ pitch :.1f} Y{ yaw :.1f} ): Code { ffmpeg_process .returncode } " })
205+ progress_queue_mp .put ({
206+ "type" : "task_result" , "viewpoint_index" : viewpoint_idx , "success" : False ,
207+ "error_message" : f"FFmpeg failed (code { ffmpeg_process .returncode } )" ,
208+ "duration" : time .time () - process_start_time
209+ })
210+ except Exception as e :
211+ log_queue_mp .put ({"type" : "log" , "level" : "CRITICAL" , "message" : f"Worker { viewpoint_idx + 1 } (P{ pitch :.1f} Y{ yaw :.1f} ) exception: { e } " })
212+ log_queue_mp .put ({"type" : "log" , "level" : "DEBUG" , "message" : traceback .format_exc ()})
213+ progress_queue_mp .put ({
214+ "type" : "task_result" , "viewpoint_index" : viewpoint_idx , "success" : False ,
215+ "error_message" : str (e ), "duration" : time .time () - process_start_time
216+ })
217+ finally :
218+ # プロセスがまだ実行中であれば強制終了
219+ if ffmpeg_process and ffmpeg_process .poll () is None :
220+ ffmpeg_process .kill ()
221+ if ffmpeg_process .stdout :
222+ ffmpeg_process .stdout .close ()
223+ # stderrも同様にクローズ (Popenでstdout=PIPE, stderr=STDOUTにしているのでstdoutのクローズで十分なはず)
224+ ffmpeg_process .wait ()
0 commit comments