diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 19e2ba2a2..7178232b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,13 +48,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.10" - - name: Install FFmpeg - run: | - sudo apt update - sudo apt install ffmpeg - name: Install Auto-Editor run: pip install -e . + - name: Run Debug + run: auto-editor --debug - name: Test - run: | - auto-editor --debug - auto-editor test all + run: auto-editor test all diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d14cafdd..c8a3fbbb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 26.1.0 + +## Features + - Use PyAV 14. + +## Fixes + - Remove `--ffmpeg-location` arg. + - Remove help text for recently removed args. + +**Full Changelog**: https://github.com/WyattBlue/auto-editor/compare/26.0.1...26.1.0 + + # 26.0.1 ## Fixes @@ -7,7 +19,7 @@ - Remove the `ae-ffmpeg` package dependency. - Remove unused args, functions. - **Full Changelog**: https://github.com/WyattBlue/auto-editor/compare/26.0.0...26.0.1 +**Full Changelog**: https://github.com/WyattBlue/auto-editor/compare/26.0.0...26.0.1 # 26.0.0 diff --git a/auto_editor/__init__.py b/auto_editor/__init__.py index fdfe29e79..67d00754f 100644 --- a/auto_editor/__init__.py +++ b/auto_editor/__init__.py @@ -1 +1 @@ -__version__ = "26.0.1" +__version__ = "26.1.0" diff --git a/auto_editor/__main__.py b/auto_editor/__main__.py index 3c5a38e43..3e34a8064 100755 --- a/auto_editor/__main__.py +++ b/auto_editor/__main__.py @@ -161,11 +161,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser: metavar="PATH", help="Set where the temporary directory is located", ) - parser.add_argument( - "--ffmpeg-location", - metavar="PATH", - help="Set a custom path to the ffmpeg location", - ) parser.add_text("Display Options:") parser.add_argument( "--progress", @@ -251,7 +246,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser: return parser -def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str: +def download_video(my_input: str, args: Args, log: Log) -> str: log.conwrite("Downloading video...") def get_domain(url: str) -> str: @@ -267,18 +262,14 @@ def get_domain(url: str) -> str: else: output_format = args.output_format - yt_dlp_path = args.yt_dlp_location - - cmd = ["--ffmpeg-location", ffmpeg.get_path("yt-dlp", log)] - if download_format is not None: cmd.extend(["-f", download_format]) cmd.extend(["-o", output_format, my_input]) - if args.yt_dlp_extras is not None: cmd.extend(args.yt_dlp_extras.split(" ")) + yt_dlp_path = args.yt_dlp_location try: location = get_stdout( [yt_dlp_path, "--get-filename", "--no-warnings"] + cmd @@ -347,11 +338,10 @@ def main() -> None: is_machine = args.progress == "machine" log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color) - ffmpeg = FFmpeg(args.ffmpeg_location) paths = [] for my_input in args.input: if my_input.startswith("http://") or my_input.startswith("https://"): - paths.append(download_video(my_input, args, ffmpeg, log)) + paths.append(download_video(my_input, args, log)) else: if not splitext(my_input)[1]: if isdir(my_input): @@ -365,7 +355,7 @@ def main() -> None: paths.append(my_input) try: - edit_media(paths, ffmpeg, args, log) + edit_media(paths, args, log) except KeyboardInterrupt: log.error("Keyboard Interrupt") log.cleanup() diff --git a/auto_editor/edit.py b/auto_editor/edit.py index 7bddd5561..681d5b47f 100644 --- a/auto_editor/edit.py +++ b/auto_editor/edit.py @@ -10,7 +10,7 @@ import av from av import AudioResampler -from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo +from auto_editor.ffwrapper import FileInfo, initFileInfo from auto_editor.lib.contracts import is_int, is_str from auto_editor.make_layers import clipify, make_av, make_timeline from auto_editor.output import Ensure, parse_bitrate @@ -160,7 +160,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]: log.error(f"'{name}': Export must be [{', '.join([s for s in parsing.keys()])}]") -def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None: +def edit_media(paths: list[str], args: Args, log: Log) -> None: bar = initBar(args.progress) tl = None @@ -294,7 +294,7 @@ def make_media(tl: v3, output_path: str) -> None: if ctr.default_aud != "none": ensure = Ensure(bar, samplerate, log) - audio_paths = make_new_audio(tl, ctr, ensure, args, ffmpeg, bar, log) + audio_paths = make_new_audio(tl, ctr, ensure, args, bar, log) else: audio_paths = [] @@ -343,8 +343,8 @@ def make_media(tl: v3, output_path: str) -> None: for i, sub_path in enumerate(sub_paths): subtitle_input = av.open(sub_path) subtitle_inputs.append(subtitle_input) - subtitle_stream = output.add_stream( - template=subtitle_input.streams.subtitles[0] + subtitle_stream = output.add_stream_from_template( + subtitle_input.streams.subtitles[0] ) if i < len(src.subtitles) and src.subtitles[i].lang is not None: subtitle_stream.metadata["language"] = src.subtitles[i].lang # type: ignore diff --git a/auto_editor/ffwrapper.py b/auto_editor/ffwrapper.py index 9a162367e..c8c91f88f 100644 --- a/auto_editor/ffwrapper.py +++ b/auto_editor/ffwrapper.py @@ -11,32 +11,6 @@ from auto_editor.utils.log import Log -def _get_ffmpeg(reason: str, ffloc: str | None, log: Log) -> str: - program = "ffmpeg" if ffloc is None else ffloc - if (path := which(program)) is None: - log.error(f"{reason} needs ffmpeg cli but couldn't find ffmpeg on PATH.") - return path - - -@dataclass(slots=True) -class FFmpeg: - ffmpeg_location: str | None - path: str | None = None - - def get_path(self, reason: str, log: Log) -> str: - if self.path is not None: - return self.path - - self.path = _get_ffmpeg(reason, self.ffmpeg_location, log) - return self.path - - def Popen(self, reason: str, cmd: list[str], log: Log) -> Popen: - if self.path is None: - self.path = _get_ffmpeg(reason, self.ffmpeg_location, log) - - return Popen([self.path] + cmd, stdout=PIPE, stderr=PIPE) - - def mux(input: Path, output: Path, stream: int) -> None: input_container = av.open(input, "r") output_container = av.open(output, "w") diff --git a/auto_editor/render/audio.py b/auto_editor/render/audio.py index be04b7357..117e6ccf8 100644 --- a/auto_editor/render/audio.py +++ b/auto_editor/render/audio.py @@ -2,12 +2,12 @@ import io from pathlib import Path -from platform import system import av import numpy as np +from av.filter.loudnorm import stats -from auto_editor.ffwrapper import FFmpeg, FileInfo +from auto_editor.ffwrapper import FileInfo from auto_editor.lang.json import Lexer, Parser from auto_editor.lang.palet import env from auto_editor.lib.contracts import andc, between_c, is_int_or_float @@ -56,25 +56,11 @@ def parse_norm(norm: str, log: Log) -> dict | None: log.error(e) -def parse_ebu_bytes(norm: dict, stderr: bytes, log: Log) -> tuple[str, str]: - start = end = 0 - lines = stderr.splitlines() - - for index, line in enumerate(lines): - if line.startswith(b"[Parsed_loudnorm"): - start = index + 1 - continue - if start != 0 and line.startswith(b"}"): - end = index + 1 - break - - if start == 0 or end == 0: - log.error(f"Invalid loudnorm stats.\n{stderr!r}") - +def parse_ebu_bytes(norm: dict, stat: bytes, log: Log) -> tuple[str, str]: try: - parsed = Parser(Lexer("loudnorm", b"\n".join(lines[start:end]))).expr() + parsed = Parser(Lexer("loudnorm", stat)).expr() except MyError: - log.error(f"Invalid loudnorm stats.\n{start=},{end=}\n{stderr!r}") + log.error(f"Invalid loudnorm stats.\n{stat!r}") for key in ("input_i", "input_tp", "input_lra", "input_thresh", "target_offset"): val = float(parsed[key]) @@ -101,29 +87,17 @@ def parse_ebu_bytes(norm: dict, stderr: bytes, log: Log) -> tuple[str, str]: def apply_audio_normalization( - ffmpeg: FFmpeg, norm: dict, pre_master: Path, path: Path, log: Log + norm: dict, pre_master: Path, path: Path, log: Log ) -> None: if norm["tag"] == "ebu": first_pass = ( - f"loudnorm=i={norm['i']}:lra={norm['lra']}:tp={norm['tp']}:" - f"offset={norm['gain']}:print_format=json" + f"i={norm['i']}:lra={norm['lra']}:tp={norm['tp']}:" f"offset={norm['gain']}" ) log.debug(f"audio norm first pass: {first_pass}") - file_null = "NUL" if system() in ("Windows", "cli") else "/dev/null" - cmd = [ - "-hide_banner", - "-i", - f"{pre_master}", - "-af", - first_pass, - "-vn", - "-sn", - "-f", - "null", - file_null, - ] - stderr = ffmpeg.Popen("EBU", cmd, log).communicate()[1] - name, filter_args = parse_ebu_bytes(norm, stderr, log) + with av.open(f"{pre_master}") as container: + stats_ = stats(first_pass, container.streams.audio[0]) + av.logging.set_level(None) + name, filter_args = parse_ebu_bytes(norm, stats_, log) else: assert "t" in norm @@ -310,13 +284,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None: def make_new_audio( - tl: v3, - ctr: Container, - ensure: Ensure, - args: Args, - ffmpeg: FFmpeg, - bar: Bar, - log: Log, + tl: v3, ctr: Container, ensure: Ensure, args: Args, bar: Bar, log: Log ) -> list[str]: sr = tl.sr tb = tl.tb @@ -390,7 +358,7 @@ def make_new_audio( with open(pre_master, "wb") as fid: write(fid, sr, arr) - apply_audio_normalization(ffmpeg, norm, pre_master, path, log) + apply_audio_normalization(norm, pre_master, path, log) bar.end() diff --git a/auto_editor/render/subtitle.py b/auto_editor/render/subtitle.py index 1da25216e..2022e7bc8 100644 --- a/auto_editor/render/subtitle.py +++ b/auto_editor/render/subtitle.py @@ -162,7 +162,7 @@ def _ensure(input_: Input, format: str, stream: int) -> str: output = av.open(output_bytes, "w", format=format) in_stream = input_.streams.subtitles[stream] - out_stream = output.add_stream(template=in_stream) + out_stream = output.add_stream_from_template(in_stream) for packet in input_.demux(in_stream): if packet.dts is None: diff --git a/auto_editor/utils/types.py b/auto_editor/utils/types.py index 35fc15667..9bd60a653 100644 --- a/auto_editor/utils/types.py +++ b/auto_editor/utils/types.py @@ -217,7 +217,6 @@ class Args: player: str | None = None no_open: bool = False temp_dir: str | None = None - ffmpeg_location: str | None = None progress: str = "modern" version: bool = False debug: bool = False diff --git a/pyproject.toml b/pyproject.toml index 1f66d3a5c..d5d0bb0ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,8 @@ license = { text = "Unlicense" } authors = [{ name = "WyattBlue", email = "wyattblue@auto-editor.com" }] requires-python = ">=3.10,<3.14" dependencies = [ - "numpy>=1.23.0,<3.0", - "pyav==13.1.*", + "numpy>=1.24,<3.0", + "pyav==14.0.0rc4", ] keywords = [ "video", "audio", "media", "editor", "editing",