• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

popstas / talks-reducer / 18643271357

20 Oct 2025 05:38AM UTC coverage: 69.746% (-1.4%) from 71.158%
18643271357

Pull #119

github

web-flow
Merge 123afec07 into 661879c59
Pull Request #119: Add Windows taskbar progress integration

83 of 196 new or added lines in 8 files covered. (42.35%)

1279 existing lines in 23 files now uncovered.

5542 of 7946 relevant lines covered (69.75%)

0.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.26
/talks_reducer/ffmpeg.py
1
"""Utilities for discovering and invoking FFmpeg commands."""
2

3
from __future__ import annotations
1✔
4

5
import logging
1✔
6
import os
1✔
7
import re
1✔
8
import subprocess
1✔
9
import sys
1✔
10
from shutil import which as _shutil_which
1✔
11
from typing import List, Optional, Tuple
1✔
12

13
from .progress import ProgressReporter, TaskbarProgressReporter, TqdmProgressReporter
1✔
14

15
logger = logging.getLogger(__name__)
1✔
16

17

18
class FFmpegNotFoundError(RuntimeError):
1✔
19
    """Raised when FFmpeg cannot be located on the current machine."""
20

21

22
def shutil_which(cmd: str) -> Optional[str]:
1✔
23
    """Wrapper around :func:`shutil.which` for easier testing."""
24

25
    return _shutil_which(cmd)
×
26

27

28
def _search_known_paths(paths: List[str]) -> Optional[str]:
1✔
29
    """Return the first existing FFmpeg path from *paths*."""
30

31
    for path in paths:
1✔
32
        if os.path.isfile(path) or shutil_which(path):
1✔
UNCOV
33
            return os.path.abspath(path) if os.path.isfile(path) else path
×
34

35
    return None
1✔
36

37

38
def _find_static_ffmpeg() -> Optional[str]:
1✔
39
    """Return the FFmpeg path bundled with static-ffmpeg when available."""
40

41
    try:
1✔
42
        import static_ffmpeg
1✔
43

44
        static_ffmpeg.add_paths()
1✔
45
        bundled_path = shutil_which("ffmpeg")
1✔
46
        if bundled_path:
1✔
47
            return bundled_path
×
48
    except ImportError:
1✔
UNCOV
49
        return None
×
50
    except Exception:
1✔
51
        return None
1✔
52

53
    return None
1✔
54

55

56
def find_ffmpeg(*, prefer_global: bool = False) -> Optional[str]:
1✔
57
    """Locate the FFmpeg executable in common installation locations."""
58

59
    env_override = os.environ.get("TALKS_REDUCER_FFMPEG") or os.environ.get(
1✔
60
        "FFMPEG_PATH"
61
    )
62
    if env_override and (os.path.isfile(env_override) or shutil_which(env_override)):
1✔
63
        return (
1✔
64
            os.path.abspath(env_override)
65
            if os.path.isfile(env_override)
66
            else env_override
67
        )
68

69
    common_paths = [
1✔
70
        "C:\\ProgramData\\chocolatey\\bin\\ffmpeg.exe",
71
        "C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe",
72
        "C:\\ffmpeg\\bin\\ffmpeg.exe",
73
        "/usr/local/bin/ffmpeg",
74
        "/opt/homebrew/bin/ffmpeg",
75
        "/usr/bin/ffmpeg",
76
        "ffmpeg",
77
    ]
78

79
    static_path: Optional[str] = None
1✔
80
    if not prefer_global:
1✔
81
        static_path = _find_static_ffmpeg()
1✔
82
        if static_path:
1✔
UNCOV
83
            return static_path
×
84

85
    candidate = _search_known_paths(common_paths)
1✔
86
    if candidate:
1✔
UNCOV
87
        return candidate
×
88

89
    if prefer_global:
1✔
UNCOV
90
        static_path = _find_static_ffmpeg()
×
UNCOV
91
        if static_path:
×
92
            return static_path
×
93

94
    return None
1✔
95

96

97
def find_ffprobe(*, prefer_global: bool = False) -> Optional[str]:
1✔
98
    """Locate the ffprobe executable, typically in the same directory as FFmpeg."""
99

100
    env_override = os.environ.get("TALKS_REDUCER_FFPROBE") or os.environ.get(
1✔
101
        "FFPROBE_PATH"
102
    )
