auto-editor 25.3.1__py3-none-any.whl → 26.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +1 -11
- auto_editor/edit.py +156 -44
- auto_editor/ffwrapper.py +2 -43
- auto_editor/help.py +4 -3
- auto_editor/output.py +22 -183
- auto_editor/render/audio.py +65 -55
- auto_editor/render/subtitle.py +69 -10
- auto_editor/render/video.py +166 -180
- auto_editor/subcommands/repl.py +12 -3
- auto_editor/subcommands/test.py +41 -37
- auto_editor/utils/container.py +2 -0
- auto_editor/utils/func.py +1 -1
- auto_editor/utils/types.py +2 -15
- {auto_editor-25.3.1.dist-info → auto_editor-26.0.0.dist-info}/METADATA +1 -1
- {auto_editor-25.3.1.dist-info → auto_editor-26.0.0.dist-info}/RECORD +20 -20
- {auto_editor-25.3.1.dist-info → auto_editor-26.0.0.dist-info}/WHEEL +1 -1
- {auto_editor-25.3.1.dist-info → auto_editor-26.0.0.dist-info}/LICENSE +0 -0
- {auto_editor-25.3.1.dist-info → auto_editor-26.0.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-25.3.1.dist-info → auto_editor-26.0.0.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "
|
1
|
+
__version__ = "26.0.0"
|
auto_editor/__main__.py
CHANGED
@@ -13,7 +13,6 @@ from auto_editor.utils.func import get_stdout
|
|
13
13
|
from auto_editor.utils.log import Log
|
14
14
|
from auto_editor.utils.types import (
|
15
15
|
Args,
|
16
|
-
bitrate,
|
17
16
|
color,
|
18
17
|
frame_rate,
|
19
18
|
margin,
|
@@ -205,16 +204,8 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
205
204
|
"--video-bitrate",
|
206
205
|
"-b:v",
|
207
206
|
metavar="BITRATE",
|
208
|
-
type=bitrate,
|
209
207
|
help="Set the number of bits per second for video",
|
210
208
|
)
|
211
|
-
parser.add_argument(
|
212
|
-
"--video-quality-scale",
|
213
|
-
"-qscale:v",
|
214
|
-
"-q:v",
|
215
|
-
metavar="SCALE",
|
216
|
-
help="Set a value to the ffmpeg option -qscale:v",
|
217
|
-
)
|
218
209
|
parser.add_argument(
|
219
210
|
"--scale",
|
220
211
|
type=number,
|
@@ -238,7 +229,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
238
229
|
"--audio-bitrate",
|
239
230
|
"-b:a",
|
240
231
|
metavar="BITRATE",
|
241
|
-
type=bitrate,
|
242
232
|
help="Set the number of bits per second for audio",
|
243
233
|
)
|
244
234
|
parser.add_argument(
|
@@ -281,7 +271,7 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
|
281
271
|
log.conwrite("Downloading video...")
|
282
272
|
|
283
273
|
def get_domain(url: str) -> str:
|
284
|
-
t = __import__("urllib").
|
274
|
+
t = __import__("urllib.parse", fromlist=["parse"]).urlparse(url).netloc
|
285
275
|
return ".".join(t.split(".")[-2:])
|
286
276
|
|
287
277
|
download_format = args.download_format
|
auto_editor/edit.py
CHANGED
@@ -2,13 +2,17 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import os
|
4
4
|
import sys
|
5
|
+
from fractions import Fraction
|
5
6
|
from subprocess import run
|
6
7
|
from typing import Any
|
7
8
|
|
9
|
+
import av
|
10
|
+
from av import AudioResampler
|
11
|
+
|
8
12
|
from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
|
9
13
|
from auto_editor.lib.contracts import is_int, is_str
|
10
14
|
from auto_editor.make_layers import make_timeline
|
11
|
-
from auto_editor.output import Ensure,
|
15
|
+
from auto_editor.output import Ensure, parse_bitrate
|
12
16
|
from auto_editor.render.audio import make_new_audio
|
13
17
|
from auto_editor.render.subtitle import make_new_subtitles
|
14
18
|
from auto_editor.render.video import render_av
|
@@ -92,11 +96,19 @@ def set_audio_codec(
|
|
92
96
|
codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
|
93
97
|
) -> str:
|
94
98
|
if codec == "auto":
|
95
|
-
|
99
|
+
if src is None or not src.audios:
|
100
|
+
codec = "aac"
|
101
|
+
else:
|
102
|
+
codec = src.audios[0].codec
|
103
|
+
ctx = av.Codec(codec)
|
104
|
+
if ctx.audio_formats is None:
|
105
|
+
codec = "aac"
|
96
106
|
if codec not in ctr.acodecs and ctr.default_aud != "none":
|
97
|
-
|
107
|
+
codec = ctr.default_aud
|
98
108
|
if codec == "mp3float":
|
99
|
-
|
109
|
+
codec = "mp3"
|
110
|
+
if codec is None:
|
111
|
+
codec = "aac"
|
100
112
|
return codec
|
101
113
|
|
102
114
|
if codec == "copy":
|
@@ -106,9 +118,8 @@ def set_audio_codec(
|
|
106
118
|
log.error("Input file does not have an audio stream to copy codec from.")
|
107
119
|
codec = src.audios[0].codec
|
108
120
|
|
109
|
-
if codec
|
110
|
-
|
111
|
-
log.error(codec_error.format(codec, out_ext))
|
121
|
+
if ctr.acodecs is None or codec not in ctr.acodecs:
|
122
|
+
log.error(codec_error.format(codec, out_ext))
|
112
123
|
|
113
124
|
return codec
|
114
125
|
|
@@ -270,49 +281,150 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
270
281
|
if args.keep_tracks_separate and ctr.max_audios == 1:
|
271
282
|
log.warning(f"'{out_ext}' container doesn't support multiple audio tracks.")
|
272
283
|
|
273
|
-
def make_media(tl: v3,
|
284
|
+
def make_media(tl: v3, output_path: str) -> None:
|
274
285
|
assert src is not None
|
275
286
|
|
276
|
-
|
277
|
-
audio_output = []
|
278
|
-
sub_output = []
|
279
|
-
apply_later = False
|
287
|
+
output = av.open(output_path, "w")
|
280
288
|
|
281
|
-
ensure = Ensure(ffmpeg, bar, samplerate, log)
|
282
289
|
if ctr.default_sub != "none" and not args.sn:
|
283
|
-
|
290
|
+
sub_paths = make_new_subtitles(tl, log)
|
291
|
+
else:
|
292
|
+
sub_paths = []
|
284
293
|
|
285
294
|
if ctr.default_aud != "none":
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
295
|
+
ensure = Ensure(bar, samplerate, log)
|
296
|
+
audio_paths = make_new_audio(tl, ensure, args, ffmpeg, bar, log)
|
297
|
+
if (
|
298
|
+
not (args.keep_tracks_separate and ctr.max_audios is None)
|
299
|
+
and len(audio_paths) > 1
|
300
|
+
):
|
301
|
+
# Merge all the audio a_tracks into one.
|
302
|
+
new_a_file = os.path.join(log.temp, "new_audio.wav")
|
303
|
+
new_cmd = []
|
304
|
+
for path in audio_paths:
|
305
|
+
new_cmd.extend(["-i", path])
|
306
|
+
new_cmd.extend(
|
307
|
+
[
|
308
|
+
"-filter_complex",
|
309
|
+
f"amix=inputs={len(audio_paths)}:duration=longest",
|
310
|
+
"-ac",
|
311
|
+
"2",
|
312
|
+
new_a_file,
|
313
|
+
]
|
314
|
+
)
|
315
|
+
ffmpeg.run(new_cmd)
|
316
|
+
audio_paths = [new_a_file]
|
317
|
+
else:
|
318
|
+
audio_paths = []
|
319
|
+
|
320
|
+
# Setup audio
|
321
|
+
if audio_paths:
|
322
|
+
try:
|
323
|
+
audio_encoder = av.Codec(args.audio_codec)
|
324
|
+
except av.FFmpegError as e:
|
325
|
+
log.error(e)
|
326
|
+
if audio_encoder.audio_formats is None:
|
327
|
+
log.error(f"{args.audio_codec}: No known audio formats avail.")
|
328
|
+
audio_format = audio_encoder.audio_formats[0]
|
329
|
+
resampler = AudioResampler(format=audio_format, layout="stereo", rate=tl.sr)
|
330
|
+
|
331
|
+
audio_streams: list[av.AudioStream] = []
|
332
|
+
audio_inputs = []
|
333
|
+
audio_gen_frames = []
|
334
|
+
for i, audio_path in enumerate(audio_paths):
|
335
|
+
audio_stream = output.add_stream(
|
336
|
+
args.audio_codec,
|
337
|
+
format=audio_format,
|
338
|
+
rate=tl.sr,
|
339
|
+
time_base=Fraction(1, tl.sr),
|
340
|
+
)
|
341
|
+
if not isinstance(audio_stream, av.AudioStream):
|
342
|
+
log.error(f"Not a known audio codec: {args.audio_codec}")
|
343
|
+
|
344
|
+
if args.audio_bitrate != "auto":
|
345
|
+
audio_stream.bit_rate = parse_bitrate(args.audio_bitrate, log)
|
346
|
+
log.debug(f"audio bitrate: {audio_stream.bit_rate}")
|
347
|
+
else:
|
348
|
+
log.debug(f"[auto] audio bitrate: {audio_stream.bit_rate}")
|
349
|
+
if i < len(src.audios) and src.audios[i].lang is not None:
|
350
|
+
audio_stream.metadata["language"] = src.audios[i].lang # type: ignore
|
351
|
+
|
352
|
+
audio_streams.append(audio_stream)
|
353
|
+
audio_input = av.open(audio_path)
|
354
|
+
audio_inputs.append(audio_input)
|
355
|
+
audio_gen_frames.append(audio_input.decode(audio=0))
|
356
|
+
|
357
|
+
# Setup subtitles
|
358
|
+
subtitle_streams = []
|
359
|
+
subtitle_inputs = []
|
360
|
+
sub_gen_frames = []
|
361
|
+
|
362
|
+
for i, sub_path in enumerate(sub_paths):
|
363
|
+
subtitle_input = av.open(sub_path)
|
364
|
+
subtitle_inputs.append(subtitle_input)
|
365
|
+
subtitle_stream = output.add_stream(
|
366
|
+
template=subtitle_input.streams.subtitles[0]
|
367
|
+
)
|
368
|
+
if i < len(src.subtitles) and src.subtitles[i].lang is not None:
|
369
|
+
subtitle_stream.metadata["language"] = src.subtitles[i].lang # type: ignore
|
370
|
+
|
371
|
+
subtitle_streams.append(subtitle_stream)
|
372
|
+
sub_gen_frames.append(subtitle_input.demux(subtitles=0))
|
373
|
+
|
374
|
+
# Setup video
|
375
|
+
if ctr.default_vid != "none" and tl.v:
|
376
|
+
vframes = render_av(output, tl, args, bar, log)
|
377
|
+
output_stream = next(vframes)
|
378
|
+
else:
|
379
|
+
output_stream, vframes = None, iter([])
|
380
|
+
|
381
|
+
# Process frames
|
382
|
+
while True:
|
383
|
+
audio_frames = [next(frames, None) for frames in audio_gen_frames]
|
384
|
+
video_frame = next(vframes, None)
|
385
|
+
subtitle_frames = [next(packet, None) for packet in sub_gen_frames]
|
386
|
+
|
387
|
+
if (
|
388
|
+
all(frame is None for frame in audio_frames)
|
389
|
+
and video_frame is None
|
390
|
+
and all(packet is None for packet in subtitle_frames)
|
391
|
+
):
|
392
|
+
break
|
393
|
+
|
394
|
+
for audio_stream, audio_frame in zip(audio_streams, audio_frames):
|
395
|
+
if audio_frame:
|
396
|
+
for reframe in resampler.resample(audio_frame):
|
397
|
+
output.mux(audio_stream.encode(reframe))
|
398
|
+
|
399
|
+
for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
|
400
|
+
if not packet or packet.dts is None:
|
401
|
+
continue
|
402
|
+
packet.stream = subtitle_stream
|
403
|
+
output.mux(packet)
|
404
|
+
|
405
|
+
if video_frame:
|
406
|
+
try:
|
407
|
+
output.mux(output_stream.encode(video_frame))
|
408
|
+
except av.error.ExternalError:
|
409
|
+
log.error(
|
410
|
+
f"Generic error for encoder: {output_stream.name}\n"
|
411
|
+
"Perhaps video quality settings are too low?"
|
412
|
+
)
|
413
|
+
except av.FFmpegError as e:
|
414
|
+
log.error(e)
|
415
|
+
|
416
|
+
# Flush streams
|
417
|
+
if output_stream is not None:
|
418
|
+
output.mux(output_stream.encode(None))
|
419
|
+
for audio_stream in audio_streams:
|
420
|
+
output.mux(audio_stream.encode(None))
|
421
|
+
|
422
|
+
# Close resources
|
423
|
+
for audio_input in audio_inputs:
|
424
|
+
audio_input.close()
|
425
|
+
for subtitle_input in subtitle_inputs:
|
426
|
+
subtitle_input.close()
|
427
|
+
output.close()
|
316
428
|
|
317
429
|
if export == "clip-sequence":
|
318
430
|
if tl.v1 is None:
|
auto_editor/ffwrapper.py
CHANGED
@@ -1,11 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import os.path
|
4
3
|
import sys
|
5
4
|
from dataclasses import dataclass
|
6
5
|
from fractions import Fraction
|
7
6
|
from pathlib import Path
|
8
|
-
from re import search
|
9
7
|
from shutil import which
|
10
8
|
from subprocess import PIPE, Popen, run
|
11
9
|
from typing import Any
|
@@ -52,41 +50,6 @@ class FFmpeg:
|
|
52
50
|
sys.stderr.write(f"{' '.join(cmd)}\n\n")
|
53
51
|
run(cmd)
|
54
52
|
|
55
|
-
def run_check_errors(
|
56
|
-
self, cmd: list[str], show_out: bool = False, path: str | None = None
|
57
|
-
) -> None:
|
58
|
-
process = self.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
59
|
-
_, stderr = process.communicate()
|
60
|
-
|
61
|
-
if process.stdin is not None:
|
62
|
-
process.stdin.close()
|
63
|
-
output = stderr.decode("utf-8", "replace")
|
64
|
-
|
65
|
-
error_list = (
|
66
|
-
r"Unknown encoder '.*'",
|
67
|
-
r"-q:v qscale not available for encoder\. Use -b:v bitrate instead\.",
|
68
|
-
r"Specified sample rate .* is not supported",
|
69
|
-
r'Unable to parse option value ".*"',
|
70
|
-
r"Error setting option .* to value .*\.",
|
71
|
-
r"Undefined constant or missing '.*' in '.*'",
|
72
|
-
r"DLL .* failed to open",
|
73
|
-
r"Incompatible pixel format '.*' for codec '[A-Za-z0-9_]*'",
|
74
|
-
r"Unrecognized option '.*'",
|
75
|
-
r"Permission denied",
|
76
|
-
)
|
77
|
-
|
78
|
-
if self.debug:
|
79
|
-
print(f"stderr: {output}")
|
80
|
-
|
81
|
-
for item in error_list:
|
82
|
-
if check := search(item, output):
|
83
|
-
self.log.error(check.group())
|
84
|
-
|
85
|
-
if path is not None and not os.path.isfile(path):
|
86
|
-
self.log.error(f"The file {path} was not created.")
|
87
|
-
if show_out and not self.debug:
|
88
|
-
print(f"stderr: {output}")
|
89
|
-
|
90
53
|
def Popen(
|
91
54
|
self, cmd: list[str], stdin: Any = None, stdout: Any = PIPE, stderr: Any = None
|
92
55
|
) -> Popen:
|
@@ -103,13 +66,9 @@ def mux(input: Path, output: Path, stream: int) -> None:
|
|
103
66
|
output_audio_stream = output_container.add_stream("pcm_s16le")
|
104
67
|
|
105
68
|
for frame in input_container.decode(input_audio_stream):
|
106
|
-
|
107
|
-
if packet:
|
108
|
-
output_container.mux(packet)
|
69
|
+
output_container.mux(output_audio_stream.encode(frame))
|
109
70
|
|
110
|
-
|
111
|
-
if packet:
|
112
|
-
output_container.mux(packet)
|
71
|
+
output_container.mux(output_audio_stream.encode(None))
|
113
72
|
|
114
73
|
output_container.close()
|
115
74
|
input_container.close()
|
auto_editor/help.py
CHANGED
@@ -148,11 +148,12 @@ Beware that the temp directory can get quite big.
|
|
148
148
|
"--my-ffmpeg": "This is equivalent to `--ffmpeg-location ffmpeg`.",
|
149
149
|
"--audio-bitrate": """
|
150
150
|
`--audio-bitrate` sets the target bitrate for the audio encoder.
|
151
|
-
|
152
|
-
|
151
|
+
By default, the value is `auto` (let the encoder decide).
|
152
|
+
It can be set to a natural number with units: ``, `k`, `K`, `M`, or `G`.
|
153
|
+
|
153
154
|
""".strip(),
|
154
155
|
"--video-bitrate": """
|
155
|
-
`--video-bitrate` sets the target bitrate for the video encoder. It accepts the same format as `--audio-bitrate`
|
156
|
+
`--video-bitrate` sets the target bitrate for the video encoder. `auto` is set as the default. It accepts the same format as `--audio-bitrate`
|
156
157
|
""".strip(),
|
157
158
|
"--margin": """
|
158
159
|
Default value: 0.2s,0.2s
|
auto_editor/output.py
CHANGED
@@ -2,26 +2,37 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import os.path
|
4
4
|
from dataclasses import dataclass, field
|
5
|
-
from fractions import Fraction
|
6
5
|
|
7
6
|
import av
|
8
7
|
from av.audio.resampler import AudioResampler
|
9
8
|
|
10
|
-
from auto_editor.ffwrapper import
|
9
|
+
from auto_editor.ffwrapper import FileInfo
|
11
10
|
from auto_editor.utils.bar import Bar
|
12
|
-
from auto_editor.utils.container import Container
|
13
11
|
from auto_editor.utils.log import Log
|
14
|
-
from auto_editor.utils.types import
|
12
|
+
from auto_editor.utils.types import _split_num_str
|
13
|
+
|
14
|
+
|
15
|
+
def parse_bitrate(input_: str, log: Log) -> int:
|
16
|
+
val, unit = _split_num_str(input_)
|
17
|
+
|
18
|
+
if unit.lower() == "k":
|
19
|
+
return int(val * 1000)
|
20
|
+
if unit == "M":
|
21
|
+
return int(val * 1_000_000)
|
22
|
+
if unit == "G":
|
23
|
+
return int(val * 1_000_000_000)
|
24
|
+
if unit == "":
|
25
|
+
return int(val)
|
26
|
+
|
27
|
+
log.error(f"Unknown bitrate: {input_}")
|
15
28
|
|
16
29
|
|
17
30
|
@dataclass(slots=True)
|
18
31
|
class Ensure:
|
19
|
-
_ffmpeg: FFmpeg
|
20
32
|
_bar: Bar
|
21
33
|
_sr: int
|
22
34
|
log: Log
|
23
35
|
_audios: list[tuple[FileInfo, int]] = field(default_factory=list)
|
24
|
-
_subtitles: list[tuple[FileInfo, int, str]] = field(default_factory=list)
|
25
36
|
|
26
37
|
def audio(self, src: FileInfo, stream: int) -> str:
|
27
38
|
try:
|
@@ -52,193 +63,21 @@ class Ensure:
|
|
52
63
|
|
53
64
|
bar.start(dur, "Extracting audio")
|
54
65
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
66
|
+
output_astream = out_container.add_stream(
|
67
|
+
"pcm_s16le", layout="stereo", rate=sample_rate
|
68
|
+
)
|
59
69
|
resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate)
|
60
70
|
for i, frame in enumerate(in_container.decode(astream)):
|
61
71
|
if i % 1500 == 0 and frame.time is not None:
|
62
72
|
bar.tick(frame.time)
|
63
73
|
|
64
74
|
for new_frame in resampler.resample(frame):
|
65
|
-
|
66
|
-
out_container.mux_one(packet)
|
75
|
+
out_container.mux(output_astream.encode(new_frame))
|
67
76
|
|
68
|
-
|
69
|
-
out_container.mux_one(packet)
|
77
|
+
out_container.mux(output_astream.encode(None))
|
70
78
|
|
71
79
|
out_container.close()
|
72
80
|
in_container.close()
|
73
81
|
bar.end()
|
74
82
|
|
75
83
|
return out_path
|
76
|
-
|
77
|
-
def subtitle(self, src: FileInfo, stream: int, ext: str) -> str:
|
78
|
-
try:
|
79
|
-
self._subtitles.index((src, stream, ext))
|
80
|
-
first_time = False
|
81
|
-
except ValueError:
|
82
|
-
self._subtitles.append((src, stream, ext))
|
83
|
-
first_time = True
|
84
|
-
|
85
|
-
out_path = os.path.join(self.log.temp, f"{stream}s.{ext}")
|
86
|
-
|
87
|
-
if first_time:
|
88
|
-
self.log.debug(f"Making external subtitle: {out_path}")
|
89
|
-
self.log.conwrite("Extracting subtitle")
|
90
|
-
self._ffmpeg.run(["-i", f"{src.path}", "-map", f"0:s:{stream}", out_path])
|
91
|
-
|
92
|
-
return out_path
|
93
|
-
|
94
|
-
|
95
|
-
def _ffset(option: str, value: str | None) -> list[str]:
|
96
|
-
if value is None or value == "unset" or value == "reserved":
|
97
|
-
return []
|
98
|
-
return [option] + [value]
|
99
|
-
|
100
|
-
|
101
|
-
def video_quality(args: Args) -> list[str]:
|
102
|
-
return (
|
103
|
-
_ffset("-b:v", args.video_bitrate)
|
104
|
-
+ ["-c:v", args.video_codec]
|
105
|
-
+ _ffset("-qscale:v", args.video_quality_scale)
|
106
|
-
+ ["-movflags", "faststart"]
|
107
|
-
)
|
108
|
-
|
109
|
-
|
110
|
-
def mux_quality_media(
|
111
|
-
ffmpeg: FFmpeg,
|
112
|
-
visual_output: list[tuple[bool, str]],
|
113
|
-
audio_output: list[str],
|
114
|
-
sub_output: list[str],
|
115
|
-
apply_v: bool,
|
116
|
-
ctr: Container,
|
117
|
-
output_path: str,
|
118
|
-
tb: Fraction,
|
119
|
-
args: Args,
|
120
|
-
src: FileInfo,
|
121
|
-
log: Log,
|
122
|
-
) -> None:
|
123
|
-
v_tracks = len(visual_output)
|
124
|
-
a_tracks = len(audio_output)
|
125
|
-
s_tracks = 0 if args.sn else len(sub_output)
|
126
|
-
|
127
|
-
cmd = ["-hide_banner", "-y", "-i", f"{src.path}"]
|
128
|
-
|
129
|
-
same_container = src.path.suffix == os.path.splitext(output_path)[1]
|
130
|
-
|
131
|
-
for is_video, path in visual_output:
|
132
|
-
if is_video or ctr.allow_image:
|
133
|
-
cmd.extend(["-i", path])
|
134
|
-
else:
|
135
|
-
v_tracks -= 1
|
136
|
-
|
137
|
-
if a_tracks > 0:
|
138
|
-
if args.keep_tracks_separate and ctr.max_audios is None:
|
139
|
-
for path in audio_output:
|
140
|
-
cmd.extend(["-i", path])
|
141
|
-
else:
|
142
|
-
# Merge all the audio a_tracks into one.
|
143
|
-
new_a_file = os.path.join(log.temp, "new_audio.wav")
|
144
|
-
if a_tracks > 1:
|
145
|
-
new_cmd = []
|
146
|
-
for path in audio_output:
|
147
|
-
new_cmd.extend(["-i", path])
|
148
|
-
new_cmd.extend(
|
149
|
-
[
|
150
|
-
"-filter_complex",
|
151
|
-
f"amix=inputs={a_tracks}:duration=longest",
|
152
|
-
"-ac",
|
153
|
-
"2",
|
154
|
-
new_a_file,
|
155
|
-
]
|
156
|
-
)
|
157
|
-
ffmpeg.run(new_cmd)
|
158
|
-
a_tracks = 1
|
159
|
-
else:
|
160
|
-
new_a_file = audio_output[0]
|
161
|
-
cmd.extend(["-i", new_a_file])
|
162
|
-
|
163
|
-
for subfile in sub_output:
|
164
|
-
cmd.extend(["-i", subfile])
|
165
|
-
|
166
|
-
for i in range(v_tracks + s_tracks + a_tracks):
|
167
|
-
cmd.extend(["-map", f"{i+1}:0"])
|
168
|
-
|
169
|
-
cmd.extend(["-map_metadata", "0"])
|
170
|
-
|
171
|
-
track = 0
|
172
|
-
for is_video, path in visual_output:
|
173
|
-
if is_video:
|
174
|
-
if apply_v:
|
175
|
-
cmd += video_quality(args)
|
176
|
-
else:
|
177
|
-
# Real video is only allowed on track 0
|
178
|
-
cmd += ["-c:v:0", "copy"]
|
179
|
-
|
180
|
-
if float(tb).is_integer():
|
181
|
-
cmd += ["-video_track_timescale", f"{tb}"]
|
182
|
-
|
183
|
-
elif ctr.allow_image:
|
184
|
-
ext = os.path.splitext(path)[1][1:]
|
185
|
-
cmd += [f"-c:v:{track}", ext, f"-disposition:v:{track}", "attached_pic"]
|
186
|
-
|
187
|
-
track += 1
|
188
|
-
del track
|
189
|
-
|
190
|
-
for i, vstream in enumerate(src.videos):
|
191
|
-
if i > v_tracks:
|
192
|
-
break
|
193
|
-
if vstream.lang is not None:
|
194
|
-
cmd.extend([f"-metadata:s:v:{i}", f"language={vstream.lang}"])
|
195
|
-
for i, astream in enumerate(src.audios):
|
196
|
-
if i > a_tracks:
|
197
|
-
break
|
198
|
-
if astream.lang is not None:
|
199
|
-
cmd.extend([f"-metadata:s:a:{i}", f"language={astream.lang}"])
|
200
|
-
for i, sstream in enumerate(src.subtitles):
|
201
|
-
if i > s_tracks:
|
202
|
-
break
|
203
|
-
if sstream.lang is not None:
|
204
|
-
cmd.extend([f"-metadata:s:s:{i}", f"language={sstream.lang}"])
|
205
|
-
|
206
|
-
if s_tracks > 0:
|
207
|
-
scodec = src.subtitles[0].codec
|
208
|
-
if same_container:
|
209
|
-
cmd.extend(["-c:s", scodec])
|
210
|
-
elif ctr.scodecs is not None:
|
211
|
-
if scodec not in ctr.scodecs:
|
212
|
-
scodec = ctr.default_sub
|
213
|
-
cmd.extend(["-c:s", scodec])
|
214
|
-
|
215
|
-
if a_tracks > 0:
|
216
|
-
cmd += _ffset("-c:a", args.audio_codec) + _ffset("-b:a", args.audio_bitrate)
|
217
|
-
|
218
|
-
if same_container and v_tracks > 0:
|
219
|
-
color_range = src.videos[0].color_range
|
220
|
-
colorspace = src.videos[0].color_space
|
221
|
-
color_prim = src.videos[0].color_primaries
|
222
|
-
color_trc = src.videos[0].color_transfer
|
223
|
-
|
224
|
-
if color_range == 1 or color_range == 2:
|
225
|
-
cmd.extend(["-color_range", f"{color_range}"])
|
226
|
-
if colorspace in (0, 1) or (colorspace >= 3 and colorspace < 16):
|
227
|
-
cmd.extend(["-colorspace", f"{colorspace}"])
|
228
|
-
if color_prim == 1 or (color_prim >= 4 and color_prim < 17):
|
229
|
-
cmd.extend(["-color_primaries", f"{color_prim}"])
|
230
|
-
if color_trc == 1 or (color_trc >= 4 and color_trc < 22):
|
231
|
-
cmd.extend(["-color_trc", f"{color_trc}"])
|
232
|
-
|
233
|
-
if args.extras is not None:
|
234
|
-
cmd.extend(args.extras.split(" "))
|
235
|
-
cmd.extend(["-strict", "-2"]) # Allow experimental codecs.
|
236
|
-
|
237
|
-
if s_tracks > 0:
|
238
|
-
cmd.extend(["-map", "0:t?"]) # Add input attachments to output.
|
239
|
-
|
240
|
-
if not args.dn:
|
241
|
-
cmd.extend(["-map", "0:d?"])
|
242
|
-
|
243
|
-
cmd.append(output_path)
|
244
|
-
ffmpeg.run_check_errors(cmd, path=output_path)
|