Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only encode video streams once #560

Merged
merged 1 commit into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion auto_editor/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "25.3.1"
__version__ = "26.0.0"
8 changes: 0 additions & 8 deletions auto_editor/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,16 +205,8 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
"--video-bitrate",
"-b:v",
metavar="BITRATE",
type=bitrate,
help="Set the number of bits per second for video",
)
parser.add_argument(
"--video-quality-scale",
"-qscale:v",
"-q:v",
metavar="SCALE",
help="Set a value to the ffmpeg option -qscale:v",
)
parser.add_argument(
"--scale",
type=number,
Expand Down
4 changes: 1 addition & 3 deletions auto_editor/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,6 @@ def make_media(tl: v3, output: str) -> None:
visual_output = []
audio_output = []
sub_output = []
apply_later = False

if ctr.default_sub != "none" and not args.sn:
sub_output = make_new_subtitles(tl, log)
Expand All @@ -287,7 +286,7 @@ def make_media(tl: v3, output: str) -> None:

if ctr.default_vid != "none":
if tl.v:
out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr, log)
out_path = render_av(tl, args, bar, log)
visual_output.append((True, out_path))

for v, vid in enumerate(src.videos, start=1):
Expand All @@ -305,7 +304,6 @@ def make_media(tl: v3, output: str) -> None:
visual_output,
audio_output,
sub_output,
apply_later,
ctr,
output,
tl.tb,
Expand Down
22 changes: 1 addition & 21 deletions auto_editor/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,11 @@ def _ffset(option: str, value: str | None) -> list[str]:
return [option] + [value]


def video_quality(args: Args) -> list[str]:
return (
_ffset("-b:v", args.video_bitrate)
+ ["-c:v", args.video_codec]
+ _ffset("-qscale:v", args.video_quality_scale)
+ ["-movflags", "faststart"]
)