103
    if env_override and (os.path.isfile(env_override) or shutil_which(env_override)):
1✔
104
        return (
1✔
105
            os.path.abspath(env_override)
106
            if os.path.isfile(env_override)
107
            else env_override
108
        )
109

110
    # Try to find ffprobe in the same directory as FFmpeg
111
    ffmpeg_path = find_ffmpeg(prefer_global=prefer_global)
1✔
112
    if ffmpeg_path:
1✔
113
        ffmpeg_dir = os.path.dirname(ffmpeg_path)
1✔
114
        ffprobe_path = os.path.join(ffmpeg_dir, "ffprobe")
1✔
115
        if os.path.isfile(ffprobe_path) or shutil_which(ffprobe_path):
1✔
116
            return (
1✔
117
                os.path.abspath(ffprobe_path)
118
                if os.path.isfile(ffprobe_path)
119
                else ffprobe_path
120
            )
121

122
    # Fallback to common locations
123
    common_paths = [
1✔
124
        "C:\\ProgramData\\chocolatey\\bin\\ffprobe.exe",
125
        "C:\\Program Files\\ffmpeg\\bin\\ffprobe.exe",
126
        "C:\\ffmpeg\\bin\\ffprobe.exe",
127
        "/usr/local/bin/ffprobe",
128
        "/opt/homebrew/bin/ffprobe",
129
        "/usr/bin/ffprobe",
130
        "ffprobe",
131
    ]
132

133
    static_path: Optional[str] = None
1✔
134
    if not prefer_global:
1✔
135
        static_path = _find_static_ffmpeg()
1✔
136
        if static_path:
1✔
UNCOV
137
            ffprobe_candidate = os.path.join(os.path.dirname(static_path), "ffprobe")
×
UNCOV
138
            if os.path.isfile(ffprobe_candidate) or shutil_which(ffprobe_candidate):
×
139
                return (
×
140
                    os.path.abspath(ffprobe_candidate)
141
                    if os.path.isfile(ffprobe_candidate)
142
                    else ffprobe_candidate
143
                )
144

145
    candidate = _search_known_paths(common_paths)
1✔
146
    if candidate:
1✔
UNCOV
147
        return candidate
×
148

149
    if prefer_global:
1✔
UNCOV
150
        static_path = _find_static_ffmpeg()
×
UNCOV
151
        if static_path:
×
152
            ffprobe_candidate = os.path.join(os.path.dirname(static_path), "ffprobe")
×
UNCOV
153
            if os.path.isfile(ffprobe_candidate) or shutil_which(ffprobe_candidate):
×
UNCOV
154
                return (
×
155
                    os.path.abspath(ffprobe_candidate)
156
                    if os.path.isfile(ffprobe_candidate)
157
                    else ffprobe_candidate
158
                )
159

160
    return None
1✔
161

162

163
def _resolve_ffmpeg_path(*, prefer_global: bool = False) -> str:
1✔
164
    """Resolve the FFmpeg executable path or raise ``FFmpegNotFoundError``."""
165

166
    ffmpeg_path = find_ffmpeg(prefer_global=prefer_global)
1✔
167
    if not ffmpeg_path:
1✔
168
        raise FFmpegNotFoundError(
1✔
169
            "FFmpeg not found. Please install static-ffmpeg (pip install static-ffmpeg) "
170
            "or install FFmpeg manually and add it to PATH, or set TALKS_REDUCER_FFMPEG environment variable."
171
        )
172

UNCOV
173
    print(f"Using FFmpeg at: {ffmpeg_path}")
×
UNCOV
174
    return ffmpeg_path
×
175

176

177
def _resolve_ffprobe_path(*, prefer_global: bool = False) -> str:
1✔
178
    """Resolve the ffprobe executable path or raise ``FFmpegNotFoundError``."""
179

180
    ffprobe_path = find_ffprobe(prefer_global=prefer_global)
1✔
181
    if not ffprobe_path:
1✔
182
        raise FFmpegNotFoundError(
1✔
183
            "ffprobe not found. Install FFmpeg (which includes ffprobe) and add it to PATH."
184
        )
185

UNCOV
186
    return ffprobe_path
×
187

188

