@@ -29,7 +29,7 @@ def get_resource_path(filename: str) -> str:
2929 Absolute path to the resource, respecting PyInstaller bundling.
3030 """
3131 if getattr (sys , 'frozen' , False ) and hasattr (sys , '_MEIPASS' ):
32- base_path = sys ._MEIPASS
32+ base_path = sys ._MEIPASS # type: ignore
3333 if sys .platform == 'win32' and not filename .lower ().endswith ('.exe' ):
3434 filename = f"{ filename } .exe"
3535 return os .path .join (base_path , filename )
@@ -90,7 +90,7 @@ def check_encoder_available(encoder_name: str) -> bool:
9090 try :
9191 # VAAPI often requires hwupload for software sources
9292 vf_args = ["-vf" , "format=nv12,hwupload" ] if encoder_name == "hevc_vaapi" else []
93- pre_args = ["-init_hw_device" , "vaapi" , "-filter_hw_device" , "vaapi" ] if encoder_name == "hevc_vaapi" else []
93+ pre_args = ["-init_hw_device" , "vaapi" ] if encoder_name == "hevc_vaapi" else []
9494
9595 cmd = [ffmpeg_exe , "-hide_banner" , "-v" , "error" ] + pre_args + [
9696 "-f" , "lavfi" , "-i" , "color=c=black:s=1280x720:r=1:d=0.1" ,
@@ -252,7 +252,7 @@ def monitor_process(process: subprocess.Popen, tracker: ProgressTracker, is_a: b
252252
253253 buf = ""
254254 while True :
255- char = process .stderr .read (1 )
255+ char = process .stderr .read (1 ) # type: ignore
256256 if not char : break
257257
258258 if char in ('\r ' , '\n ' ):
@@ -300,30 +300,17 @@ def encode_single_pass_hw(
300300 Returns:
301301 True on success, False on failure.
302302 """
303- cmd = [ffmpeg_exe , "-y" ]
304-
305- # Pre-input flags for VAAPI
306- if encoder == "hevc_vaapi" :
307- cmd .extend (["-init_hw_device" , "vaapi" , "-filter_hw_device" , "vaapi" ])
308-
309- cmd .extend (["-i" , input_path , "-c:v" , encoder , "-b:v" , f"{ bitrate_k } k" ])
303+ cmd = build_single_pass_cmd (
304+ ffmpeg_exe = ffmpeg_exe ,
305+ input_path = input_path ,
306+ encoder = encoder ,
307+ bitrate_k = bitrate_k ,
308+ fps = fps ,
309+ start = None ,
310+ end = None ,
311+ output_path = output_path ,
312+ )
310313
311- # Encoder specific flags
312- if encoder == "hevc_amf" :
313- cmd .extend (["-usage" , "transcoding" , "-quality" , "balanced" , "-rc" , "cbr" ])
314- elif encoder == "hevc_qsv" :
315- cmd .extend (["-load_plugin" , "hevc_hw" , "-preset" , "medium" ])
316- elif encoder == "hevc_videotoolbox" :
317- cmd .extend (["-allow_sw" , "1" , "-realtime" , "0" ])
318- elif encoder == "hevc_vaapi" :
319- cmd .extend (["-vf" , "format=nv12,hwupload" ])
320- elif encoder == "libx265" :
321- cmd .extend (["-preset" , "medium" , "-tag:v" , "hvc1" , "-filter:v" , f"fps={ fps } " ])
322-
323- # Common rate control
324- cmd .extend (["-maxrate:v" , f"{ bitrate_k } k" , "-bufsize:v" , f"{ bitrate_k * 2 } k" ])
325- cmd .extend (["-c:a" , "copy" , "-loglevel" , "error" , "-stats" , output_path ])
326-
327314 process = subprocess .Popen (cmd , stdout = subprocess .PIPE , stderr = subprocess .PIPE , bufsize = 0 )
328315
329316 # Simple single-process monitor
@@ -333,7 +320,7 @@ def encode_single_pass_hw(
333320
334321 try :
335322 while True :
336- byte = process .stderr .read (1 )
323+ byte = process .stderr .read (1 ) # type: ignore
337324 if not byte : break
338325 char = byte .decode ('utf-8' , errors = 'ignore' )
339326 if char == '\r ' or char == '\n ' :
@@ -357,11 +344,135 @@ def encode_single_pass_hw(
357344 print ()
358345 return process .returncode == 0
359346
347+
348+ def build_single_pass_cmd (
349+ ffmpeg_exe : str ,
350+ input_path : str ,
351+ encoder : str ,
352+ bitrate_k : int ,
353+ fps : float ,
354+ start : Optional [float ],
355+ end : Optional [float ],
356+ output_path : str ,
357+ ) -> List [str ]:
358+ """Build a single-pass FFmpeg command for the requested encoder.
359+
360+ Args:
361+ ffmpeg_exe: Path to the ffmpeg executable.
362+ input_path: Source video path.
363+ encoder: FFmpeg encoder name.
364+ bitrate_k: Target bitrate in kbps.
365+ fps: Source frames per second.
366+ start: Optional start time for segmenting.
367+ end: Optional end time for segmenting.
368+ output_path: Destination video path.
369+
370+ Returns:
371+ A command list ready for subprocess execution.
372+ """
373+ cmd : List [str ] = [ffmpeg_exe , "-y" ]
374+
375+ if encoder == "hevc_vaapi" :
376+ cmd .extend (["-init_hw_device" , "vaapi" ])
377+
378+ if start is not None :
379+ cmd .extend (["-ss" , str (start )])
380+ if end is not None :
381+ cmd .extend (["-to" , str (end )])
382+
383+ cmd .extend (["-i" , input_path , "-c:v" , encoder , "-b:v" , f"{ bitrate_k } k" ])
384+
385+ if encoder == "hevc_amf" :
386+ cmd .extend (["-usage" , "transcoding" , "-quality" , "balanced" , "-rc" , "cbr" ])
387+ elif encoder == "hevc_qsv" :
388+ cmd .extend (["-load_plugin" , "hevc_hw" , "-preset" , "medium" ])
389+ elif encoder == "hevc_videotoolbox" :
390+ cmd .extend (["-allow_sw" , "1" , "-realtime" , "0" ])
391+ elif encoder == "hevc_vaapi" :
392+ cmd .extend (["-vf" , "format=nv12,hwupload" ])
393+ elif encoder == "libx265" :
394+ cmd .extend (["-preset" , "medium" , "-tag:v" , "hvc1" , "-filter:v" , f"fps={ fps } " ])
395+
396+ cmd .extend (["-maxrate:v" , f"{ bitrate_k } k" , "-bufsize:v" , f"{ bitrate_k * 2 } k" ])
397+ cmd .extend (["-c:a" , "copy" , "-loglevel" , "error" , "-stats" , output_path ])
398+ return cmd
399+
400+
401+ def encode_split_single_pass_hw (
402+ ffmpeg_exe : str ,
403+ input_path : str ,
404+ output_path : str ,
405+ encoder : str ,
406+ bitrates_k : Tuple [int , int ],
407+ fps : float ,
408+ durations : Tuple [float , float ],
409+ split_time : float ,
410+ ) -> Tuple [bool , str ]:
411+ """Run split single-pass encoding for hardware encoders other than NVENC.
412+
413+ Args:
414+ ffmpeg_exe: Path to the ffmpeg executable.
415+ input_path: Source video path.
416+ output_path: Destination file path.
417+ encoder: Active hardware encoder.
418+ bitrates_k: Tuple of bitrates (kbps) for first and second segments.
419+ fps: Source frames per second.
420+ durations: Durations of the first and second segments.
421+ split_time: Timestamp marking the segment boundary.
422+
423+ Returns:
424+ Tuple of (success flag, error message when unsuccessful).
425+ """
426+ temp_dir = tempfile .mkdtemp (prefix = "vidcomp_hw_" )
427+ p1_path = os .path .join (temp_dir , "p1.mp4" )
428+ p2_path = os .path .join (temp_dir , "p2.mp4" )
429+ list_path = os .path .join (temp_dir , "list.txt" )
430+
431+ try :
432+ cmd_a = build_single_pass_cmd (ffmpeg_exe , input_path , encoder , bitrates_k [0 ], fps , 0 , split_time , p1_path )
433+ cmd_b = build_single_pass_cmd (ffmpeg_exe , input_path , encoder , bitrates_k [1 ], fps , split_time , None , p2_path )
434+
435+ pa = subprocess .Popen (cmd_a , stderr = subprocess .PIPE , text = True , bufsize = 0 )
436+ pb = subprocess .Popen (cmd_b , stderr = subprocess .PIPE , text = True , bufsize = 0 )
437+
438+ tracker = ProgressTracker (durations [0 ], durations [1 ])
439+ t1 = threading .Thread (target = monitor_process , args = (pa , tracker , True ))
440+ t2 = threading .Thread (target = monitor_process , args = (pb , tracker , False ))
441+ t1 .start (); t2 .start ()
442+
443+ while pa .poll () is None or pb .poll () is None :
444+ prog , fps_total , eta = tracker .get_stats ()
445+ speed = fps_total / fps if fps > 0 else 0
446+ print (f"\r Prog: { prog :.1f} % | FPS: { fps_total :.1f} | Speed: { speed :.2f} x | ETA: { eta // 3600 :02} :{ (eta % 3600 )// 60 :02} :{ eta % 60 :02} " , end = "" )
447+ time .sleep (0.5 )
448+
449+ t1 .join (); t2 .join ()
450+
451+ if pa .returncode != 0 or pb .returncode != 0 :
452+ return False , "Split encode failed"
453+
454+ print ("\n Stitching..." )
455+ with open (list_path , "w" ) as lf :
456+ lf .write (f"file '{ p1_path } '\n file '{ p2_path } '" )
457+
458+ try :
459+ subprocess .run ([ffmpeg_exe , "-f" , "concat" , "-safe" , "0" , "-i" , list_path , "-c" , "copy" , "-y" , output_path ], check = True , stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
460+ except subprocess .CalledProcessError :
461+ return False , "Stitching failed"
462+
463+ return True , ""
464+ finally :
465+ try :
466+ shutil .rmtree (temp_dir )
467+ except OSError :
468+ pass
469+
360470def compress_video (input_path : str , output_path : Optional [str ] = None , target_size_mb : int = 100 ) -> Tuple [bool , str ]:
361471 """Compress a video to an approximate target size.
362472
363- Chooses the best available encoder and uses either a parallel 2-pass (NVENC)
364- or a single-pass approach for other encoders.
473+ Chooses the best available encoder and uses either a parallel 2-pass split
474+ (NVENC), a split single-pass for other hardware encoders, or a single
475+ unsplit pass for CPU (libx265).
365476
366477 Args:
367478 input_path: Path to the input video.
@@ -489,7 +600,28 @@ def compress_video(input_path: str, output_path: Optional[str] = None, target_si
489600 except OSError : pass
490601 clean_log_file ()
491602
492- # --- SERIAL SINGLE-PASS (Other HW / CPU) ---
603+ # --- SPLIT SINGLE-PASS FOR OTHER HW ENCODERS ---
604+ elif active_encoder in {"hevc_vaapi" , "hevc_videotoolbox" , "hevc_amf" , "hevc_qsv" }:
605+ split_time = get_smart_split_point (input_path , duration )
606+ print (f"Splitting at { split_time :.2f} s" )
607+
608+ durs = (split_time , duration - split_time )
609+ brs : List [int ] = []
610+
611+ tgt_part_mb = target_size_mb / 2
612+ for seg_dur in durs :
613+ audio_mb = (audio_kbps * seg_dur * 1000 ) / 8 / MB_TO_BYTES
614+ video_mb = max (0.5 , tgt_part_mb - audio_mb )
615+ br_k = math .floor (((video_mb * MB_TO_BITS ) / seg_dur / 1000 ) * BITRATE_SAFETY_FACTOR )
616+ brs .append (br_k )
617+
618+ print (f"Worker 1: { brs [0 ]} k | Worker 2: { brs [1 ]} k" )
619+ ok , err = encode_split_single_pass_hw (ffmpeg_exe , input_path , output_path , active_encoder , (brs [0 ], brs [1 ]), fps , durs , split_time )
620+ if not ok :
621+ clean_log_file ()
622+ return False , err
623+
624+ # --- SERIAL SINGLE-PASS (CPU) ---
493625 else :
494626 tgt_bits = target_size_mb * MB_TO_BITS
495627 vid_bits = max (0 , tgt_bits - (audio_kbps * 1000 * duration ))
0 commit comments