Skip to content

Commit 1589256

Browse files
committed
Fix for multiple physical encoder core presence for all encoders.
1 parent 2bebbe8 commit 1589256

1 file changed

Lines changed: 161 additions & 29 deletions

File tree

videocompress.py

Lines changed: 161 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
@@ -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", "-filter_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"\rProg: {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("\nStitching...")
455+
with open(list_path, "w") as lf:
456+
lf.write(f"file '{p1_path}'\nfile '{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+
360470
def 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

Comments
 (0)