189
_FFMPEG_PATH_CACHE: dict[bool, Optional[str]] = {False: None, True: None}
1✔
190
_FFPROBE_PATH_CACHE: dict[bool, Optional[str]] = {False: None, True: None}
1✔
191
_GLOBAL_FFMPEG_AVAILABLE: Optional[bool] = None
1✔
192

193

194
def get_ffmpeg_path(prefer_global: bool = False) -> str:
1✔
195
    """Return the cached FFmpeg path, resolving it on first use."""
196

197
    cached = _FFMPEG_PATH_CACHE.get(prefer_global)
1✔
198
    if cached is None:
1✔
199
        cached = _resolve_ffmpeg_path(prefer_global=prefer_global)
1✔
200
        _FFMPEG_PATH_CACHE[prefer_global] = cached
1✔
201
    return cached
1✔
202

203

204
def get_ffprobe_path(prefer_global: bool = False) -> str:
1✔
205
    """Return the cached ffprobe path, resolving it on first use."""
206

207
    cached = _FFPROBE_PATH_CACHE.get(prefer_global)
1✔
208
    if cached is None:
1✔
209
        cached = _resolve_ffprobe_path(prefer_global=prefer_global)
1✔
210
        _FFPROBE_PATH_CACHE[prefer_global] = cached
1✔
211
    return cached
1✔
212

213

214
def _normalize_executable_path(candidate: Optional[str]) -> Optional[str]:
1✔
215
    """Return an absolute path for *candidate* when it can be resolved."""
216

217
    if not candidate:
1✔
218
        return None
1✔
219

220
    if os.path.isfile(candidate):
1✔
221
        return os.path.abspath(candidate)
1✔
222

223
    resolved = shutil_which(candidate)
1✔
224
    if resolved:
1✔
225
        return os.path.abspath(resolved)
1✔
226

UNCOV
227
    return os.path.abspath(candidate) if os.path.exists(candidate) else None
×
228

229

230
def is_global_ffmpeg_available() -> bool:
1✔
231
    """Return ``True`` when a non-bundled FFmpeg binary is available."""
232

233
    global _GLOBAL_FFMPEG_AVAILABLE
234
    if _GLOBAL_FFMPEG_AVAILABLE is not None:
1✔
UNCOV
235
        return _GLOBAL_FFMPEG_AVAILABLE
×
236

237
    global_candidate = _normalize_executable_path(find_ffmpeg(prefer_global=True))
1✔
238
    if not global_candidate:
1✔
UNCOV
239
        _GLOBAL_FFMPEG_AVAILABLE = False
×
UNCOV
240
        return False
×
241

242
    static_candidate = _normalize_executable_path(_find_static_ffmpeg())
1✔
243
    if static_candidate is None:
1✔
244
        _GLOBAL_FFMPEG_AVAILABLE = True
1✔
245
        return True
1✔
246

247
    try:
1✔
248
        same_binary = os.path.samefile(global_candidate, static_candidate)
1✔
249
    except (FileNotFoundError, OSError, ValueError):
×
UNCOV
250
        same_binary = os.path.normcase(global_candidate) == os.path.normcase(
×
251
            static_candidate
252
        )
253

254
    _GLOBAL_FFMPEG_AVAILABLE = not same_binary
1✔
255
    return _GLOBAL_FFMPEG_AVAILABLE
1✔
256

257

258
_ENCODER_LISTING: dict[str, str] = {}
1✔
259

260

261
def _probe_ffmpeg_output(args: List[str]) -> Optional[str]:
1✔
262
    """Return stdout from an FFmpeg invocation, handling common failures."""
263

264
    creationflags = 0
1✔
265
    if sys.platform == "win32":
1✔
266
        # CREATE_NO_WINDOW = 0x08000000
UNCOV
267
        creationflags = 0x08000000
×
268

269
    try:
1✔
270
        result = subprocess.run(
1✔
271
            args,
272
            capture_output=True,
273
            text=True,
274
            timeout=5,
275
            creationflags=creationflags,
276
        )
277
    except (
1✔
278
        subprocess.TimeoutExpired,
279
        subprocess.CalledProcessError,
280
        FileNotFoundError,
281
    ):
282
        return None
1✔
283

284
    if result.returncode != 0:
1✔
285
        return None
1✔
286

287
    return result.stdout
1✔
288

289

290
def _get_encoder_listing(ffmpeg_path: Optional[str] = None) -> Optional[str]:
1✔
291
    """Return the cached FFmpeg encoder listing output."""
