auto-editor 28.0.2__py3-none-any.whl → 28.1.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 +4 -3
- auto_editor/analyze.py +13 -13
- auto_editor/cmds/desc.py +2 -2
- auto_editor/cmds/levels.py +3 -3
- auto_editor/cmds/subdump.py +4 -4
- auto_editor/cmds/test.py +31 -27
- auto_editor/edit.py +35 -23
- auto_editor/exports/kdenlive.py +322 -0
- auto_editor/ffwrapper.py +8 -8
- auto_editor/help.py +1 -0
- auto_editor/lang/stdenv.py +0 -5
- auto_editor/make_layers.py +3 -3
- auto_editor/render/audio.py +42 -42
- auto_editor/render/subtitle.py +5 -5
- auto_editor/render/video.py +28 -33
- auto_editor/utils/container.py +2 -3
- auto_editor/utils/log.py +3 -1
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/METADATA +2 -2
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/RECORD +24 -23
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/WHEEL +0 -0
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/licenses/LICENSE +0 -0
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "28.0
|
1
|
+
__version__ = "28.1.0"
|
auto_editor/__main__.py
CHANGED
@@ -441,6 +441,7 @@ def main() -> None:
|
|
441
441
|
({"--export-to-resolve", "-exr"}, ["--export", "resolve"]),
|
442
442
|
({"--export-to-final-cut-pro", "-exf"}, ["--export", "final-cut-pro"]),
|
443
443
|
({"--export-to-shotcut", "-exs"}, ["--export", "shotcut"]),
|
444
|
+
({"--export-to-kdenlive", "-exk"}, ["--export", "kdenlive"]),
|
444
445
|
({"--export-as-clip-sequence", "-excs"}, ["--export", "clip-sequence"]),
|
445
446
|
({"--edit-based-on"}, ["--edit"]),
|
446
447
|
],
|
@@ -454,13 +455,13 @@ def main() -> None:
|
|
454
455
|
buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
|
455
456
|
buf.write(f"Python: {plat.python_version()}\nAV: ")
|
456
457
|
try:
|
457
|
-
import
|
458
|
+
import av
|
458
459
|
except (ModuleNotFoundError, ImportError):
|
459
460
|
buf.write("not found")
|
460
461
|
else:
|
461
462
|
try:
|
462
|
-
buf.write(f"{
|
463
|
-
license =
|
463
|
+
buf.write(f"{av.__version__} ")
|
464
|
+
license = av._core.library_meta["libavcodec"]["license"]
|
464
465
|
buf.write(f"({license})")
|
465
466
|
except AttributeError:
|
466
467
|
buf.write("error")
|
auto_editor/analyze.py
CHANGED
@@ -9,10 +9,10 @@ from math import ceil
|
|
9
9
|
from tempfile import gettempdir
|
10
10
|
from typing import TYPE_CHECKING
|
11
11
|
|
12
|
-
import
|
12
|
+
import av
|
13
13
|
import numpy as np
|
14
|
-
from
|
15
|
-
from
|
14
|
+
from av.audio.fifo import AudioFifo
|
15
|
+
from av.subtitles.subtitle import AssSubtitle
|
16
16
|
|
17
17
|
from auto_editor import __version__
|
18
18
|
|
@@ -72,7 +72,7 @@ def mut_remove_large(
|
|
72
72
|
active = False
|
73
73
|
|
74
74
|
|
75
|
-
def iter_audio(audio_stream:
|
75
|
+
def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float32]:
|
76
76
|
fifo = AudioFifo()
|
77
77
|
sr = audio_stream.rate
|
78
78
|
|
@@ -80,10 +80,10 @@ def iter_audio(audio_stream: bv.AudioStream, tb: Fraction) -> Iterator[np.float3
|
|
80
80
|
accumulated_error = Fraction(0)
|
81
81
|
|
82
82
|
# Resample so that audio data is between [-1, 1]
|
83
|
-
resampler =
|
83
|
+
resampler = av.AudioResampler(av.AudioFormat("flt"), audio_stream.layout, sr)
|
84
84
|
|
85
85
|
container = audio_stream.container
|
86
|
-
assert isinstance(container,
|
86
|
+
assert isinstance(container, av.container.InputContainer)
|
87
87
|
|
88
88
|
for frame in container.decode(audio_stream):
|
89
89
|
frame.pts = None # Skip time checks
|
@@ -103,7 +103,7 @@ def iter_audio(audio_stream: bv.AudioStream, tb: Fraction) -> Iterator[np.float3
|
|
103
103
|
|
104
104
|
|
105
105
|
def iter_motion(
|
106
|
-
video:
|
106
|
+
video: av.VideoStream, tb: Fraction, blur: int, width: int
|
107
107
|
) -> Iterator[np.float32]:
|
108
108
|
video.thread_type = "AUTO"
|
109
109
|
|
@@ -113,7 +113,7 @@ def iter_motion(
|
|
113
113
|
index = 0
|
114
114
|
prev_index = -1
|
115
115
|
|
116
|
-
graph =
|
116
|
+
graph = av.filter.Graph()
|
117
117
|
graph.link_nodes(
|
118
118
|
graph.add_buffer(template=video),
|
119
119
|
graph.add("scale", f"{width}:-1"),
|
@@ -123,7 +123,7 @@ def iter_motion(
|
|
123
123
|
).configure()
|
124
124
|
|
125
125
|
container = video.container
|
126
|
-
assert isinstance(container,
|
126
|
+
assert isinstance(container, av.container.InputContainer)
|
127
127
|
|
128
128
|
for unframe in container.decode(video):
|
129
129
|
if unframe.pts is None:
|
@@ -154,7 +154,7 @@ def iter_motion(
|
|
154
154
|
|
155
155
|
@dataclass(slots=True)
|
156
156
|
class Levels:
|
157
|
-
container:
|
157
|
+
container: av.container.InputContainer
|
158
158
|
name: str
|
159
159
|
mod_time: int
|
160
160
|
tb: Fraction
|
@@ -258,7 +258,7 @@ class Levels:
|
|
258
258
|
if audio.duration is not None and audio.time_base is not None:
|
259
259
|
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
|
260
260
|
elif container.duration is not None:
|
261
|
-
inaccurate_dur = int(container.duration /
|
261
|
+
inaccurate_dur = int(container.duration / av.time_base * self.tb)
|
262
262
|
else:
|
263
263
|
inaccurate_dur = 1024
|
264
264
|
|
@@ -385,8 +385,8 @@ def initLevels(
|
|
385
385
|
src: FileInfo, tb: Fraction, bar: Bar, no_cache: bool, log: Log
|
386
386
|
) -> Levels:
|
387
387
|
try:
|
388
|
-
container =
|
389
|
-
except
|
388
|
+
container = av.open(src.path)
|
389
|
+
except av.FFmpegError as e:
|
390
390
|
log.error(e)
|
391
391
|
|
392
392
|
mod_time = int(src.path.stat().st_mtime)
|
auto_editor/cmds/desc.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import sys
|
2
2
|
from dataclasses import dataclass, field
|
3
3
|
|
4
|
-
import
|
4
|
+
import av
|
5
5
|
|
6
6
|
from auto_editor.vanparse import ArgumentParser
|
7
7
|
|
@@ -21,7 +21,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
21
21
|
args = desc_options(ArgumentParser("desc")).parse_args(DescArgs, sys_args)
|
22
22
|
for path in args.input:
|
23
23
|
try:
|
24
|
-
container =
|
24
|
+
container = av.open(path)
|
25
25
|
desc = container.metadata.get("description", None)
|
26
26
|
except Exception:
|
27
27
|
desc = None
|
auto_editor/cmds/levels.py
CHANGED
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|
5
5
|
from fractions import Fraction
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
-
import
|
8
|
+
import av
|
9
9
|
import numpy as np
|
10
10
|
|
11
11
|
from auto_editor.analyze import *
|
@@ -137,7 +137,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
137
137
|
):
|
138
138
|
print_arr(arr)
|
139
139
|
else:
|
140
|
-
container =
|
140
|
+
container = av.open(src.path, "r")
|
141
141
|
audio_stream = container.streams.audio[obj["stream"]]
|
142
142
|
|
143
143
|
values = []
|
@@ -162,7 +162,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
162
162
|
):
|
163
163
|
print_arr(arr)
|
164
164
|
else:
|
165
|
-
container =
|
165
|
+
container = av.open(src.path, "r")
|
166
166
|
video_stream = container.streams.video[obj["stream"]]
|
167
167
|
|
168
168
|
values = []
|
auto_editor/cmds/subdump.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
import sys
|
2
2
|
from dataclasses import dataclass, field
|
3
3
|
|
4
|
-
import
|
5
|
-
from
|
4
|
+
import av
|
5
|
+
from av.subtitles.subtitle import AssSubtitle
|
6
6
|
|
7
7
|
from auto_editor.json import dump
|
8
8
|
from auto_editor.vanparse import ArgumentParser
|
@@ -24,7 +24,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
24
24
|
if args.json:
|
25
25
|
data = {}
|
26
26
|
for input_file in args.input:
|
27
|
-
container =
|
27
|
+
container = av.open(input_file)
|
28
28
|
for s in range(len(container.streams.subtitles)):
|
29
29
|
entry_data = []
|
30
30
|
|
@@ -59,7 +59,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
59
59
|
return
|
60
60
|
|
61
61
|
for input_file in args.input:
|
62
|
-
with
|
62
|
+
with av.open(input_file) as container:
|
63
63
|
for s in range(len(container.streams.subtitles)):
|
64
64
|
print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
|
65
65
|
for sub2 in container.decode(subtitles=s):
|
auto_editor/cmds/test.py
CHANGED
@@ -11,8 +11,9 @@ from hashlib import sha256
|
|
11
11
|
from tempfile import mkdtemp
|
12
12
|
from time import perf_counter
|
13
13
|
|
14
|
-
import
|
14
|
+
import av
|
15
15
|
import numpy as np
|
16
|
+
from av import AudioStream, VideoStream
|
16
17
|
|
17
18
|
from auto_editor.ffwrapper import FileInfo
|
18
19
|
from auto_editor.lang.palet import Lexer, Parser, env, interpret
|
@@ -58,7 +59,7 @@ all_files = (
|
|
58
59
|
"mov_text.mp4",
|
59
60
|
"testsrc.mkv",
|
60
61
|
)
|
61
|
-
log = Log()
|
62
|
+
log = Log(is_debug=True)
|
62
63
|
|
63
64
|
|
64
65
|
def fileinfo(path: str) -> FileInfo:
|
@@ -103,10 +104,11 @@ class Runner:
|
|
103
104
|
|
104
105
|
return output
|
105
106
|
|
106
|
-
def raw(self, cmd: list[str]) ->
|
107
|
+
def raw(self, cmd: list[str]) -> str:
|
107
108
|
returncode, stdout, stderr = pipe_to_console(self.program + cmd)
|
108
109
|
if returncode > 0:
|
109
110
|
raise Exception(f"{stdout}\n{stderr}\n")
|
111
|
+
return stdout
|
110
112
|
|
111
113
|
def check(self, cmd: list[str], match=None) -> None:
|
112
114
|
returncode, stdout, stderr = pipe_to_console(self.program + cmd)
|
@@ -133,11 +135,13 @@ class Runner:
|
|
133
135
|
|
134
136
|
def test_version(self):
|
135
137
|
"""Test version flags and debug by itself."""
|
136
|
-
self.raw(["--version"])
|
137
|
-
self.raw(["-V"])
|
138
|
+
v1 = self.raw(["--version"])
|
139
|
+
v2 = self.raw(["-V"])
|
140
|
+
assert "." in v1 and len(v1) > 4
|
141
|
+
assert v1 == v2
|
138
142
|
|
139
143
|
def test_parser(self):
|
140
|
-
self.check(["example.mp4", "--
|
144
|
+
self.check(["example.mp4", "--margin"], "needs argument")
|
141
145
|
|
142
146
|
def info(self):
|
143
147
|
self.raw(["info", "example.mp4"])
|
@@ -161,36 +165,36 @@ class Runner:
|
|
161
165
|
file = "resources/testsrc.mp4"
|
162
166
|
out = self.main([file], ["--faststart"]) + ".mp4"
|
163
167
|
fast = calculate_sha256(out)
|
164
|
-
with
|
165
|
-
assert isinstance(container.streams[0],
|
166
|
-
assert isinstance(container.streams[1],
|
168
|
+
with av.open(out) as container:
|
169
|
+
assert isinstance(container.streams[0], VideoStream)
|
170
|
+
assert isinstance(container.streams[1], AudioStream)
|
167
171
|
|
168
172
|
out = self.main([file], ["--no-faststart"]) + ".mp4"
|
169
173
|
nofast = calculate_sha256(out)
|
170
|
-
with
|
171
|
-
assert isinstance(container.streams[0],
|
172
|
-
assert isinstance(container.streams[1],
|
174
|
+
with av.open(out) as container:
|
175
|
+
assert isinstance(container.streams[0], VideoStream)
|
176
|
+
assert isinstance(container.streams[1], AudioStream)
|
173
177
|
|
174
178
|
out = self.main([file], ["--fragmented"]) + ".mp4"
|
175
179
|
frag = calculate_sha256(out)
|
176
|
-
with
|
177
|
-
assert isinstance(container.streams[0],
|
178
|
-
assert isinstance(container.streams[1],
|
180
|
+
with av.open(out) as container:
|
181
|
+
assert isinstance(container.streams[0], VideoStream)
|
182
|
+
assert isinstance(container.streams[1], AudioStream)
|
179
183
|
|
180
184
|
assert fast != nofast, "+faststart is not being applied"
|
181
185
|
assert frag not in (fast, nofast), "fragmented output should diff."
|
182
186
|
|
183
187
|
def test_example(self) -> None:
|
184
188
|
out = self.main(["example.mp4"], [], output="example_ALTERED.mp4")
|
185
|
-
with
|
189
|
+
with av.open(out) as container:
|
186
190
|
assert container.duration is not None
|
187
191
|
assert container.duration > 17300000 and container.duration < 2 << 24
|
188
192
|
|
189
193
|
assert len(container.streams) == 2
|
190
194
|
video = container.streams[0]
|
191
195
|
audio = container.streams[1]
|
192
|
-
assert isinstance(video,
|
193
|
-
assert isinstance(audio,
|
196
|
+
assert isinstance(video, VideoStream)
|
197
|
+
assert isinstance(audio, AudioStream)
|
194
198
|
assert video.base_rate == 30
|
195
199
|
assert video.average_rate is not None
|
196
200
|
assert video.average_rate == 30, video.average_rate
|
@@ -204,28 +208,28 @@ class Runner:
|
|
204
208
|
|
205
209
|
def test_video_to_mp3(self) -> None:
|
206
210
|
out = self.main(["example.mp4"], [], output="example_ALTERED.mp3")
|
207
|
-
with
|
211
|
+
with av.open(out) as container:
|
208
212
|
assert container.duration is not None
|
209
213
|
assert container.duration > 17300000 and container.duration < 2 << 24
|
210
214
|
|
211
215
|
assert len(container.streams) == 1
|
212
216
|
audio = container.streams[0]
|
213
|
-
assert isinstance(audio,
|
217
|
+
assert isinstance(audio, AudioStream)
|
214
218
|
assert audio.codec.name in ("mp3", "mp3float")
|
215
219
|
assert audio.sample_rate == 48000
|
216
220
|
assert audio.layout.name == "stereo"
|
217
221
|
|
218
222
|
def test_to_mono(self) -> None:
|
219
223
|
out = self.main(["example.mp4"], ["-layout", "mono"], output="example_mono.mp4")
|
220
|
-
with
|
224
|
+
with av.open(out) as container:
|
221
225
|
assert container.duration is not None
|
222
226
|
assert container.duration > 17300000 and container.duration < 2 << 24
|
223
227
|
|
224
228
|
assert len(container.streams) == 2
|
225
229
|
video = container.streams[0]
|
226
230
|
audio = container.streams[1]
|
227
|
-
assert isinstance(video,
|
228
|
-
assert isinstance(audio,
|
231
|
+
assert isinstance(video, VideoStream)
|
232
|
+
assert isinstance(audio, AudioStream)
|
229
233
|
assert video.base_rate == 30
|
230
234
|
assert video.average_rate is not None
|
231
235
|
assert video.average_rate == 30, video.average_rate
|
@@ -302,18 +306,18 @@ class Runner:
|
|
302
306
|
self.check([path, "--no-open"], "must have an extension")
|
303
307
|
|
304
308
|
def test_silent_threshold(self):
|
305
|
-
with
|
309
|
+
with av.open("resources/new-commentary.mp3") as container:
|
306
310
|
assert container.duration is not None
|
307
|
-
assert container.duration /
|
311
|
+
assert container.duration / av.time_base == 6.732
|
308
312
|
|
309
313
|
out = self.main(
|
310
314
|
["resources/new-commentary.mp3"], ["--edit", "audio:threshold=0.1"]
|
311
315
|
)
|
312
316
|
out += ".mp3"
|
313
317
|
|
314
|
-
with
|
318
|
+
with av.open(out) as container:
|
315
319
|
assert container.duration is not None
|
316
|
-
assert container.duration /
|
320
|
+
assert container.duration / av.time_base == 6.552
|
317
321
|
|
318
322
|
def test_track(self):
|
319
323
|
out = self.main(["resources/multi-track.mov"], []) + ".mov"
|
auto_editor/edit.py
CHANGED
@@ -9,7 +9,8 @@ from pathlib import Path
|
|
9
9
|
from subprocess import run
|
10
10
|
from typing import TYPE_CHECKING, Any
|
11
11
|
|
12
|
-
import
|
12
|
+
import av
|
13
|
+
from av import Codec
|
13
14
|
|
14
15
|
from auto_editor.ffwrapper import FileInfo
|
15
16
|
from auto_editor.lib.contracts import is_int, is_str
|
@@ -50,6 +51,8 @@ def set_output(
|
|
50
51
|
export = "final-cut-pro"
|
51
52
|
case ".mlt":
|
52
53
|
export = "shotcut"
|
54
|
+
case ".kdenlive":
|
55
|
+
export = "kdenlive"
|
53
56
|
case ".json" | ".v1":
|
54
57
|
export = "v1"
|
55
58
|
case ".v3":
|
@@ -64,6 +67,8 @@ def set_output(
|
|
64
67
|
ext = ".fcpxml"
|
65
68
|
case "shotcut":
|
66
69
|
ext = ".mlt"
|
70
|
+
case "kdenlive":
|
71
|
+
ext = ".kdenlive"
|
67
72
|
case "v1":
|
68
73
|
if ext != ".json":
|
69
74
|
ext = ".v1"
|
@@ -79,9 +84,6 @@ def set_output(
|
|
79
84
|
return f"{root}{ext}", export
|
80
85
|
|
81
86
|
|
82
|
-
codec_error = "'{}' codec is not supported in '{}' container."
|
83
|
-
|
84
|
-
|
85
87
|
def set_video_codec(
|
86
88
|
codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
|
87
89
|
) -> str:
|
@@ -93,12 +95,14 @@ def set_video_codec(
|
|
93
95
|
|
94
96
|
if ctr.vcodecs is not None and codec not in ctr.vcodecs:
|
95
97
|
try:
|
96
|
-
cobj =
|
97
|
-
except
|
98
|
+
cobj = Codec(codec, "w")
|
99
|
+
except av.codec.codec.UnknownCodecError:
|
98
100
|
log.error(f"Unknown encoder: {codec}")
|
99
101
|
# Normalize encoder names
|
100
|
-
if cobj.id not in (
|
101
|
-
log.error(
|
102
|
+
if cobj.id not in (Codec(x, "w").id for x in ctr.vcodecs):
|
103
|
+
log.error(
|
104
|
+
f"'{codec}' video encoder is not supported in the '{out_ext}' container"
|
105
|
+
)
|
102
106
|
|
103
107
|
return codec
|
104
108
|
|
@@ -111,7 +115,7 @@ def set_audio_codec(
|
|
111
115
|
codec = "aac"
|
112
116
|
else:
|
113
117
|
codec = src.audios[0].codec
|
114
|
-
if
|
118
|
+
if Codec(codec, "w").audio_formats is None:
|
115
119
|
codec = "aac"
|
116
120
|
if codec not in ctr.acodecs and ctr.default_aud != "none":
|
117
121
|
codec = ctr.default_aud
|
@@ -121,13 +125,14 @@ def set_audio_codec(
|
|
121
125
|
|
122
126
|
if ctr.acodecs is None or codec not in ctr.acodecs:
|
123
127
|
try:
|
124
|
-
cobj =
|
125
|
-
except
|
128
|
+
cobj = Codec(codec, "w")
|
129
|
+
except av.codec.codec.UnknownCodecError:
|
126
130
|
log.error(f"Unknown encoder: {codec}")
|
127
131
|
# Normalize encoder names
|
128
|
-
if cobj.id not in (
|
129
|
-
log.error(
|
130
|
-
|
132
|
+
if cobj.id not in (Codec(x, "w").id for x in ctr.acodecs):
|
133
|
+
log.error(
|
134
|
+
f"'{codec}' audio encoder is not supported in the '{out_ext}' container"
|
135
|
+
)
|
131
136
|
return codec
|
132
137
|
|
133
138
|
|
@@ -148,6 +153,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
148
153
|
"resolve": pAttrs("resolve", name_attr),
|
149
154
|
"resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
|
150
155
|
"shotcut": pAttrs("shotcut"),
|
156
|
+
"kdenlive": pAttrs("kdenlive"),
|
151
157
|
"v1": pAttrs("v1"),
|
152
158
|
"v3": pAttrs("v3"),
|
153
159
|
"clip-sequence": pAttrs("clip-sequence"),
|
@@ -261,6 +267,12 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
261
267
|
shotcut_write_mlt(output, tl)
|
262
268
|
return
|
263
269
|
|
270
|
+
if export == "kdenlive":
|
271
|
+
from auto_editor.exports.kdenlive import kdenlive_write
|
272
|
+
|
273
|
+
kdenlive_write(output, tl)
|
274
|
+
return
|
275
|
+
|
264
276
|
if output == "-":
|
265
277
|
log.error("Exporting media files to stdout is not supported.")
|
266
278
|
out_ext = splitext(output)[1].replace(".", "")
|
@@ -287,26 +299,26 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
287
299
|
if mov_flags:
|
288
300
|
options["movflags"] = "+".join(mov_flags)
|
289
301
|
|
290
|
-
output =
|
302
|
+
output = av.open(output_path, "w", container_options=options)
|
291
303
|
|
292
304
|
# Setup video
|
293
305
|
if ctr.default_vid not in ("none", "png") and tl.v:
|
294
306
|
vframes = render_av(output, tl, args, log)
|
295
|
-
output_stream:
|
307
|
+
output_stream: av.VideoStream | None
|
296
308
|
output_stream = next(vframes) # type: ignore
|
297
309
|
else:
|
298
310
|
output_stream, vframes = None, iter([])
|
299
311
|
|
300
312
|
# Setup audio
|
301
313
|
try:
|
302
|
-
audio_encoder =
|
303
|
-
except
|
314
|
+
audio_encoder = Codec(args.audio_codec, "w")
|
315
|
+
except av.FFmpegError as e:
|
304
316
|
log.error(e)
|
305
317
|
if audio_encoder.audio_formats is None:
|
306
318
|
log.error(f"{args.audio_codec}: No known audio formats avail.")
|
307
319
|
fmt = audio_encoder.audio_formats[0]
|
308
320
|
|
309
|
-
audio_streams: list[
|
321
|
+
audio_streams: list[av.AudioStream] = []
|
310
322
|
|
311
323
|
if ctr.default_aud == "none":
|
312
324
|
while len(tl.a) > 0:
|
@@ -333,7 +345,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
333
345
|
sub_gen_frames = []
|
334
346
|
|
335
347
|
for i, sub_path in enumerate(sub_paths):
|
336
|
-
subtitle_input =
|
348
|
+
subtitle_input = av.open(sub_path)
|
337
349
|
subtitle_inputs.append(subtitle_input)
|
338
350
|
subtitle_stream = output.add_stream_from_template(
|
339
351
|
subtitle_input.streams.subtitles[0]
|
@@ -460,14 +472,14 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
460
472
|
output.mux(item.stream.encode(item.frame))
|
461
473
|
elif frame_type == "subtitle":
|
462
474
|
output.mux(item.frame)
|
463
|
-
except
|
475
|
+
except av.error.ExternalError:
|
464
476
|
log.error(
|
465
477
|
f"Generic error for encoder: {item.stream.name}\n"
|
466
478
|
f"at {item.index} time_base\nPerhaps video quality settings are too low?"
|
467
479
|
)
|
468
|
-
except
|
480
|
+
except av.FileNotFoundError:
|
469
481
|
log.error(f"File not found: {output_path}")
|
470
|
-
except
|
482
|
+
except av.FFmpegError as e:
|
471
483
|
log.error(e)
|
472
484
|
|
473
485
|
if bar_index:
|