Skip to content

Commit

Permalink
Only encode video streams once
Browse files Browse the repository at this point in the history
  • Loading branch information
WyattBlue committed Oct 16, 2024
1 parent 578a145 commit c3027ae
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 73 deletions.
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:/WyattBlue/auto-editor/compare/25.3.1...26.0.0


# 25.3.1

## Features
Expand Down

0 comments on commit c3027ae

Please sign in to comment.