292

293
    ffmpeg_path = ffmpeg_path or get_ffmpeg_path()
1✔
294
    cache_key = os.path.abspath(ffmpeg_path)
1✔
295
    if cache_key in _ENCODER_LISTING:
1✔
NEW
296
        return _ENCODER_LISTING[cache_key]
×
297

298
    output = _probe_ffmpeg_output([ffmpeg_path, "-hide_banner", "-encoders"])
1✔
299
    if output is None:
1✔
300
        return None
1✔
301

302
    normalized = output.lower()
1✔
303
    _ENCODER_LISTING[cache_key] = normalized
1✔
304
    return normalized
1✔
305

306

307
def encoder_available(encoder_name: str, ffmpeg_path: Optional[str] = None) -> bool:
1✔
308
    """Return True if ``encoder_name`` is listed in the FFmpeg encoder catalog."""
309

310
    listing = _get_encoder_listing(ffmpeg_path)
1✔
311
    if not listing:
1✔
312
        return False
1✔
313

NEW
314
    pattern = rf"\b{re.escape(encoder_name.lower())}\b"
×
NEW
315
    return re.search(pattern, listing) is not None
×
316

317

318
def check_cuda_available(ffmpeg_path: Optional[str] = None) -> bool:
1✔
319
    """Return whether CUDA hardware encoders are usable in the FFmpeg build."""
320

321
    ffmpeg_path = ffmpeg_path or get_ffmpeg_path()
1✔
322

323
    hwaccels_output = _probe_ffmpeg_output([ffmpeg_path, "-hide_banner", "-hwaccels"])
1✔
324
    if not hwaccels_output or "cuda" not in hwaccels_output.lower():
1✔
325
        return False
1✔
326

327
    encoder_output = _get_encoder_listing(ffmpeg_path)
1✔
328
    if not encoder_output:
1✔
UNCOV
329
        return False
×
330

331
    return any(
1✔
332
        encoder in encoder_output
333
        for encoder in ["h264_nvenc", "hevc_nvenc", "av1_nvenc", "nvenc"]
334
    )
335

336

337
def run_timed_ffmpeg_command(
1✔
338
    command: str,
339
    *,
340
    reporter: Optional[ProgressReporter] = None,
341
    desc: str = "",
342
    total: Optional[int] = None,
343
    unit: str = "frames",
344
    process_callback: Optional[callable] = None,
345
) -> None:
346
    """Execute an FFmpeg command while streaming progress information.
347

348
    Args:
349
        process_callback: Optional callback that receives the subprocess.Popen object
350
    """
351

352
    import shlex
1✔
353

354
    try:
1✔
355
        args = shlex.split(command)
1✔
356
    except Exception as exc:  # pragma: no cover - defensive logging
357
        print(f"Error parsing command: {exc}", file=sys.stderr)
358
        raise
359

360
    # Hide console window on Windows
361
    creationflags = 0
1✔
362
    if sys.platform == "win32":
1✔
363
        # CREATE_NO_WINDOW = 0x08000000
364
        creationflags = 0x08000000
1✔
365

366
    try:
1✔
367
        process = subprocess.Popen(
1✔
368
            args,
369
            stdout=subprocess.PIPE,
370
            stderr=subprocess.PIPE,
371
            universal_newlines=True,
372
            bufsize=1,
373
            errors="replace",
374
            creationflags=creationflags,
375
        )
376
    except Exception as exc:  # pragma: no cover - defensive logging
377
        print(f"Error starting FFmpeg: {exc}", file=sys.stderr)
378
        raise
379

380
    # Notify callback with process object
381
    if process_callback:
1✔
382
        process_callback(process)
1✔
383

384
    base_reporter = reporter or TqdmProgressReporter()
1✔
385
    progress_reporter: ProgressReporter = base_reporter
1✔
386

387
    if sys.platform == "win32":
1✔
388
        taskbar_hwnd: Optional[int] = None
1✔
389
        hwnd_attr = getattr(base_reporter, "taskbar_hwnd", None)
1✔
390
        if callable(hwnd_attr):
1✔
391
            try:
1✔
392
                taskbar_hwnd = hwnd_attr()
1✔
393
                logger.debug(
1✔
394
                    "Reporter callable returned taskbar HWND: %s", taskbar_hwnd
395
                )
