Skip to content

Commit

Permalink
Use PyAV only for muxing
Browse files Browse the repository at this point in the history
  • Loading branch information
WyattBlue committed Oct 21, 2024
1 parent 184f8b0 commit dfc9912
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 199 deletions.
43 changes: 9 additions & 34 deletions auto_editor/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
from auto_editor.lib.contracts import is_int, is_str
from auto_editor.make_layers import make_timeline
from auto_editor.output import Ensure, mux_quality_media
from auto_editor.output import Ensure
from auto_editor.render.audio import make_new_audio
from auto_editor.render.subtitle import make_new_subtitles
from auto_editor.render.video import render_av
Expand Down Expand Up @@ -273,25 +273,24 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
def make_media(tl: v3, output: str) -> None:
assert src is not None

visual_output = []
audio_output = []
sub_output = []
audio_inputs = []
sub_inputs = []

if ctr.default_sub != "none" and not args.sn:
sub_output = make_new_subtitles(tl, log)
sub_inputs = make_new_subtitles(tl, log)

if ctr.default_aud != "none":
ensure = Ensure(bar, samplerate, log)
audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, log)
atracks = len(audio_output)
audio_inputs = make_new_audio(tl, ensure, args, ffmpeg, bar, log)
atracks = len(audio_inputs)
if (
not (args.keep_tracks_separate and ctr.max_audios is None)
and atracks > 1
):
# Merge all the audio a_tracks into one.
new_a_file = os.path.join(log.temp, "new_audio.wav")
new_cmd = []
for path in audio_output:
for path in audio_inputs:
new_cmd.extend(["-i", path])
new_cmd.extend(
[
Expand All @@ -303,35 +302,11 @@ def make_media(tl: v3, output: str) -> None:
]
)
ffmpeg.run(new_cmd)
audio_output = [new_a_file]
audio_inputs = [new_a_file]

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

for v, vid in enumerate(src.videos, start=1):
if ctr.allow_image and vid.codec in ("png", "mjpeg", "webp"):
out_path = os.path.join(log.temp, f"{v}.{vid.codec}")
# fmt: off
ffmpeg.run(["-i", f"{src.path}", "-map", "0:v", "-map", "-0:V",
"-c", "copy", out_path])
# fmt: on
visual_output.append((False, out_path))

log.conwrite("Writing output file")
mux_quality_media(
ffmpeg,
visual_output,
audio_output,
sub_output,
ctr,
output,
tl.tb,
args,
src,
log,
)
render_av(output, audio_inputs, sub_inputs, tl, args, bar, log)

if export == "clip-sequence":
if tl.v1 is None:
Expand Down
8 changes: 2 additions & 6 deletions auto_editor/ffwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,9 @@ def mux(input: Path, output: Path, stream: int) -> None:
output_audio_stream = output_container.add_stream("pcm_s16le")

for frame in input_container.decode(input_audio_stream):
packet = output_audio_stream.encode(frame)
if packet:
output_container.mux(packet)
output_container.mux(output_audio_stream.encode(frame))

packet = output_audio_stream.encode(None)
if packet:
output_container.mux(packet)
output_container.mux(output_audio_stream.encode(None))

output_container.close()
input_container.close()
Expand Down
148 changes: 6 additions & 142 deletions auto_editor/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@

import os.path
from dataclasses import dataclass, field
from fractions import Fraction
from re import search
from subprocess import PIPE

import av
from av.audio.resampler import AudioResampler

from auto_editor.ffwrapper import FFmpeg, FileInfo
from auto_editor.ffwrapper import FileInfo
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


@dataclass(slots=True)
Expand Down Expand Up @@ -52,152 +47,21 @@ def audio(self, src: FileInfo, stream: int) -> str:

bar.start(dur, "Extracting audio")

# PyAV always uses "stereo" layout, which is what we want.
output_astream = out_container.add_stream("pcm_s16le", rate=sample_rate)
assert isinstance(output_astream, av.audio.stream.AudioStream)

output_astream = out_container.add_stream(
"pcm_s16le", layout="stereo", rate=sample_rate
)
resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate)
for i, frame in enumerate(in_container.decode(astream)):
if i % 1500 == 0 and frame.time is not None:
bar.tick(frame.time)

