auto-editor 25.3.0__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 +67 -16
- auto_editor/analyze.py +11 -14
- auto_editor/edit.py +177 -52
- auto_editor/ffwrapper.py +36 -114
- auto_editor/help.py +4 -3
- auto_editor/output.py +22 -183
- auto_editor/render/audio.py +66 -57
- auto_editor/render/subtitle.py +74 -13
- auto_editor/render/video.py +166 -180
- auto_editor/subcommands/repl.py +12 -3
- auto_editor/subcommands/test.py +47 -36
- auto_editor/utils/container.py +2 -0
- auto_editor/utils/func.py +1 -27
- auto_editor/utils/types.py +2 -15
- {auto_editor-25.3.0.dist-info → auto_editor-26.0.0.dist-info}/METADATA +2 -2
- {auto_editor-25.3.0.dist-info → auto_editor-26.0.0.dist-info}/RECORD +22 -24
- {auto_editor-25.3.0.dist-info → auto_editor-26.0.0.dist-info}/WHEEL +1 -1
- docs/build.py +1 -0
- auto_editor/utils/subtitle_tools.py +0 -29
- auto_editor/validate_input.py +0 -88
- {auto_editor-25.3.0.dist-info → auto_editor-26.0.0.dist-info}/LICENSE +0 -0
- {auto_editor-25.3.0.dist-info → auto_editor-26.0.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-25.3.0.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
@@ -1,15 +1,18 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
|
3
|
+
import re
|
3
4
|
import sys
|
4
5
|
from os import environ
|
6
|
+
from os.path import exists, isdir, isfile, lexists, splitext
|
7
|
+
from subprocess import run
|
5
8
|
|
6
9
|
import auto_editor
|
7
10
|
from auto_editor.edit import edit_media
|
8
|
-
from auto_editor.ffwrapper import FFmpeg
|
11
|
+
from auto_editor.ffwrapper import FFmpeg, initFFmpeg
|
12
|
+
from auto_editor.utils.func import get_stdout
|
9
13
|
from auto_editor.utils.log import Log
|
10
14
|
from auto_editor.utils.types import (
|
11
15
|
Args,
|
12
|
-
bitrate,
|
13
16
|
color,
|
14
17
|
frame_rate,
|
15
18
|
margin,
|
@@ -20,7 +23,6 @@ from auto_editor.utils.types import (
|
|
20
23
|
speed_range,
|
21
24
|
time_range,
|
22
25
|
)
|
23
|
-
from auto_editor.validate_input import valid_input
|
24
26
|
from auto_editor.vanparse import ArgumentParser
|
25
27
|
|
26
28
|
|
@@ -202,16 +204,8 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
202
204
|
"--video-bitrate",
|
203
205
|
"-b:v",
|
204
206
|
metavar="BITRATE",
|
205
|
-
type=bitrate,
|
206
207
|
help="Set the number of bits per second for video",
|
207
208
|
)
|
208
|
-
parser.add_argument(
|
209
|
-
"--video-quality-scale",
|
210
|
-
"-qscale:v",
|
211
|
-
"-q:v",
|
212
|
-
metavar="SCALE",
|
213
|
-
help="Set a value to the ffmpeg option -qscale:v",
|
214
|
-
)
|
215
209
|
parser.add_argument(
|
216
210
|
"--scale",
|
217
211
|
type=number,
|
@@ -235,7 +229,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
235
229
|
"--audio-bitrate",
|
236
230
|
"-b:a",
|
237
231
|
metavar="BITRATE",
|
238
|
-
type=bitrate,
|
239
232
|
help="Set the number of bits per second for audio",
|
240
233
|
)
|
241
234
|
parser.add_argument(
|
@@ -274,6 +267,50 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
274
267
|
return parser
|
275
268
|
|
276
269
|
|
270
|
+
def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
|
271
|
+
log.conwrite("Downloading video...")
|
272
|
+
|
273
|
+
def get_domain(url: str) -> str:
|
274
|
+
t = __import__("urllib.parse", fromlist=["parse"]).urlparse(url).netloc
|
275
|
+
return ".".join(t.split(".")[-2:])
|
276
|
+
|
277
|
+
download_format = args.download_format
|
278
|
+
if download_format is None and get_domain(my_input) == "youtube.com":
|
279
|
+
download_format = "bestvideo[ext=mp4]+bestaudio[ext=m4a]"
|
280
|
+
|
281
|
+
if args.output_format is None:
|
282
|
+
output_format = re.sub(r"\W+", "-", splitext(my_input)[0]) + ".%(ext)s"
|
283
|
+
else:
|
284
|
+
output_format = args.output_format
|
285
|
+
|
286
|
+
yt_dlp_path = args.yt_dlp_location
|
287
|
+
|
288
|
+
cmd = ["--ffmpeg-location", ffmpeg.path]
|
289
|
+
|
290
|
+
if download_format is not None:
|
291
|
+
cmd.extend(["-f", download_format])
|
292
|
+
|
293
|
+
cmd.extend(["-o", output_format, my_input])
|
294
|
+
|
295
|
+
if args.yt_dlp_extras is not None:
|
296
|
+
cmd.extend(args.yt_dlp_extras.split(" "))
|
297
|
+
|
298
|
+
try:
|
299
|
+
location = get_stdout(
|
300
|
+
[yt_dlp_path, "--get-filename", "--no-warnings"] + cmd
|
301
|
+
).strip()
|
302
|
+
except FileNotFoundError:
|
303
|
+
log.error("Program `yt-dlp` must be installed and on PATH.")
|
304
|
+
|
305
|
+
if not isfile(location):
|
306
|
+
run([yt_dlp_path] + cmd)
|
307
|
+
|
308
|
+
if not isfile(location):
|
309
|
+
log.error(f"Download file wasn't created: {location}")
|
310
|
+
|
311
|
+
return location
|
312
|
+
|
313
|
+
|
277
314
|
def main() -> None:
|
278
315
|
subcommands = ("test", "info", "levels", "subdump", "desc", "repl", "palet")
|
279
316
|
|
@@ -284,8 +321,7 @@ def main() -> None:
|
|
284
321
|
obj.main(sys.argv[2:])
|
285
322
|
return
|
286
323
|
|
287
|
-
|
288
|
-
no_color = bool(environ.get("NO_COLOR")) or bool(environ.get(ff_color))
|
324
|
+
no_color = bool(environ.get("NO_COLOR") or environ.get("AV_LOG_FORCE_NOCOLOR"))
|
289
325
|
log = Log(no_color=no_color)
|
290
326
|
|
291
327
|
args = main_options(ArgumentParser("Auto-Editor")).parse_args(
|
@@ -327,13 +363,28 @@ def main() -> None:
|
|
327
363
|
is_machine = args.progress == "machine"
|
328
364
|
log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
|
329
365
|
|
330
|
-
ffmpeg =
|
366
|
+
ffmpeg = initFFmpeg(
|
367
|
+
log,
|
331
368
|
args.ffmpeg_location,
|
332
369
|
args.my_ffmpeg,
|
333
370
|
args.show_ffmpeg_commands,
|
334
371
|
args.show_ffmpeg_output,
|
335
372
|
)
|
336
|
-
paths =
|
373
|
+
paths = []
|
374
|
+
for my_input in args.input:
|
375
|
+
if my_input.startswith("http://") or my_input.startswith("https://"):
|
376
|
+
paths.append(download_video(my_input, args, ffmpeg, log))
|
377
|
+
else:
|
378
|
+
if not splitext(my_input)[1]:
|
379
|
+
if isdir(my_input):
|
380
|
+
log.error("Input must be a file or a URL, not a directory.")
|
381
|
+
if exists(my_input):
|
382
|
+
log.error(f"Input file must have an extension: {my_input}")
|
383
|
+
if lexists(my_input):
|
384
|
+
log.error(f"Input file is a broken symbolic link: {my_input}")
|
385
|
+
if my_input.startswith("-"):
|
386
|
+
log.error(f"Option/Input file doesn't exist: {my_input}")
|
387
|
+
paths.append(my_input)
|
337
388
|
|
338
389
|
try:
|
339
390
|
edit_media(paths, ffmpeg, args, log)
|
auto_editor/analyze.py
CHANGED
@@ -14,13 +14,11 @@ from av.audio.fifo import AudioFifo
|
|
14
14
|
from av.subtitles.subtitle import AssSubtitle
|
15
15
|
|
16
16
|
from auto_editor import __version__
|
17
|
-
from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
18
17
|
|
19
18
|
if TYPE_CHECKING:
|
20
|
-
from collections.abc import Iterator
|
19
|
+
from collections.abc import Iterator, Sequence
|
21
20
|
from fractions import Fraction
|
22
21
|
from pathlib import Path
|
23
|
-
from typing import Any
|
24
22
|
|
25
23
|
from numpy.typing import NDArray
|
26
24
|
|
@@ -156,10 +154,10 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
156
154
|
container.close()
|
157
155
|
|
158
156
|
|
159
|
-
def obj_tag(path: Path, kind: str, tb: Fraction, obj:
|
157
|
+
def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
|
160
158
|
mod_time = int(path.stat().st_mtime)
|
161
159
|
key = f"{path.name}:{mod_time:x}:{kind}:{tb}:"
|
162
|
-
return key + ",".join(f"{v}" for v in obj
|
160
|
+
return key + ",".join(f"{v}" for v in obj)
|
163
161
|
|
164
162
|
|
165
163
|
@dataclass(slots=True)
|
@@ -174,7 +172,7 @@ class Levels:
|
|
174
172
|
@property
|
175
173
|
def media_length(self) -> int:
|
176
174
|
if self.src.audios:
|
177
|
-
if (arr := self.read_cache("audio",
|
175
|
+
if (arr := self.read_cache("audio", (0,))) is not None:
|
178
176
|
return len(arr)
|
179
177
|
|
180
178
|
result = sum(1 for _ in iter_audio(self.src, self.tb, 0))
|
@@ -202,7 +200,7 @@ class Levels:
|
|
202
200
|
def all(self) -> NDArray[np.bool_]:
|
203
201
|
return np.zeros(self.media_length, dtype=np.bool_)
|
204
202
|
|
205
|
-
def read_cache(self, kind: str, obj:
|
203
|
+
def read_cache(self, kind: str, obj: Sequence[object]) -> None | np.ndarray:
|
206
204
|
if self.no_cache:
|
207
205
|
return None
|
208
206
|
|
@@ -221,7 +219,7 @@ class Levels:
|
|
221
219
|
self.log.debug("Using cache")
|
222
220
|
return npzfile[key]
|
223
221
|
|
224
|
-
def cache(self, arr: np.ndarray, kind: str, obj:
|
222
|
+
def cache(self, arr: np.ndarray, kind: str, obj: Sequence[object]) -> np.ndarray:
|
225
223
|
if self.no_cache:
|
226
224
|
return arr
|
227
225
|
|
@@ -238,7 +236,7 @@ class Levels:
|
|
238
236
|
if stream >= len(self.src.audios):
|
239
237
|
raise LevelError(f"audio: audio stream '{stream}' does not exist.")
|
240
238
|
|
241
|
-
if (arr := self.read_cache("audio",
|
239
|
+
if (arr := self.read_cache("audio", (stream,))) is not None:
|
242
240
|
return arr
|
243
241
|
|
244
242
|
with av.open(self.src.path, "r") as container:
|
@@ -265,13 +263,13 @@ class Levels:
|
|
265
263
|
index += 1
|
266
264
|
|
267
265
|
bar.end()
|
268
|
-
return self.cache(result[:index], "audio",
|
266
|
+
return self.cache(result[:index], "audio", (stream,))
|
269
267
|
|
270
268
|
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
|
271
269
|
if stream >= len(self.src.videos):
|
272
270
|
raise LevelError(f"motion: video stream '{stream}' does not exist.")
|
273
271
|
|
274
|
-
mobj =
|
272
|
+
mobj = (stream, width, blur)
|
275
273
|
if (arr := self.read_cache("motion", mobj)) is not None:
|
276
274
|
return arr
|
277
275
|
|
@@ -360,11 +358,10 @@ class Levels:
|
|
360
358
|
san_end = round((start + dur) * self.tb)
|
361
359
|
|
362
360
|
for sub in subset:
|
363
|
-
if isinstance(sub, AssSubtitle):
|
364
|
-
line = convert_ass_to_text(sub.ass.decode(errors="ignore"))
|
365
|
-
else:
|
361
|
+
if not isinstance(sub, AssSubtitle):
|
366
362
|
continue
|
367
363
|
|
364
|
+
line = sub.dialogue.decode(errors="ignore")
|
368
365
|
if line and re.search(re_pattern, line):
|
369
366
|
result[san_start:san_end] = 1
|
370
367
|
count += 1
|
auto_editor/edit.py
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import os
|
4
|
+
import sys
|
5
|
+
from fractions import Fraction
|
6
|
+
from subprocess import run
|
4
7
|
from typing import Any
|
5
8
|
|
9
|
+
import av
|
10
|
+
from av import AudioResampler
|
11
|
+
|
6
12
|
from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
|
7
13
|
from auto_editor.lib.contracts import is_int, is_str
|
8
14
|
from auto_editor.make_layers import make_timeline
|
9
|
-
from auto_editor.output import Ensure,
|
15
|
+
from auto_editor.output import Ensure, parse_bitrate
|
10
16
|
from auto_editor.render.audio import make_new_audio
|
11
17
|
from auto_editor.render.subtitle import make_new_subtitles
|
12
18
|
from auto_editor.render.video import render_av
|
@@ -15,7 +21,6 @@ from auto_editor.utils.bar import initBar
|
|
15
21
|
from auto_editor.utils.chunks import Chunk, Chunks
|
16
22
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
17
23
|
from auto_editor.utils.container import Container, container_constructor
|
18
|
-
from auto_editor.utils.func import open_with_system_default
|
19
24
|
from auto_editor.utils.log import Log
|
20
25
|
from auto_editor.utils.types import Args
|
21
26
|
|
@@ -91,11 +96,19 @@ def set_audio_codec(
|
|
91
96
|
codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
|
92
97
|
) -> str:
|
93
98
|
if codec == "auto":
|
94
|
-
|
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"
|
95
106
|
if codec not in ctr.acodecs and ctr.default_aud != "none":
|
96
|
-
|
107
|
+
codec = ctr.default_aud
|
97
108
|
if codec == "mp3float":
|
98
|
-
|
109
|
+
codec = "mp3"
|
110
|
+
if codec is None:
|
111
|
+
codec = "aac"
|
99
112
|
return codec
|
100
113
|
|
101
114
|
if codec == "copy":
|
@@ -105,9 +118,8 @@ def set_audio_codec(
|
|
105
118
|
log.error("Input file does not have an audio stream to copy codec from.")
|
106
119
|
codec = src.audios[0].codec
|
107
120
|
|
108
|
-
if codec
|
109
|
-
|
110
|
-
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))
|
111
123
|
|
112
124
|
return codec
|
113
125
|
|
@@ -269,49 +281,150 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
269
281
|
if args.keep_tracks_separate and ctr.max_audios == 1:
|
270
282
|
log.warning(f"'{out_ext}' container doesn't support multiple audio tracks.")
|
271
283
|
|
272
|
-
def make_media(tl: v3,
|
284
|
+
def make_media(tl: v3, output_path: str) -> None:
|
273
285
|
assert src is not None
|
274
286
|
|
275
|
-
|
276
|
-
audio_output = []
|
277
|
-
sub_output = []
|
278
|
-
apply_later = False
|
287
|
+
output = av.open(output_path, "w")
|
279
288
|
|
280
|
-
ensure = Ensure(ffmpeg, bar, samplerate, log)
|
281
289
|
if ctr.default_sub != "none" and not args.sn:
|
282
|
-
|
290
|
+
sub_paths = make_new_subtitles(tl, log)
|
291
|
+
else:
|
292
|
+
sub_paths = []
|
283
293
|
|
284
294
|
if ctr.default_aud != "none":
|
285
|
-
|
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
|
-
|
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()
|
315
428
|
|
316
429
|
if export == "clip-sequence":
|
317
430
|
if tl.v1 is None:
|
@@ -328,7 +441,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
328
441
|
total_frames = tl.v1.chunks[-1][1] - 1
|
329
442
|
clip_num = 0
|
330
443
|
for chunk in tl.v1.chunks:
|
331
|
-
if chunk[2] == 99999:
|
444
|
+
if chunk[2] == 0 or chunk[2] >= 99999:
|
332
445
|
continue
|
333
446
|
|
334
447
|
padded_chunks = pad_chunk(chunk, total_frames)
|
@@ -354,11 +467,23 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
354
467
|
|
355
468
|
log.stop_timer()
|
356
469
|
|
357
|
-
if not args.no_open and export in ("default", "audio"
|
470
|
+
if not args.no_open and export in ("default", "audio"):
|
358
471
|
if args.player is None:
|
359
|
-
|
472
|
+
if sys.platform == "win32":
|
473
|
+
try:
|
474
|
+
os.startfile(output)
|
475
|
+
except OSError:
|
476
|
+
log.warning(f"Could not find application to open file: {output}")
|
477
|
+
else:
|
478
|
+
try: # MacOS case
|
479
|
+
run(["open", output])
|
480
|
+
except Exception:
|
481
|
+
try: # WSL2 case
|
482
|
+
run(["cmd.exe", "/C", "start", output])
|
483
|
+
except Exception:
|
484
|
+
try: # Linux case
|
485
|
+
run(["xdg-open", output])
|
486
|
+
except Exception:
|
487
|
+
log.warning(f"Could not open output file: {output}")
|
360
488
|
else:
|
361
|
-
|
362
|
-
from shlex import split
|
363
|
-
|
364
|
-
subprocess.run(split(args.player) + [output])
|
489
|
+
run(__import__("shlex").split(args.player) + [output])
|