396
            except Exception as exc:  # pragma: no cover - defensive logging
397
                logger.debug(
398
                    "Failed to resolve reporter taskbar window handle: %s",
399
                    exc,
400
                    exc_info=True,
401
                )
UNCOV
402
        elif isinstance(hwnd_attr, int):
×
UNCOV
403
            taskbar_hwnd = hwnd_attr
×
UNCOV
404
            logger.debug("Reporter provided taskbar HWND attribute: %s", taskbar_hwnd)
×
405
        else:
UNCOV
406
            logger.debug("Reporter did not expose a taskbar HWND (attr=%r)", hwnd_attr)
×
407

408
        try:
1✔
409
            from .windows_taskbar import TaskbarProgress, TaskbarUnavailableError
1✔
410

411
            logger.debug(
1✔
412
                "Attempting to initialise Windows taskbar progress (hwnd=%s)",
413
                taskbar_hwnd,
414
            )
415
            taskbar = TaskbarProgress(hwnd=taskbar_hwnd)
1✔
416
            progress_reporter = TaskbarProgressReporter(base_reporter, taskbar)
1✔
417
            logger.debug("Windows taskbar progress initialised successfully")
1✔
UNCOV
418
        except (ImportError, TaskbarUnavailableError, OSError) as exc:
×
UNCOV
419
            logger.debug(
×
420
                "Windows taskbar progress unavailable, falling back to standard reporter: %s",
421
                exc,
422
                exc_info=True,
423
            )
424
        except Exception as exc:  # pragma: no cover - defensive logging
425
            logger.debug(
426
                "Unexpected failure initialising Windows taskbar progress, disabling integration: %s",
427
                exc,
428
                exc_info=True,
429
            )
430

431
    task_manager = progress_reporter.task(desc=desc, total=total, unit=unit)
1✔
432
    with task_manager as progress:
1✔
433
        while True:
1✔
434
            line = process.stderr.readline()
1✔
435
            if not line and process.poll() is not None:
1✔
436
                break
1✔
437

438
            if not line:
1✔
UNCOV
439
                continue
×
440

441
            # Filter out excessive progress output, only show important lines
442
            if any(
1✔
443
                keyword in line.lower()
444
                for keyword in [
445
                    "error",
446
                    "warning",
447
                    "encoded successfully",
448
                    "frame=",
449
                    "time=",
450
                    "size=",
451
                    "bitrate=",
452
                    "speed=",
453
                ]
454
            ):
455
                sys.stderr.write(line)
1✔
456
                sys.stderr.flush()
1✔
457

458
            # Send FFmpeg output to reporter for GUI display (filtered)
459
            if any(
1✔
460
                keyword in line.lower()
461
                for keyword in ["error", "warning", "encoded successfully", "frame="]
462
            ):
463
                progress_reporter.log(line.strip())
1✔
464

465
            match = re.search(r"frame=\s*(\d+)", line)
1✔
466
            if match:
1✔
467
                try:
1✔
468
                    new_frame = int(match.group(1))
1✔
469
                    progress.ensure_total(new_frame)
1✔
470
                    progress.advance(new_frame - progress.current)
1✔
UNCOV
471
                except (ValueError, IndexError):
×
UNCOV
472
                    pass
×
473

474
        process.wait()
1✔
475

476
        if process.returncode != 0:
1✔
UNCOV
477
            error_output = process.stderr.read()
×
UNCOV
478
            print(
×
479
                f"\nFFmpeg error (return code {process.returncode}):", file=sys.stderr
480
            )
UNCOV
481
            print(error_output, file=sys.stderr)
×
UNCOV
482
            raise subprocess.CalledProcessError(process.returncode, args)
×
483

484
        progress.finish()
1✔
485

486

487
def build_extract_audio_command(
1✔
488
    input_file: str,
489
    output_wav: str,
490
    sample_rate: int,
491
    audio_bitrate: str,
492
    hwaccel: Optional[List[str]] = None,
493
    ffmpeg_path: Optional[str] = None,
494
) -> str:
495
    """Build the FFmpeg command used to extract audio into a temporary WAV file."""
496

497
    hwaccel = hwaccel or []
1✔
498
    ffmpeg_path = ffmpeg_path or get_ffmpeg_path()