for new_frame in resampler.resample(frame):
for packet in output_astream.encode(new_frame):
out_container.mux_one(packet)
out_container.mux(output_astream.encode(new_frame))

for packet in output_astream.encode():
out_container.mux_one(packet)
out_container.mux(output_astream.encode(None))

out_container.close()
in_container.close()
bar.end()

return out_path


def _ffset(option: str, value: str | None) -> list[str]:
if value is None or value == "unset" or value == "reserved":
return []
return [option] + [value]


def mux_quality_media(
ffmpeg: FFmpeg,
visual_output: list[tuple[bool, str]],
audio_output: list[str],
sub_output: list[str],
ctr: Container,
output_path: str,
tb: Fraction,
args: Args,
src: FileInfo,
log: Log,
) -> None:
v_tracks = len(visual_output)
a_tracks = len(audio_output)
s_tracks = 0 if args.sn else len(sub_output)

cmd = ["-hide_banner", "-y"]

same_container = src.path.suffix == os.path.splitext(output_path)[1]

for is_video, path in visual_output:
if is_video or ctr.allow_image:
cmd.extend(["-i", path])
else:
v_tracks -= 1

for audfile in audio_output:
cmd.extend(["-i", audfile])

for subfile in sub_output:
cmd.extend(["-i", subfile])

for i in range(v_tracks + s_tracks + a_tracks):
cmd.extend(["-map", f"{i}:0"])

cmd.extend(["-map_metadata", "0"])

track = 0
for is_video, path in visual_output:
if is_video:
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"]

track += 1
del track

for i, vstream in enumerate(src.videos):
if i > v_tracks:
break
if vstream.lang is not None:
cmd.extend([f"-metadata:s:v:{i}", f"language={vstream.lang}"])
for i, astream in enumerate(src.audios):
if i > a_tracks:
break
if astream.lang is not None:
cmd.extend([f"-metadata:s:a:{i}", f"language={astream.lang}"])
for i, sstream in enumerate(src.subtitles):
if i > s_tracks:
break
if sstream.lang is not None:
cmd.extend([f"-metadata:s:s:{i}", f"language={sstream.lang}"])

if s_tracks > 0:
scodec = src.subtitles[0].codec
if same_container:
cmd.extend(["-c:s", scodec])
elif ctr.scodecs is not None:
if scodec not in ctr.scodecs:
scodec = ctr.default_sub
cmd.extend(["-c:s", scodec])

if a_tracks > 0:
cmd += _ffset("-c:a", args.audio_codec) + _ffset("-b:a", args.audio_bitrate)

if same_container and v_tracks > 0:
color_range = src.videos[0].color_range
colorspace = src.videos[0].color_space
color_prim = src.videos[0].color_primaries
color_trc = src.videos[0].color_transfer

if color_range == 1 or color_range == 2:
cmd.extend(["-color_range", f"{color_range}"])
if colorspace in (0, 1) or (colorspace >= 3 and colorspace < 16):
cmd.extend(["-colorspace", f"{colorspace}"])
if color_prim == 1 or (color_prim >= 4 and color_prim < 17):
cmd.extend(["-color_primaries", f"{color_prim}"])
if color_trc == 1 or (color_trc >= 4 and color_trc < 22):
cmd.extend(["-color_trc", f"{color_trc}"])

cmd.extend(["-strict", "-2"]) # Allow experimental codecs.

if s_tracks > 0:
cmd.extend(["-map", "0:t?"]) # Add input attachments to output.

if not args.dn:
cmd.extend(["-map", "0:d?"])

cmd.append(output_path)

process = ffmpeg.Popen(cmd, stdout=PIPE, stderr=PIPE)
stderr = process.communicate()[1].decode("utf-8", "replace")
error_list = (
r"Unknown encoder '.*'",
r"-q:v qscale not available for encoder\. Use -b:v bitrate instead\.",
r"Specified sample rate .* is not supported",
r'Unable to parse option value ".*"',
r"Error setting option .* to value .*\.",
r"DLL .* failed to open",
r"Incompatible pixel format '.*' for codec '[A-Za-z0-9_]*'",
r"Unrecognized option '.*'",
r"Permission denied",
)
for item in error_list:
if check := search(item, stderr):
log.error(check.group())

if not os.path.isfile(output_path):
log.error(f"The file {output_path} was not created.")
Loading

0 comments on commit dfc9912

Please sign in to comment.