auto-editor 28.1.0__py3-none-any.whl → 29.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-28.1.0.dist-info → auto_editor-29.0.0.dist-info}/METADATA +4 -3
- auto_editor-29.0.0.dist-info/RECORD +5 -0
- auto_editor-29.0.0.dist-info/top_level.txt +1 -0
- auto_editor/__init__.py +0 -1
- auto_editor/__main__.py +0 -504
- auto_editor/analyze.py +0 -393
- auto_editor/cmds/__init__.py +0 -0
- auto_editor/cmds/cache.py +0 -69
- auto_editor/cmds/desc.py +0 -32
- auto_editor/cmds/info.py +0 -213
- auto_editor/cmds/levels.py +0 -199
- auto_editor/cmds/palet.py +0 -29
- auto_editor/cmds/repl.py +0 -113
- auto_editor/cmds/subdump.py +0 -72
- auto_editor/cmds/test.py +0 -816
- auto_editor/edit.py +0 -560
- auto_editor/exports/__init__.py +0 -0
- auto_editor/exports/fcp11.py +0 -195
- auto_editor/exports/fcp7.py +0 -313
- auto_editor/exports/json.py +0 -63
- auto_editor/exports/kdenlive.py +0 -322
- auto_editor/exports/shotcut.py +0 -147
- auto_editor/ffwrapper.py +0 -187
- auto_editor/help.py +0 -224
- auto_editor/imports/__init__.py +0 -0
- auto_editor/imports/fcp7.py +0 -275
- auto_editor/imports/json.py +0 -234
- auto_editor/json.py +0 -297
- auto_editor/lang/__init__.py +0 -0
- auto_editor/lang/libintrospection.py +0 -10
- auto_editor/lang/libmath.py +0 -23
- auto_editor/lang/palet.py +0 -724
- auto_editor/lang/stdenv.py +0 -1179
- auto_editor/lib/__init__.py +0 -0
- auto_editor/lib/contracts.py +0 -235
- auto_editor/lib/data_structs.py +0 -278
- auto_editor/lib/err.py +0 -2
- auto_editor/make_layers.py +0 -315
- auto_editor/preview.py +0 -93
- auto_editor/render/__init__.py +0 -0
- auto_editor/render/audio.py +0 -517
- auto_editor/render/subtitle.py +0 -205
- auto_editor/render/video.py +0 -307
- auto_editor/timeline.py +0 -331
- auto_editor/utils/__init__.py +0 -0
- auto_editor/utils/bar.py +0 -142
- auto_editor/utils/chunks.py +0 -2
- auto_editor/utils/cmdkw.py +0 -206
- auto_editor/utils/container.py +0 -101
- auto_editor/utils/func.py +0 -128
- auto_editor/utils/log.py +0 -126
- auto_editor/utils/types.py +0 -277
- auto_editor/vanparse.py +0 -313
- auto_editor-28.1.0.dist-info/RECORD +0 -57
- auto_editor-28.1.0.dist-info/entry_points.txt +0 -6
- auto_editor-28.1.0.dist-info/top_level.txt +0 -2
- docs/build.py +0 -70
- {auto_editor-28.1.0.dist-info → auto_editor-29.0.0.dist-info}/WHEEL +0 -0
- {auto_editor-28.1.0.dist-info → auto_editor-29.0.0.dist-info}/licenses/LICENSE +0 -0
auto_editor/edit.py
DELETED
@@ -1,560 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import os
|
4
|
-
import sys
|
5
|
-
from fractions import Fraction
|
6
|
-
from heapq import heappop, heappush
|
7
|
-
from os.path import splitext
|
8
|
-
from pathlib import Path
|
9
|
-
from subprocess import run
|
10
|
-
from typing import TYPE_CHECKING, Any
|
11
|
-
|
12
|
-
import av
|
13
|
-
from av import Codec
|
14
|
-
|
15
|
-
from auto_editor.ffwrapper import FileInfo
|
16
|
-
from auto_editor.lib.contracts import is_int, is_str
|
17
|
-
from auto_editor.make_layers import clipify, make_av, make_timeline
|
18
|
-
from auto_editor.render.audio import make_new_audio
|
19
|
-
from auto_editor.render.subtitle import make_new_subtitles
|
20
|
-
from auto_editor.render.video import render_av
|
21
|
-
from auto_editor.timeline import set_stream_to_0, v1, v3
|
22
|
-
from auto_editor.utils.bar import initBar
|
23
|
-
from auto_editor.utils.chunks import Chunk, Chunks
|
24
|
-
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
25
|
-
from auto_editor.utils.container import Container, container_constructor
|
26
|
-
from auto_editor.utils.log import Log
|
27
|
-
|
28
|
-
if TYPE_CHECKING:
|
29
|
-
from auto_editor.__main__ import Args
|
30
|
-
|
31
|
-
|
32
|
-
def set_output(
|
33
|
-
out: str | None, export: str | None, path: Path | None, log: Log
|
34
|
-
) -> tuple[str, str]:
|
35
|
-
if out is None or out == "-":
|
36
|
-
if path is None:
|
37
|
-
log.error("`--output` must be set.") # When a timeline file is the input.
|
38
|
-
root, ext = splitext(path)
|
39
|
-
else:
|
40
|
-
root, ext = splitext(out)
|
41
|
-
|
42
|
-
if ext == "":
|
43
|
-
# Use `mp4` as the default, because it is most compatible.
|
44
|
-
ext = ".mp4" if path is None else path.suffix
|
45
|
-
|
46
|
-
if export is None:
|
47
|
-
match ext:
|
48
|
-
case ".xml":
|
49
|
-
export = "premiere"
|
50
|
-
case ".fcpxml":
|
51
|
-
export = "final-cut-pro"
|
52
|
-
case ".mlt":
|
53
|
-
export = "shotcut"
|
54
|
-
case ".kdenlive":
|
55
|
-
export = "kdenlive"
|
56
|
-
case ".json" | ".v1":
|
57
|
-
export = "v1"
|
58
|
-
case ".v3":
|
59
|
-
export = "v3"
|
60
|
-
case _:
|
61
|
-
export = "default"
|
62
|
-
|
63
|
-
match export:
|
64
|
-
case "premiere" | "resolve-fcp7":
|
65
|
-
ext = ".xml"
|
66
|
-
case "final-cut-pro" | "resolve":
|
67
|
-
ext = ".fcpxml"
|
68
|
-
case "shotcut":
|
69
|
-
ext = ".mlt"
|
70
|
-
case "kdenlive":
|
71
|
-
ext = ".kdenlive"
|
72
|
-
case "v1":
|
73
|
-
if ext != ".json":
|
74
|
-
ext = ".v1"
|
75
|
-
case "v3":
|
76
|
-
ext = ".v3"
|
77
|
-
|
78
|
-
if out == "-":
|
79
|
-
return "-", export
|
80
|
-
|
81
|
-
if out is None:
|
82
|
-
return f"{root}_ALTERED{ext}", export
|
83
|
-
|
84
|
-
return f"{root}{ext}", export
|
85
|
-
|
86
|
-
|
87
|
-
def set_video_codec(
|
88
|
-
codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
|
89
|
-
) -> str:
|
90
|
-
if codec == "auto":
|
91
|
-
codec = "h264" if (src is None or not src.videos) else src.videos[0].codec
|
92
|
-
if codec not in ctr.vcodecs and ctr.default_vid != "none":
|
93
|
-
return ctr.default_vid
|
94
|
-
return codec
|
95
|
-
|
96
|
-
if ctr.vcodecs is not None and codec not in ctr.vcodecs:
|
97
|
-
try:
|
98
|
-
cobj = Codec(codec, "w")
|
99
|
-
except av.codec.codec.UnknownCodecError:
|
100
|
-
log.error(f"Unknown encoder: {codec}")
|
101
|
-
# Normalize encoder names
|
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
|
-
)
|
106
|
-
|
107
|
-
return codec
|
108
|
-
|
109
|
-
|
110
|
-
def set_audio_codec(
|
111
|
-
codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
|
112
|
-
) -> str:
|
113
|
-
if codec == "auto":
|
114
|
-
if src is None or not src.audios:
|
115
|
-
codec = "aac"
|
116
|
-
else:
|
117
|
-
codec = src.audios[0].codec
|
118
|
-
if Codec(codec, "w").audio_formats is None:
|
119
|
-
codec = "aac"
|
120
|
-
if codec not in ctr.acodecs and ctr.default_aud != "none":
|
121
|
-
codec = ctr.default_aud
|
122
|
-
if codec is None:
|
123
|
-
codec = "aac"
|
124
|
-
return codec
|
125
|
-
|
126
|
-
if ctr.acodecs is None or codec not in ctr.acodecs:
|
127
|
-
try:
|
128
|
-
cobj = Codec(codec, "w")
|
129
|
-
except av.codec.codec.UnknownCodecError:
|
130
|
-
log.error(f"Unknown encoder: {codec}")
|
131
|
-
# Normalize encoder names
|
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
|
-
)
|
136
|
-
return codec
|
137
|
-
|
138
|
-
|
139
|
-
def parse_export(export: str, log: Log) -> dict[str, Any]:
|
140
|
-
exploded = export.split(":", maxsplit=1)
|
141
|
-
if len(exploded) == 1:
|
142
|
-
name, text = exploded[0], ""
|
143
|
-
else:
|
144
|
-
name, text = exploded
|
145
|
-
|
146
|
-
name_attr = pAttr("name", "Auto-Editor Media Group", is_str)
|
147
|
-
parsing = {
|
148
|
-
"default": pAttrs("default"),
|
149
|
-
"premiere": pAttrs("premiere", name_attr),
|
150
|
-
"final-cut-pro": pAttrs(
|
151
|
-
"final-cut-pro", name_attr, pAttr("version", 11, is_int)
|
152
|
-
),
|
153
|
-
"resolve": pAttrs("resolve", name_attr),
|
154
|
-
"resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
|
155
|
-
"shotcut": pAttrs("shotcut"),
|
156
|
-
"kdenlive": pAttrs("kdenlive"),
|
157
|
-
"v1": pAttrs("v1"),
|
158
|
-
"v3": pAttrs("v3"),
|
159
|
-
"clip-sequence": pAttrs("clip-sequence"),
|
160
|
-
}
|
161
|
-
|
162
|
-
if name in parsing:
|
163
|
-
try:
|
164
|
-
return {"export": name} | parse_with_palet(text, parsing[name], {})
|
165
|
-
except ParserError as e:
|
166
|
-
log.error(e)
|
167
|
-
|
168
|
-
valid_choices = " ".join(f'"{s}"' for s in parsing.keys())
|
169
|
-
log.error(f'Invalid export format: "{name}"\nValid choices: {valid_choices}')
|
170
|
-
|
171
|
-
|
172
|
-
def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
173
|
-
bar = initBar(args.progress)
|
174
|
-
tl = src = use_path = None
|
175
|
-
|
176
|
-
if paths:
|
177
|
-
path_ext = splitext(paths[0])[1].lower()
|
178
|
-
if path_ext == ".xml":
|
179
|
-
from auto_editor.imports.fcp7 import fcp7_read_xml
|
180
|
-
|
181
|
-
tl = fcp7_read_xml(paths[0], log)
|
182
|
-
elif path_ext == ".mlt":
|
183
|
-
log.error("Reading mlt files not implemented")
|
184
|
-
elif path_ext in {".v1", ".v3", ".json"}:
|
185
|
-
from auto_editor.imports.json import read_json
|
186
|
-
|
187
|
-
tl = read_json(paths[0], log)
|
188
|
-
else:
|
189
|
-
sources = [FileInfo.init(path, log) for path in paths]
|
190
|
-
src = sources[0]
|
191
|
-
use_path = src.path
|
192
|
-
|
193
|
-
if args.export is None:
|
194
|
-
output, export = set_output(args.output, args.export, use_path, log)
|
195
|
-
export_ops: dict[str, Any] = {"export": export}
|
196
|
-
else:
|
197
|
-
export_ops = parse_export(args.export, log)
|
198
|
-
export = export_ops["export"]
|
199
|
-
output, _ = set_output(args.output, export, use_path, log)
|
200
|
-
|
201
|
-
if output == "-":
|
202
|
-
# When printing to stdout, silence all logs.
|
203
|
-
log.quiet = True
|
204
|
-
|
205
|
-
if not args.preview:
|
206
|
-
log.conwrite("Starting")
|
207
|
-
|
208
|
-
if os.path.isdir(output):
|
209
|
-
log.error("Output path already has an existing directory!")
|
210
|
-
|
211
|
-
if args.sample_rate is None:
|
212
|
-
if tl is None:
|
213
|
-
samplerate = 48000 if src is None else src.get_sr()
|
214
|
-
else:
|
215
|
-
samplerate = tl.sr
|
216
|
-
else:
|
217
|
-
samplerate = args.sample_rate
|
218
|
-
|
219
|
-
if tl is None:
|
220
|
-
tl = make_timeline(sources, args, samplerate, bar, log)
|
221
|
-
else:
|
222
|
-
if args.resolution is not None:
|
223
|
-
tl.T.res = args.resolution
|
224
|
-
if args.background is not None:
|
225
|
-
tl.background = args.background
|
226
|
-
if args.frame_rate is not None:
|
227
|
-
log.warning(
|
228
|
-
"Setting timebase/framerate is not supported when importing timelines"
|
229
|
-
)
|
230
|
-
|
231
|
-
if args.preview:
|
232
|
-
from auto_editor.preview import preview
|
233
|
-
|
234
|
-
preview(tl, log)
|
235
|
-
return
|
236
|
-
|
237
|
-
if export in {"v1", "v3"}:
|
238
|
-
from auto_editor.exports.json import make_json_timeline
|
239
|
-
|
240
|
-
make_json_timeline(export, output, tl, log)
|
241
|
-
return
|
242
|
-
|
243
|
-
if export in {"premiere", "resolve-fcp7"}:
|
244
|
-
from auto_editor.exports.fcp7 import fcp7_write_xml
|
245
|
-
|
246
|
-
is_resolve = export.startswith("resolve")
|
247
|
-
fcp7_write_xml(export_ops["name"], output, is_resolve, tl)
|
248
|
-
return
|
249
|
-
|
250
|
-
if export == "final-cut-pro":
|
251
|
-
from auto_editor.exports.fcp11 import fcp11_write_xml
|
252
|
-
|
253
|
-
ver = export_ops["version"]
|
254
|
-
fcp11_write_xml(export_ops["name"], ver, output, False, tl, log)
|
255
|
-
return
|
256
|
-
|
257
|
-
if export == "resolve":
|
258
|
-
from auto_editor.exports.fcp11 import fcp11_write_xml
|
259
|
-
|
260
|
-
set_stream_to_0(tl, log)
|
261
|
-
fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
|
262
|
-
return
|
263
|
-
|
264
|
-
if export == "shotcut":
|
265
|
-
from auto_editor.exports.shotcut import shotcut_write_mlt
|
266
|
-
|
267
|
-
shotcut_write_mlt(output, tl)
|
268
|
-
return
|
269
|
-
|
270
|
-
if export == "kdenlive":
|
271
|
-
from auto_editor.exports.kdenlive import kdenlive_write
|
272
|
-
|
273
|
-
kdenlive_write(output, tl)
|
274
|
-
return
|
275
|
-
|
276
|
-
if output == "-":
|
277
|
-
log.error("Exporting media files to stdout is not supported.")
|
278
|
-
out_ext = splitext(output)[1].replace(".", "")
|
279
|
-
|
280
|
-
# Check if export options make sense.
|
281
|
-
ctr = container_constructor(out_ext.lower(), log)
|
282
|
-
|
283
|
-
if ctr.samplerate is not None and args.sample_rate not in ctr.samplerate:
|
284
|
-
log.error(f"'{out_ext}' container only supports samplerates: {ctr.samplerate}")
|
285
|
-
|
286
|
-
args.video_codec = set_video_codec(args.video_codec, src, out_ext, ctr, log)
|
287
|
-
args.audio_codec = set_audio_codec(args.audio_codec, src, out_ext, ctr, log)
|
288
|
-
|
289
|
-
def make_media(tl: v3, output_path: str) -> None:
|
290
|
-
options = {}
|
291
|
-
mov_flags = []
|
292
|
-
if args.fragmented and not args.no_fragmented:
|
293
|
-
mov_flags.extend(["default_base_moof", "frag_keyframe", "separate_moof"])
|
294
|
-
options["frag_duration"] = "0.2"
|
295
|
-
if args.faststart:
|
296
|
-
log.warning("Fragmented is enabled, will not apply faststart.")
|
297
|
-
elif not args.no_faststart:
|
298
|
-
mov_flags.append("faststart")
|
299
|
-
if mov_flags:
|
300
|
-
options["movflags"] = "+".join(mov_flags)
|
301
|
-
|
302
|
-
output = av.open(output_path, "w", container_options=options)
|
303
|
-
|
304
|
-
# Setup video
|
305
|
-
if ctr.default_vid not in ("none", "png") and tl.v:
|
306
|
-
vframes = render_av(output, tl, args, log)
|
307
|
-
output_stream: av.VideoStream | None
|
308
|
-
output_stream = next(vframes) # type: ignore
|
309
|
-
else:
|
310
|
-
output_stream, vframes = None, iter([])
|
311
|
-
|
312
|
-
# Setup audio
|
313
|
-
try:
|
314
|
-
audio_encoder = Codec(args.audio_codec, "w")
|
315
|
-
except av.FFmpegError as e:
|
316
|
-
log.error(e)
|
317
|
-
if audio_encoder.audio_formats is None:
|
318
|
-
log.error(f"{args.audio_codec}: No known audio formats avail.")
|
319
|
-
fmt = audio_encoder.audio_formats[0]
|
320
|
-
|
321
|
-
audio_streams: list[av.AudioStream] = []
|
322
|
-
|
323
|
-
if ctr.default_aud == "none":
|
324
|
-
while len(tl.a) > 0:
|
325
|
-
tl.a.pop()
|
326
|
-
elif len(tl.a) > 1 and ctr.max_audios == 1:
|
327
|
-
log.warning("Dropping extra audio streams (container only allows one)")
|
328
|
-
|
329
|
-
while len(tl.a) > 1:
|
330
|
-
tl.a.pop()
|
331
|
-
|
332
|
-
if len(tl.a) > 0:
|
333
|
-
audio_streams, audio_gen_frames = make_new_audio(output, fmt, tl, args, log)
|
334
|
-
else:
|
335
|
-
audio_streams, audio_gen_frames = [], [iter([])]
|
336
|
-
|
337
|
-
# Setup subtitles
|
338
|
-
if ctr.default_sub != "none" and not args.sn:
|
339
|
-
sub_paths = make_new_subtitles(tl, log)
|
340
|
-
else:
|
341
|
-
sub_paths = []
|
342
|
-
|
343
|
-
subtitle_streams = []
|
344
|
-
subtitle_inputs = []
|
345
|
-
sub_gen_frames = []
|
346
|
-
|
347
|
-
for i, sub_path in enumerate(sub_paths):
|
348
|
-
subtitle_input = av.open(sub_path)
|
349
|
-
subtitle_inputs.append(subtitle_input)
|
350
|
-
subtitle_stream = output.add_stream_from_template(
|
351
|
-
subtitle_input.streams.subtitles[0]
|
352
|
-
)
|
353
|
-
if i < len(tl.T.subtitles) and (lang := tl.T.subtitles[i].lang) is not None:
|
354
|
-
subtitle_stream.metadata["language"] = lang
|
355
|
-
|
356
|
-
subtitle_streams.append(subtitle_stream)
|
357
|
-
sub_gen_frames.append(subtitle_input.demux(subtitles=0))
|
358
|
-
|
359
|
-
no_color = log.no_color or log.machine
|
360
|
-
encoder_titles = []
|
361
|
-
if output_stream is not None:
|
362
|
-
name = output_stream.codec.canonical_name
|
363
|
-
encoder_titles.append(name if no_color else f"\033[95m{name}")
|
364
|
-
if audio_streams:
|
365
|
-
name = audio_streams[0].codec.canonical_name
|
366
|
-
encoder_titles.append(name if no_color else f"\033[96m{name}")
|
367
|
-
if subtitle_streams:
|
368
|
-
name = subtitle_streams[0].codec.canonical_name
|
369
|
-
encoder_titles.append(name if no_color else f"\033[32m{name}")
|
370
|
-
|
371
|
-
title = f"({os.path.splitext(output_path)[1][1:]}) "
|
372
|
-
if no_color:
|
373
|
-
title += "+".join(encoder_titles)
|
374
|
-
else:
|
375
|
-
title += "\033[0m+".join(encoder_titles) + "\033[0m"
|
376
|
-
bar.start(tl.end, title)
|
377
|
-
|
378
|
-
MAX_AUDIO_AHEAD = 30 # In timebase, how far audio can be ahead of video.
|
379
|
-
MAX_SUB_AHEAD = 30
|
380
|
-
|
381
|
-
class Priority:
|
382
|
-
__slots__ = ("index", "frame_type", "frame", "stream")
|
383
|
-
|
384
|
-
def __init__(self, value: int | Fraction, frame, stream):
|
385
|
-
self.frame_type: str = stream.type
|
386
|
-
assert self.frame_type in ("audio", "subtitle", "video")
|
387
|
-
if self.frame_type in {"audio", "subtitle"}:
|
388
|
-
self.index: int | float = round(value * frame.time_base * tl.tb)
|
389
|
-
else:
|
390
|
-
self.index = float("inf") if value is None else int(value)
|
391
|
-
self.frame = frame
|
392
|
-
self.stream = stream
|
393
|
-
|
394
|
-
def __lt__(self, other):
|
395
|
-
return self.index < other.index
|
396
|
-
|
397
|
-
def __eq__(self, other):
|
398
|
-
return self.index == other.index
|
399
|
-
|
400
|
-
# Priority queue for ordered frames by time_base.
|
401
|
-
frame_queue: list[Priority] = []
|
402
|
-
latest_audio_index = float("-inf")
|
403
|
-
latest_sub_index = float("-inf")
|
404
|
-
earliest_video_index = None
|
405
|
-
|
406
|
-
while True:
|
407
|
-
if earliest_video_index is None:
|
408
|
-
should_get_audio = True
|
409
|
-
should_get_sub = True
|
410
|
-
else:
|
411
|
-
for item in frame_queue:
|
412
|
-
if item.frame_type == "audio":
|
413
|
-
latest_audio_index = max(latest_audio_index, item.index)
|
414
|
-
elif item.frame_type == "subtitle":
|
415
|
-
latest_sub_index = max(latest_sub_index, item.index)
|
416
|
-
|
417
|
-
should_get_audio = (
|
418
|
-
latest_audio_index <= earliest_video_index + MAX_AUDIO_AHEAD
|
419
|
-
)
|
420
|
-
should_get_sub = (
|
421
|
-
latest_sub_index <= earliest_video_index + MAX_SUB_AHEAD
|
422
|
-
)
|
423
|
-
|
424
|
-
index, video_frame = next(vframes, (0, None))
|
425
|
-
|
426
|
-
if video_frame:
|
427
|
-
earliest_video_index = index
|
428
|
-
heappush(frame_queue, Priority(index, video_frame, output_stream))
|
429
|
-
|
430
|
-
if should_get_audio:
|
431
|
-
audio_frames = [next(frames, None) for frames in audio_gen_frames]
|
432
|
-
if output_stream is None and audio_frames and audio_frames[-1]:
|
433
|
-
assert audio_frames[-1].time is not None
|
434
|
-
index = round(audio_frames[-1].time * tl.tb)
|
435
|
-
else:
|
436
|
-
audio_frames = [None]
|
437
|
-
if should_get_sub:
|
438
|
-
subtitle_frames = [next(packet, None) for packet in sub_gen_frames]
|
439
|
-
else:
|
440
|
-
subtitle_frames = [None]
|
441
|
-
|
442
|
-
# Break if no more frames
|
443
|
-
if (
|
444
|
-
all(frame is None for frame in audio_frames)
|
445
|
-
and video_frame is None
|
446
|
-
and all(packet is None for packet in subtitle_frames)
|
447
|
-
):
|
448
|
-
break
|
449
|
-
|
450
|
-
if should_get_audio:
|
451
|
-
for audio_stream, aframe in zip(audio_streams, audio_frames):
|
452
|
-
if aframe is None:
|
453
|
-
continue
|
454
|
-
assert aframe.pts is not None
|
455
|
-
heappush(frame_queue, Priority(aframe.pts, aframe, audio_stream))
|
456
|
-
if should_get_sub:
|
457
|
-
for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
|
458
|
-
if packet and packet.pts is not None:
|
459
|
-
packet.stream = subtitle_stream
|
460
|
-
heappush(
|
461
|
-
frame_queue, Priority(packet.pts, packet, subtitle_stream)
|
462
|
-
)
|
463
|
-
|
464
|
-
while frame_queue and frame_queue[0].index <= index:
|
465
|
-
item = heappop(frame_queue)
|
466
|
-
frame_type = item.frame_type
|
467
|
-
bar_index = None
|
468
|
-
try:
|
469
|
-
if frame_type in {"video", "audio"}:
|
470
|
-
if item.frame.time is not None:
|
471
|
-
bar_index = round(item.frame.time * tl.tb)
|
472
|
-
output.mux(item.stream.encode(item.frame))
|
473
|
-
elif frame_type == "subtitle":
|
474
|
-
output.mux(item.frame)
|
475
|
-
except av.error.ExternalError:
|
476
|
-
log.error(
|
477
|
-
f"Generic error for encoder: {item.stream.name}\n"
|
478
|
-
f"at {item.index} time_base\nPerhaps video quality settings are too low?"
|
479
|
-
)
|
480
|
-
except av.FileNotFoundError:
|
481
|
-
log.error(f"File not found: {output_path}")
|
482
|
-
except av.FFmpegError as e:
|
483
|
-
log.error(e)
|
484
|
-
|
485
|
-
if bar_index:
|
486
|
-
bar.tick(bar_index)
|
487
|
-
|
488
|
-
# Flush streams
|
489
|
-
if output_stream is not None:
|
490
|
-
output.mux(output_stream.encode(None))
|
491
|
-
for audio_stream in audio_streams:
|
492
|
-
output.mux(audio_stream.encode(None))
|
493
|
-
|
494
|
-
bar.end()
|
495
|
-
|
496
|
-
# Close resources
|
497
|
-
for subtitle_input in subtitle_inputs:
|
498
|
-
subtitle_input.close()
|
499
|
-
output.close()
|
500
|
-
|
501
|
-
if export == "clip-sequence":
|
502
|
-
if tl.v1 is None:
|
503
|
-
log.error("Timeline too complex to use clip-sequence export")
|
504
|
-
|
505
|
-
def pad_chunk(chunk: Chunk, total: int) -> Chunks:
|
506
|
-
start = [] if chunk[0] == 0 else [(0, chunk[0], 99999.0)]
|
507
|
-
end = [] if chunk[1] == total else [(chunk[1], total, 99999.0)]
|
508
|
-
return start + [chunk] + end
|
509
|
-
|
510
|
-
def append_filename(path: str, val: str) -> str:
|
511
|
-
root, ext = splitext(path)
|
512
|
-
return root + val + ext
|
513
|
-
|
514
|
-
total_frames = tl.v1.chunks[-1][1] - 1
|
515
|
-
clip_num = 0
|
516
|
-
for chunk in tl.v1.chunks:
|
517
|
-
if chunk[2] == 0 or chunk[2] >= 99999:
|
518
|
-
continue
|
519
|
-
|
520
|
-
padded_chunks = pad_chunk(chunk, total_frames)
|
521
|
-
|
522
|
-
vspace, aspace = make_av(
|
523
|
-
tl.v1.source, [clipify(padded_chunks, tl.v1.source)]
|
524
|
-
)
|
525
|
-
my_timeline = v3(
|
526
|
-
tl.tb,
|
527
|
-
"#000",
|
528
|
-
tl.template,
|
529
|
-
vspace,
|
530
|
-
aspace,
|
531
|
-
v1(tl.v1.source, padded_chunks),
|
532
|
-
)
|
533
|
-
|
534
|
-
make_media(my_timeline, append_filename(output, f"-{clip_num}"))
|
535
|
-
clip_num += 1
|
536
|
-
else:
|
537
|
-
make_media(tl, output)
|
538
|
-
|
539
|
-
log.stop_timer()
|
540
|
-
|
541
|
-
if not args.no_open and export == "default":
|
542
|
-
if args.player is None:
|
543
|
-
if sys.platform == "win32":
|
544
|
-
try:
|
545
|
-
os.startfile(output)
|
546
|
-
except OSError:
|
547
|
-
log.warning(f"Could not find application to open file: {output}")
|
548
|
-
else:
|
549
|
-
try: # MacOS case
|
550
|
-
run(["open", output])
|
551
|
-
except Exception:
|
552
|
-
try: # WSL2 case
|
553
|
-
run(["cmd.exe", "/C", "start", output])
|
554
|
-
except Exception:
|
555
|
-
try: # Linux case
|
556
|
-
run(["xdg-open", output])
|
557
|
-
except Exception:
|
558
|
-
log.warning(f"Could not open output file: {output}")
|
559
|
-
else:
|
560
|
-
run(__import__("shlex").split(args.player) + [output])
|
auto_editor/exports/__init__.py
DELETED
File without changes
|