1✔
499
    command_parts: List[str] = [f'"{ffmpeg_path}"']
1✔
500
    command_parts.extend(hwaccel)
1✔
501
    command_parts.extend(
1✔
502
        [
503
            f'-i "{input_file}"',
504
            f"-ab {audio_bitrate} -ac 2",
505
            f"-ar {sample_rate}",
506
            "-vn",
507
            f'"{output_wav}"',
508
            "-hide_banner -loglevel warning -stats",
509
        ]
510
    )
511
    return " ".join(command_parts)
1✔
512

513

514
def build_video_commands(
1✔
515
    input_file: str,
516
    audio_file: str,
517
    filter_script: str,
518
    output_file: str,
519
    *,
520
    ffmpeg_path: Optional[str] = None,
521
    cuda_available: bool,
522
    small: bool,
523
    frame_rate: Optional[float] = None,
524
    keyframe_interval_seconds: float = 30.0,
525
    video_codec: str = "hevc",
526
) -> Tuple[str, Optional[str], bool]:
527
    """Create the FFmpeg command strings used to render the final video output.
528

529
    Args:
530
        frame_rate: Optional source frame rate used to size GOP/keyframe spacing for
531
            the small preset when generating hardware/software encoder commands.
532
    """
533

534
    ffmpeg_path = ffmpeg_path or get_ffmpeg_path()
1✔
535
    global_parts: List[str] = [f'"{ffmpeg_path}"', "-y"]
1✔
536
    hwaccel_args: List[str] = []
1✔
537

538
    if cuda_available and not small:
1✔
539
        hwaccel_args = ["-hwaccel", "cuda", "-hwaccel_output_format", "cuda"]
1✔
540
        global_parts.extend(hwaccel_args)
1✔
541
    elif small and cuda_available:
1✔
542
        pass
1✔
543

544
    input_parts = [f'-i "{input_file}"', f'-i "{audio_file}"']
1✔
545

546
    output_parts = [
1✔
547
        "-map 0 -map -0:a -map 1:a",
548
        f'-filter_script:v "{filter_script}"',
549
    ]
550

551
    codec_choice = (video_codec or "hevc").strip().lower()
1✔
552
    if codec_choice not in {"h264", "hevc", "av1"}:
1✔
UNCOV
553
        codec_choice = "hevc"
×
554

555
    video_encoder_args: List[str]
556
    fallback_encoder_args: List[str] = []
1✔
557
    use_cuda_encoder = False
1✔
558

559
    keyframe_args: List[str] = []
1✔
560
    if small:
1✔
561
        if keyframe_interval_seconds <= 0:
1✔
UNCOV
562
            keyframe_interval_seconds = 30.0
×
563
        formatted_interval = f"{keyframe_interval_seconds:.6g}"
1✔
564
        gop_size = 900
1✔
565
        if frame_rate and frame_rate > 0:
1✔
566
            gop_size = max(1, int(round(frame_rate * keyframe_interval_seconds)))
1✔
567
        keyframe_args = [
1✔
568
            f"-g {gop_size}",
569
            f"-keyint_min {gop_size}",
570
            f"-force_key_frames expr:gte(t,n_forced*{formatted_interval})",
571
        ]
572
    else:
573
        global_parts.append("-filter_complex_threads 1")
1✔
574

575
    if codec_choice == "av1":
1✔
576
        if encoder_available("libsvtav1", ffmpeg_path=ffmpeg_path):
1✔
577
            cpu_encoder_base = ["-c:v libsvtav1", "-preset 6", "-crf 28", "-b:v 0"]
1✔
578
        else:
579
            cpu_encoder_base = ["-c:v libaom-av1", "-crf 32", "-b:v 0", "-row-mt 1"]
1✔
580

581
        if small:
1✔
582
            cpu_encoder_args = cpu_encoder_base + keyframe_args
1✔
583
        else:
584
            cpu_encoder_args = cpu_encoder_base
1✔
585

586
        if cuda_available and encoder_available("av1_nvenc", ffmpeg_path=ffmpeg_path):
1✔
587
            use_cuda_encoder = True
1✔
588
            video_encoder_args = [
1✔
589
                "-c:v av1_nvenc",
590
                "-preset p6",
591
                "-rc vbr",
592
                "-b:v 0",
593
                "-cq 36",
594
                "-spatial-aq 1",
595
                "-temporal-aq 1",
596
            ]