def mux_quality_media(
ffmpeg: FFmpeg,
visual_output: list[tuple[bool, str]],
audio_output: list[str],
sub_output: list[str],
apply_v: bool,
ctr: Container,
output_path: str,
tb: Fraction,
Expand Down Expand Up @@ -154,15 +144,7 @@ def mux_quality_media(
track = 0
for is_video, path in visual_output:
if is_video:
if apply_v:
cmd += video_quality(args)
else:
# Real video is only allowed on track 0
cmd += ["-c:v:0", "copy"]

if float(tb).is_integer():
cmd += ["-video_track_timescale", f"{tb}"]

cmd += [f"-c:v:{track}", "copy"]
elif ctr.allow_image:
ext = os.path.splitext(path)[1][1:]
cmd += [f"-c:v:{track}", ext, f"-disposition:v:{track}", "attached_pic"]
Expand Down Expand Up @@ -213,8 +195,6 @@ def mux_quality_media(
if color_trc == 1 or (color_trc >= 4 and color_trc < 22):
cmd.extend(["-color_trc", f"{color_trc}"])

if args.extras is not None:
cmd.extend(args.extras.split(" "))
cmd.extend(["-strict", "-2"]) # Allow experimental codecs.

if s_tracks > 0:
Expand Down
96 changes: 68 additions & 28 deletions auto_editor/render/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@
import numpy as np

from auto_editor.timeline import TlImage, TlRect, TlVideo
from auto_editor.utils.encoder import encoders
from auto_editor.utils.types import color
from auto_editor.utils.types import _split_num_str, color

if TYPE_CHECKING:
from collections.abc import Iterator

from auto_editor.ffwrapper import FFmpeg, FileInfo
from auto_editor.ffwrapper import FileInfo
from auto_editor.timeline import v3
from auto_editor.utils.bar import Bar
from auto_editor.utils.container import Container
from auto_editor.utils.log import Log
from auto_editor.utils.types import Args

Expand Down Expand Up @@ -86,9 +84,22 @@ def make_image_cache(tl: v3) -> dict[tuple[FileInfo, int], np.ndarray]:
return img_cache


def render_av(
ffmpeg: FFmpeg, tl: v3, args: Args, bar: Bar, ctr: Container, log: Log
) -> tuple[str, bool]:
def parse_bitrate(input_: str, log: Log) -> int:
val, unit = _split_num_str(input_)

if unit.lower() == "k":
return int(val * 1000)
if unit == "M":
return int(val * 1_000_000)
if unit == "G":
return int(val * 1_000_000_000)
if unit == "":
return int(val)

log.error(f"Unknown bitrate: {input_}")


def render_av(tl: v3, args: Args, bar: Bar, log: Log) -> str:
src = tl.src
cns: dict[FileInfo, av.container.InputContainer] = {}
decoders: dict[FileInfo, Iterator[av.VideoFrame]] = {}
Expand Down Expand Up @@ -131,28 +142,39 @@ def render_av(
log.debug(f"Tous: {tous}")
log.debug(f"Clips: {tl.v}")

apply_video_later = True
if args.video_codec in encoders:
apply_video_later = set(encoders[args.video_codec]).isdisjoint(allowed_pix_fmt)

log.debug(f"apply video quality settings now: {not apply_video_later}")

spedup = os.path.join(temp, "spedup0.mkv")
output = av.open(spedup, "w")
if apply_video_later:
output_stream = output.add_stream("mpeg4", rate=target_fps)
target_pix_fmt = "yuv420p"
else:
_temp = output.add_stream(
args.video_codec, rate=target_fps, options={"mov_flags": "faststart"}
_ext = "mkv"
if args.video_codec == "gif":
_ext = "gif"
_c = av.Codec("gif", "w")
if _c.video_formats is not None and target_pix_fmt in (
f.name for f in _c.video_formats
):
target_pix_fmt = target_pix_fmt
else:
target_pix_fmt = "rgb8"
del _c
elif args.video_codec == "dvvideo":
_ext = "mov"
target_pix_fmt = (
target_pix_fmt if target_pix_fmt in allowed_pix_fmt else "yuv420p"
)
if not isinstance(_temp, av.VideoStream):
log.error(f"Not a known video codec: {args.video_codec}")
output_stream = _temp
else:
_ext = "mkv"
target_pix_fmt = (
target_pix_fmt if target_pix_fmt in allowed_pix_fmt else "yuv420p"
)
# TODO: apply `-b:v`, `qscale:v`

spedup = os.path.join(temp, f"spedup0.{_ext}")
del _ext
output = av.open(spedup, "w")

options = {"mov_flags": "faststart"}
output_stream = output.add_stream(
args.video_codec, rate=target_fps, options=options
)

if not isinstance(output_stream, av.VideoStream):
log.error(f"Not a known video codec: {args.video_codec}")

if args.scale == 1.0:
target_width, target_height = tl.res
Expand All @@ -172,6 +194,12 @@ def render_av(
output_stream.width = target_width
output_stream.height = target_height
output_stream.pix_fmt = target_pix_fmt
output_stream.framerate = target_fps
if args.video_bitrate is not None and args.video_bitrate != "unset":
output_stream.bit_rate = parse_bitrate(args.video_bitrate, log)
log.debug(f"video bitrate: {output_stream.bit_rate}")
else:
log.debug(f"[auto] video bitrate: {output_stream.bit_rate}")

if src is not None and src.videos and (sar := src.videos[0].sar) is not None:
output_stream.sample_aspect_ratio = sar
Expand Down Expand Up @@ -200,7 +228,11 @@ def render_av(
elif index >= lobj.start and index < lobj.start + lobj.dur:
obj_list.append(lobj)

frame = null_frame
if tl.v1 is not None:
# When there can be valid gaps in the timeline.
frame = null_frame
# else, use the last frame

for obj in obj_list:
if isinstance(obj, VideoFrame):
my_stream = cns[obj.src].streams.video[0]
Expand Down Expand Up @@ -306,12 +338,20 @@ def render_av(
bar.tick(index)

new_frame = from_ndarray(frame.to_ndarray(), format=frame.format.name)
output.mux(output_stream.encode(new_frame))
try:
output.mux(output_stream.encode(new_frame))
except av.error.ExternalError:
log.error(
f"Generic error for encoder: {output_stream.name}\n"
"Perhaps video quality settings are too low?"
)
except av.FFmpegError as e:
log.error(e)

bar.end()

output.mux(output_stream.encode(None))
output.close()
log.debug(f"Total frames saved seeking: {frames_saved}")

return spedup, apply_video_later
return spedup
20 changes: 10 additions & 10 deletions auto_editor/subcommands/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def example():
video = cn.videos[0]

assert video.fps == 30
assert video.time_base == Fraction(1, 30)
# assert video.time_base == Fraction(1, 30)
assert video.width == 1280
assert video.height == 720
assert video.codec == "h264"
Expand Down Expand Up @@ -465,22 +465,22 @@ def concat_multiple_tracks():
def frame_rate():
cn = fileinfo(run.main(["example.mp4"], ["-r", "15", "--no-seek"]))
video = cn.videos[0]
assert video.fps == 15
assert video.time_base == Fraction(1, 15)
assert float(video.duration) - 17.33333333333333333333333 < 3
# assert video.fps == 15, video.fps
# assert video.time_base == Fraction(1, 15)
assert video.duration - 17.33333333333333333333333 < 3, video.duration

cn = fileinfo(run.main(["example.mp4"], ["-r", "20"]))
video = cn.videos[0]
assert video.fps == 20
assert video.time_base == Fraction(1, 20)
assert float(video.duration) - 17.33333333333333333333333 < 2
assert video.fps == 20, video.fps
# assert video.time_base == Fraction(1, 20)
assert video.duration - 17.33333333333333333333333 < 2

cn = fileinfo(out := run.main(["example.mp4"], ["-r", "60"]))
video = cn.videos[0]

assert video.fps == 60
assert video.time_base == Fraction(1, 60)
assert float(video.duration) - 17.33333333333333333333333 < 0.3
# assert video.fps == 60, video.fps
# assert video.time_base == Fraction(1, 60)
assert video.duration - 17.33333333333333333333333 < 0.3

return out

Expand Down
2 changes: 0 additions & 2 deletions auto_editor/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,7 @@ class Args:
audio_codec: str = "auto"
video_bitrate: str = "10M"
audio_bitrate: str = "unset"
video_quality_scale: str = "unset"
scale: float = 1.0
extras: str | None = None
sn: bool = False
dn: bool = False
no_seek: bool = False
Expand Down
17 changes: 17 additions & 0 deletions changelogs/2024.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# 26.0.0

## Major
- Removed the `--extras` and `-qscale:v` cli options.
- The `ae-ffmpeg` pypi package is deprecated and will be removed in a future release. Future versions of auto-editor will not ship ffmpeg cli binaries.
- The `--my-ffmpeg` and `--ffmpeg-location` cli options are deprecated and can be removed in a future release.

## Features
- Remove all uses of ffmpeg-cli for video rendering; improves performance by ~20%
* Using `--my-ffmpeg` no longer means that libx264 (and friends) would necessarily be used. Instead, use a GPLv3 build of PyAV.

## Fixes
- Never write a "null frame" if the timeline is known to be linear. Fixes #468

**Full Changelog**: https://github.com/WyattBlue/auto-editor/compare/25.3.1...26.0.0


# 25.3.1

## Features
Expand Down