597
            if small:
1✔
598
                video_encoder_args = video_encoder_args + keyframe_args
1✔
599
            fallback_encoder_args = cpu_encoder_args
1✔
600
        else:
601
            video_encoder_args = cpu_encoder_args
1✔
602
    elif codec_choice == "hevc":
1✔
603
        if small:
1✔
604
            cpu_encoder_args = [
1✔
605
                "-c:v libx265",
606
                "-preset medium",
607
                "-crf 28",
608
            ] + keyframe_args
609
        else:
610
            cpu_encoder_args = ["-c:v libx265", "-preset medium", "-crf 26"]
1✔
611

612
        if cuda_available and encoder_available("hevc_nvenc", ffmpeg_path=ffmpeg_path):
1✔
613
            use_cuda_encoder = True
1✔
614
            video_encoder_args = [
1✔
615
                "-c:v hevc_nvenc",
616
                "-preset p6",
617
                "-rc vbr",
618
                "-b:v 0",
619
                "-cq 32",
620
                "-spatial-aq 1",
621
                "-temporal-aq 1",
622
                "-rc-lookahead 32",
623
                "-multipass fullres",
624
            ]
625
            if small:
1✔
626
                video_encoder_args = video_encoder_args + keyframe_args
1✔
627
            fallback_encoder_args = cpu_encoder_args
1✔
628
        else:
629
            video_encoder_args = cpu_encoder_args
1✔
630
    else:
631
        # Fallback to H.264
UNCOV
632
        if small:
×
UNCOV
633
            cpu_encoder_args = [
×
634
                "-c:v libx264",
635
                "-preset veryfast",
636
                "-crf 24",
637
                "-tune",
638
                "zerolatency",
639
            ] + keyframe_args
UNCOV
640
            if cuda_available:
×
UNCOV
641
                use_cuda_encoder = True
×
UNCOV
642
                video_encoder_args = [
×
643
                    "-c:v h264_nvenc",
644
                    "-preset p1",
645
                    "-cq 28",
646
                    "-tune",
647
                    "ll",
648
                    "-forced-idr 1",
649
                ] + keyframe_args
UNCOV
650
                fallback_encoder_args = cpu_encoder_args
×
651
            else:
UNCOV
652
                video_encoder_args = cpu_encoder_args
×
653
        else:
UNCOV
654
            software_args = ["-c:v libx264", "-preset veryfast", "-crf 23"]
×
UNCOV
655
            if cuda_available:
×
UNCOV
656
                use_cuda_encoder = True
×
UNCOV
657
                video_encoder_args = ["-c:v h264_nvenc", "-preset p1", "-cq 23"]
×
UNCOV
658
                fallback_encoder_args = software_args
×
659
            else:
UNCOV
660
                video_encoder_args = software_args
×
661

662
    audio_parts = [
1✔
663
        "-c:a aac",
664
        f'"{output_file}"',
665
        "-loglevel warning -stats -hide_banner",
666
    ]
667

668
    full_command_parts = (
1✔
669
        global_parts + input_parts + output_parts + video_encoder_args + audio_parts
670
    )
671
    command_str = " ".join(full_command_parts)
1✔
672

673
    fallback_command_str: Optional[str] = None
1✔
674
    if fallback_encoder_args:
1✔
675
        fallback_global_parts = list(global_parts)
1✔
676
        if hwaccel_args:
1✔
UNCOV
677
            fallback_global_parts = [
×
678
                part for part in fallback_global_parts if part not in hwaccel_args
679
            ]
680
        fallback_parts = (
1✔
681
            fallback_global_parts
682
            + input_parts
683
            + output_parts
684
            + fallback_encoder_args
685
            + audio_parts
686
        )
687
        fallback_command_str = " ".join(fallback_parts)
1✔
688

689
    return command_str, fallback_command_str, use_cuda_encoder
1✔
690

691

692
__all__ = [
1✔
693
    "FFmpegNotFoundError",
694
    "find_ffmpeg",
695
    "find_ffprobe",
696
    "get_ffmpeg_path",
697
    "get_ffprobe_path",
698
    "check_cuda_available",
699
    "run_timed_ffmpeg_command",
700
    "build_extract_audio_command",
701
    "build_video_commands",
702
    "shutil_which",
703
]
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc