auto-editor 26.3.3__py3-none-any.whl → 27.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 +17 -5
- auto_editor/analyze.py +30 -36
- auto_editor/cmds/desc.py +2 -2
- auto_editor/cmds/info.py +3 -3
- auto_editor/cmds/levels.py +5 -5
- auto_editor/cmds/repl.py +3 -8
- auto_editor/cmds/subdump.py +62 -8
- auto_editor/cmds/test.py +92 -42
- auto_editor/edit.py +59 -111
- auto_editor/ffwrapper.py +91 -87
- auto_editor/formats/fcp11.py +10 -8
- auto_editor/formats/fcp7.py +11 -12
- auto_editor/formats/json.py +10 -11
- auto_editor/{lang/json.py → json.py} +39 -43
- auto_editor/lang/palet.py +2 -2
- auto_editor/lang/stdenv.py +13 -0
- auto_editor/make_layers.py +18 -8
- auto_editor/render/audio.py +239 -102
- auto_editor/render/subtitle.py +10 -14
- auto_editor/render/video.py +41 -46
- auto_editor/timeline.py +60 -10
- auto_editor/utils/container.py +21 -14
- auto_editor/utils/func.py +21 -0
- {auto_editor-26.3.3.dist-info → auto_editor-27.1.0.dist-info}/METADATA +8 -7
- auto_editor-27.1.0.dist-info/RECORD +54 -0
- {auto_editor-26.3.3.dist-info → auto_editor-27.1.0.dist-info}/WHEEL +1 -1
- docs/build.py +16 -7
- auto_editor/output.py +0 -86
- auto_editor/wavfile.py +0 -310
- auto_editor-26.3.3.dist-info/RECORD +0 -56
- {auto_editor-26.3.3.dist-info → auto_editor-27.1.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.3.3.dist-info → auto_editor-27.1.0.dist-info/licenses}/LICENSE +0 -0
- {auto_editor-26.3.3.dist-info → auto_editor-27.1.0.dist-info}/top_level.txt +0 -0
auto_editor/edit.py
CHANGED
@@ -8,13 +8,11 @@ from os.path import splitext
|
|
8
8
|
from subprocess import run
|
9
9
|
from typing import TYPE_CHECKING, Any
|
10
10
|
|
11
|
-
import
|
12
|
-
from av import AudioResampler, Codec
|
11
|
+
import bv
|
13
12
|
|
14
|
-
from auto_editor.ffwrapper import FileInfo
|
13
|
+
from auto_editor.ffwrapper import FileInfo
|
15
14
|
from auto_editor.lib.contracts import is_int, is_str
|
16
15
|
from auto_editor.make_layers import clipify, make_av, make_timeline
|
17
|
-
from auto_editor.output import Ensure, parse_bitrate
|
18
16
|
from auto_editor.render.audio import make_new_audio
|
19
17
|
from auto_editor.render.subtitle import make_new_subtitles
|
20
18
|
from auto_editor.render.video import render_av
|
@@ -83,21 +81,13 @@ def set_video_codec(
|
|
83
81
|
return ctr.default_vid
|
84
82
|
return codec
|
85
83
|
|
86
|
-
if codec == "copy":
|
87
|
-
log.deprecated("The `copy` codec is deprecated. auto-editor always re-encodes")
|
88
|
-
if src is None:
|
89
|
-
log.error("No input to copy its codec from.")
|
90
|
-
if not src.videos:
|
91
|
-
log.error("Input file does not have a video stream to copy codec from.")
|
92
|
-
codec = src.videos[0].codec
|
93
|
-
|
94
84
|
if ctr.vcodecs is not None and codec not in ctr.vcodecs:
|
95
85
|
try:
|
96
|
-
cobj = Codec(codec, "w")
|
97
|
-
except
|
86
|
+
cobj = bv.Codec(codec, "w")
|
87
|
+
except bv.codec.codec.UnknownCodecError:
|
98
88
|
log.error(f"Unknown encoder: {codec}")
|
99
89
|
# Normalize encoder names
|
100
|
-
if cobj.id not in (Codec(x, "w").id for x in ctr.vcodecs):
|
90
|
+
if cobj.id not in (bv.Codec(x, "w").id for x in ctr.vcodecs):
|
101
91
|
log.error(codec_error.format(codec, out_ext))
|
102
92
|
|
103
93
|
return codec
|
@@ -111,7 +101,7 @@ def set_audio_codec(
|
|
111
101
|
codec = "aac"
|
112
102
|
else:
|
113
103
|
codec = src.audios[0].codec
|
114
|
-
if
|
104
|
+
if bv.Codec(codec, "w").audio_formats is None:
|
115
105
|
codec = "aac"
|
116
106
|
if codec not in ctr.acodecs and ctr.default_aud != "none":
|
117
107
|
codec = ctr.default_aud
|
@@ -119,21 +109,13 @@ def set_audio_codec(
|
|
119
109
|
codec = "aac"
|
120
110
|
return codec
|
121
111
|
|
122
|
-
if codec == "copy":
|
123
|
-
log.deprecated("The `copy` codec is deprecated. auto-editor always re-encodes")
|
124
|
-
if src is None:
|
125
|
-
log.error("No input to copy its codec from.")
|
126
|
-
if not src.audios:
|
127
|
-
log.error("Input file does not have an audio stream to copy codec from.")
|
128
|
-
codec = src.audios[0].codec
|
129
|
-
|
130
112
|
if ctr.acodecs is None or codec not in ctr.acodecs:
|
131
113
|
try:
|
132
|
-
cobj = Codec(codec, "w")
|
133
|
-
except
|
114
|
+
cobj = bv.Codec(codec, "w")
|
115
|
+
except bv.codec.codec.UnknownCodecError:
|
134
116
|
log.error(f"Unknown encoder: {codec}")
|
135
117
|
# Normalize encoder names
|
136
|
-
if cobj.id not in (Codec(x, "w").id for x in ctr.acodecs):
|
118
|
+
if cobj.id not in (bv.Codec(x, "w").id for x in ctr.acodecs):
|
137
119
|
log.error(codec_error.format(codec, out_ext))
|
138
120
|
|
139
121
|
return codec
|
@@ -177,6 +159,11 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
177
159
|
def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
178
160
|
bar = initBar(args.progress)
|
179
161
|
tl = None
|
162
|
+
src = None
|
163
|
+
|
164
|
+
if args.keep_tracks_separate:
|
165
|
+
log.deprecated("--keep-tracks-separate is deprecated.")
|
166
|
+
args.keep_tracks_separate = False
|
180
167
|
|
181
168
|
if paths:
|
182
169
|
path_ext = splitext(paths[0])[1].lower()
|
@@ -184,30 +171,18 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
184
171
|
from auto_editor.formats.fcp7 import fcp7_read_xml
|
185
172
|
|
186
173
|
tl = fcp7_read_xml(paths[0], log)
|
187
|
-
assert tl.src is not None
|
188
|
-
sources: list[FileInfo] = [tl.src]
|
189
|
-
src: FileInfo | None = tl.src
|
190
|
-
|
191
174
|
elif path_ext == ".mlt":
|
192
175
|
from auto_editor.formats.shotcut import shotcut_read_mlt
|
193
176
|
|
194
177
|
tl = shotcut_read_mlt(paths[0], log)
|
195
|
-
assert tl.src is not None
|
196
|
-
sources = [tl.src]
|
197
|
-
src = tl.src
|
198
|
-
|
199
178
|
elif path_ext == ".json":
|
200
179
|
from auto_editor.formats.json import read_json
|
201
180
|
|
202
181
|
tl = read_json(paths[0], log)
|
203
|
-
sources = [] if tl.src is None else [tl.src]
|
204
|
-
src = tl.src
|
205
182
|
else:
|
206
|
-
sources = [
|
183
|
+
sources = [FileInfo.init(path, log) for path in paths]
|
207
184
|
src = None if not sources else sources[0]
|
208
185
|
|
209
|
-
del paths
|
210
|
-
|
211
186
|
output, export_ops = set_output(args.output, args.export, src, log)
|
212
187
|
assert "export" in export_ops
|
213
188
|
export = export_ops["export"]
|
@@ -268,7 +243,8 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
268
243
|
from auto_editor.formats.fcp11 import fcp11_write_xml
|
269
244
|
from auto_editor.timeline import set_stream_to_0
|
270
245
|
|
271
|
-
|
246
|
+
assert src is not None
|
247
|
+
set_stream_to_0(src, tl, log)
|
272
248
|
fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
|
273
249
|
return
|
274
250
|
|
@@ -281,7 +257,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
281
257
|
out_ext = splitext(output)[1].replace(".", "")
|
282
258
|
|
283
259
|
# Check if export options make sense.
|
284
|
-
ctr = container_constructor(out_ext.lower())
|
260
|
+
ctr = container_constructor(out_ext.lower(), log)
|
285
261
|
|
286
262
|
if ctr.samplerate is not None and args.sample_rate not in ctr.samplerate:
|
287
263
|
log.error(f"'{out_ext}' container only supports samplerates: {ctr.samplerate}")
|
@@ -289,12 +265,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
289
265
|
args.video_codec = set_video_codec(args.video_codec, src, out_ext, ctr, log)
|
290
266
|
args.audio_codec = set_audio_codec(args.audio_codec, src, out_ext, ctr, log)
|
291
267
|
|
292
|
-
if args.keep_tracks_separate and ctr.max_audios == 1:
|
293
|
-
log.warning(f"'{out_ext}' container doesn't support multiple audio tracks.")
|
294
|
-
|
295
268
|
def make_media(tl: v3, output_path: str) -> None:
|
296
|
-
assert src is not None
|
297
|
-
|
298
269
|
options = {}
|
299
270
|
mov_flags = []
|
300
271
|
if args.fragmented and not args.no_fragmented:
|
@@ -307,76 +278,59 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
307
278
|
if mov_flags:
|
308
279
|
options["movflags"] = "+".join(mov_flags)
|
309
280
|
|
310
|
-
output =
|
311
|
-
|
312
|
-
if ctr.default_sub != "none" and not args.sn:
|
313
|
-
sub_paths = make_new_subtitles(tl, log)
|
314
|
-
else:
|
315
|
-
sub_paths = []
|
316
|
-
|
317
|
-
if ctr.default_aud != "none":
|
318
|
-
ensure = Ensure(bar, samplerate, log)
|
319
|
-
audio_paths = make_new_audio(tl, ctr, ensure, args, bar, log)
|
320
|
-
else:
|
321
|
-
audio_paths = []
|
281
|
+
output = bv.open(output_path, "w", container_options=options)
|
322
282
|
|
323
283
|
# Setup video
|
324
284
|
if ctr.default_vid != "none" and tl.v:
|
325
285
|
vframes = render_av(output, tl, args, log)
|
326
|
-
output_stream
|
286
|
+
output_stream: bv.VideoStream | None
|
287
|
+
output_stream = next(vframes) # type: ignore
|
327
288
|
else:
|
328
289
|
output_stream, vframes = None, iter([])
|
329
290
|
|
330
291
|
# Setup audio
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
audio_format = audio_encoder.audio_formats[0]
|
339
|
-
resampler = AudioResampler(format=audio_format, layout="stereo", rate=tl.sr)
|
340
|
-
|
341
|
-
audio_streams: list[av.AudioStream] = []
|
342
|
-
audio_inputs = []
|
343
|
-
audio_gen_frames = []
|
344
|
-
for i, audio_path in enumerate(audio_paths):
|
345
|
-
audio_stream = output.add_stream(
|
346
|
-
args.audio_codec,
|
347
|
-
format=audio_format,
|
348
|
-
rate=tl.sr,
|
349
|
-
time_base=Fraction(1, tl.sr),
|
350
|
-
)
|
351
|
-
if not isinstance(audio_stream, av.AudioStream):
|
352
|
-
log.error(f"Not a known audio codec: {args.audio_codec}")
|
292
|
+
try:
|
293
|
+
audio_encoder = bv.Codec(args.audio_codec, "w")
|
294
|
+
except bv.FFmpegError as e:
|
295
|
+
log.error(e)
|
296
|
+
if audio_encoder.audio_formats is None:
|
297
|
+
log.error(f"{args.audio_codec}: No known audio formats avail.")
|
298
|
+
fmt = audio_encoder.audio_formats[0]
|
353
299
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
300
|
+
audio_streams: list[bv.AudioStream] = []
|
301
|
+
|
302
|
+
if ctr.default_aud == "none":
|
303
|
+
while len(tl.a) > 0:
|
304
|
+
tl.a.pop()
|
305
|
+
elif len(tl.a) > 1 and ctr.max_audios == 1:
|
306
|
+
log.warning("Dropping extra audio streams (container only allows one)")
|
361
307
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
308
|
+
while len(tl.a) > 1:
|
309
|
+
tl.a.pop()
|
310
|
+
|
311
|
+
if len(tl.a) > 0:
|
312
|
+
audio_streams, audio_gen_frames = make_new_audio(output, fmt, tl, args, log)
|
313
|
+
else:
|
314
|
+
audio_streams, audio_gen_frames = [], [iter([])]
|
366
315
|
|
367
316
|
# Setup subtitles
|
317
|
+
if ctr.default_sub != "none" and not args.sn:
|
318
|
+
sub_paths = make_new_subtitles(tl, log)
|
319
|
+
else:
|
320
|
+
sub_paths = []
|
321
|
+
|
368
322
|
subtitle_streams = []
|
369
323
|
subtitle_inputs = []
|
370
324
|
sub_gen_frames = []
|
371
325
|
|
372
326
|
for i, sub_path in enumerate(sub_paths):
|
373
|
-
subtitle_input =
|
327
|
+
subtitle_input = bv.open(sub_path)
|
374
328
|
subtitle_inputs.append(subtitle_input)
|
375
329
|
subtitle_stream = output.add_stream_from_template(
|
376
330
|
subtitle_input.streams.subtitles[0]
|
377
331
|
)
|
378
|
-
if i < len(
|
379
|
-
subtitle_stream.metadata["language"] =
|
332
|
+
if i < len(tl.T.subtitles) and (lang := tl.T.subtitles[i].lang) is not None:
|
333
|
+
subtitle_stream.metadata["language"] = lang
|
380
334
|
|
381
335
|
subtitle_streams.append(subtitle_stream)
|
382
336
|
sub_gen_frames.append(subtitle_input.demux(subtitles=0))
|
@@ -473,13 +427,11 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
473
427
|
break
|
474
428
|
|
475
429
|
if should_get_audio:
|
476
|
-
for audio_stream,
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
Priority(reframe.pts, reframe, audio_stream),
|
482
|
-
)
|
430
|
+
for audio_stream, aframe in zip(audio_streams, audio_frames):
|
431
|
+
if aframe is None:
|
432
|
+
continue
|
433
|
+
assert aframe.pts is not None
|
434
|
+
heappush(frame_queue, Priority(aframe.pts, aframe, audio_stream))
|
483
435
|
if should_get_sub:
|
484
436
|
for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
|
485
437
|
if packet and packet.pts is not None:
|
@@ -499,14 +451,14 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
499
451
|
output.mux(item.stream.encode(item.frame))
|
500
452
|
elif frame_type == "subtitle":
|
501
453
|
output.mux(item.frame)
|
502
|
-
except
|
454
|
+
except bv.error.ExternalError:
|
503
455
|
log.error(
|
504
456
|
f"Generic error for encoder: {item.stream.name}\n"
|
505
457
|
f"at {item.index} time_base\nPerhaps video quality settings are too low?"
|
506
458
|
)
|
507
|
-
except
|
459
|
+
except bv.FileNotFoundError:
|
508
460
|
log.error(f"File not found: {output_path}")
|
509
|
-
except
|
461
|
+
except bv.FFmpegError as e:
|
510
462
|
log.error(e)
|
511
463
|
|
512
464
|
if bar_index:
|
@@ -521,8 +473,6 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
521
473
|
bar.end()
|
522
474
|
|
523
475
|
# Close resources
|
524
|
-
for audio_input in audio_inputs:
|
525
|
-
audio_input.close()
|
526
476
|
for subtitle_input in subtitle_inputs:
|
527
477
|
subtitle_input.close()
|
528
478
|
output.close()
|
@@ -552,11 +502,9 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
|
|
552
502
|
tl.v1.source, [clipify(padded_chunks, tl.v1.source)]
|
553
503
|
)
|
554
504
|
my_timeline = v3(
|
555
|
-
tl.v1.source,
|
556
505
|
tl.tb,
|
557
|
-
tl.sr,
|
558
|
-
tl.res,
|
559
506
|
"#000",
|
507
|
+
tl.template,
|
560
508
|
vspace,
|
561
509
|
aspace,
|
562
510
|
v1(tl.v1.source, padded_chunks),
|
auto_editor/ffwrapper.py
CHANGED
@@ -4,14 +4,14 @@ from dataclasses import dataclass
|
|
4
4
|
from fractions import Fraction
|
5
5
|
from pathlib import Path
|
6
6
|
|
7
|
-
import
|
7
|
+
import bv
|
8
8
|
|
9
9
|
from auto_editor.utils.log import Log
|
10
10
|
|
11
11
|
|
12
12
|
def mux(input: Path, output: Path, stream: int) -> None:
|
13
|
-
input_container =
|
14
|
-
output_container =
|
13
|
+
input_container = bv.open(input, "r")
|
14
|
+
output_container = bv.open(output, "w")
|
15
15
|
|
16
16
|
input_audio_stream = input_container.streams.audio[stream]
|
17
17
|
output_audio_stream = output_container.add_stream("pcm_s16le")
|
@@ -86,89 +86,93 @@ class FileInfo:
|
|
86
86
|
return self.audios[0].samplerate
|
87
87
|
return 48000
|
88
88
|
|
89
|
+
@classmethod
|
90
|
+
def init(self, path: str, log: Log) -> FileInfo:
|
91
|
+
try:
|
92
|
+
cont = bv.open(path, "r")
|
93
|
+
except bv.error.FileNotFoundError:
|
94
|
+
log.error(f"Input file doesn't exist: {path}")
|
95
|
+
except bv.error.IsADirectoryError:
|
96
|
+
log.error(f"Expected a media file, but got a directory: {path}")
|
97
|
+
except bv.error.InvalidDataError:
|
98
|
+
log.error(f"Invalid data when processing: {path}")
|
99
|
+
|
100
|
+
videos: tuple[VideoStream, ...] = ()
|
101
|
+
audios: tuple[AudioStream, ...] = ()
|
102
|
+
subtitles: tuple[SubtitleStream, ...] = ()
|
103
|
+
|
104
|
+
for v in cont.streams.video:
|
105
|
+
if v.duration is not None and v.time_base is not None:
|
106
|
+
vdur = float(v.duration * v.time_base)
|
107
|
+
else:
|
108
|
+
vdur = 0.0
|
109
|
+
|
110
|
+
fps = v.average_rate
|
111
|
+
if (fps is None or fps < 1) and v.name in {"png", "mjpeg", "webp"}:
|
112
|
+
fps = Fraction(25)
|
113
|
+
if fps is None or fps == 0:
|
114
|
+
fps = Fraction(30)
|
115
|
+
|
116
|
+
if v.sample_aspect_ratio is None:
|
117
|
+
sar = Fraction(1)
|
118
|
+
else:
|
119
|
+
sar = v.sample_aspect_ratio
|
120
|
+
|
121
|
+
cc = v.codec_context
|
122
|
+
|
123
|
+
if v.name is None:
|
124
|
+
log.error(f"Can't detect codec for video stream {v}")
|
125
|
+
|
126
|
+
videos += (
|
127
|
+
VideoStream(
|
128
|
+
v.width,
|
129
|
+
v.height,
|
130
|
+
v.codec.canonical_name,
|
131
|
+
fps,
|
132
|
+
vdur,
|
133
|
+
sar,
|
134
|
+
v.time_base,
|
135
|
+
getattr(v.format, "name", None),
|
136
|
+
cc.color_range,
|
137
|
+
cc.colorspace,
|
138
|
+
cc.color_primaries,
|
139
|
+
cc.color_trc,
|
140
|
+
0 if v.bit_rate is None else v.bit_rate,
|
141
|
+
v.language,
|
142
|
+
),
|
143
|
+
)
|
144
|
+
|
145
|
+
for a in cont.streams.audio:
|
146
|
+
adur = 0.0
|
147
|
+
if a.duration is not None and a.time_base is not None:
|
148
|
+
adur = float(a.duration * a.time_base)
|
149
|
+
|
150
|
+
a_cc = a.codec_context
|
151
|
+
audios += (
|
152
|
+
AudioStream(
|
153
|
+
a_cc.codec.canonical_name,
|
154
|
+
0 if a_cc.sample_rate is None else a_cc.sample_rate,
|
155
|
+
a.layout.name,
|
156
|
+
a_cc.channels,
|
157
|
+
adur,
|
158
|
+
0 if a_cc.bit_rate is None else a_cc.bit_rate,
|
159
|
+
a.language,
|
160
|
+
),
|
161
|
+
)
|
162
|
+
|
163
|
+
for s in cont.streams.subtitles:
|
164
|
+
codec = s.codec_context.name
|
165
|
+
sub_exts = {"mov_text": "srt", "ass": "ass", "webvtt": "vtt"}
|
166
|
+
ext = sub_exts.get(codec, "vtt")
|
167
|
+
subtitles += (SubtitleStream(codec, ext, s.language),)
|
168
|
+
|
169
|
+
desc = cont.metadata.get("description", None)
|
170
|
+
bitrate = 0 if cont.bit_rate is None else cont.bit_rate
|
171
|
+
dur = 0 if cont.duration is None else cont.duration / bv.time_base
|
172
|
+
|
173
|
+
cont.close()
|
174
|
+
|
175
|
+
return FileInfo(Path(path), bitrate, dur, desc, videos, audios, subtitles)
|
176
|
+
|
89
177
|
def __repr__(self) -> str:
|
90
178
|
return f"@{self.path.name}"
|
91
|
-
|
92
|
-
|
93
|
-
def initFileInfo(path: str, log: Log) -> FileInfo:
|
94
|
-
try:
|
95
|
-
cont = av.open(path, "r")
|
96
|
-
except av.error.FileNotFoundError:
|
97
|
-
log.error(f"Input file doesn't exist: {path}")
|
98
|
-
except av.error.IsADirectoryError:
|
99
|
-
log.error(f"Expected a media file, but got a directory: {path}")
|
100
|
-
except av.error.InvalidDataError:
|
101
|
-
log.error(f"Invalid data when processing: {path}")
|
102
|
-
|
103
|
-
videos: tuple[VideoStream, ...] = ()
|
104
|
-
audios: tuple[AudioStream, ...] = ()
|
105
|
-
subtitles: tuple[SubtitleStream, ...] = ()
|
106
|
-
|
107
|
-
for v in cont.streams.video:
|
108
|
-
if v.duration is not None and v.time_base is not None:
|
109
|
-
vdur = float(v.duration * v.time_base)
|
110
|
-
else:
|
111
|
-
vdur = 0.0
|
112
|
-
|
113
|
-
fps = v.average_rate
|
114
|
-
if (fps is None or fps < 1) and v.name in {"png", "mjpeg", "webp"}:
|
115
|
-
fps = Fraction(25)
|
116
|
-
if fps is None or fps == 0:
|
117
|
-
fps = Fraction(30)
|
118
|
-
|
119
|
-
sar = Fraction(1) if v.sample_aspect_ratio is None else v.sample_aspect_ratio
|
120
|
-
cc = v.codec_context
|
121
|
-
|
122
|
-
if v.name is None:
|
123
|
-
log.error(f"Can't detect codec for video stream {v}")
|
124
|
-
|
125
|
-
videos += (
|
126
|
-
VideoStream(
|
127
|
-
v.width,
|
128
|
-
v.height,
|
129
|
-
v.name,
|
130
|
-
fps,
|
131
|
-
vdur,
|
132
|
-
sar,
|
133
|
-
v.time_base,
|
134
|
-
getattr(v.format, "name", None),
|
135
|
-
cc.color_range,
|
136
|
-
cc.colorspace,
|
137
|
-
cc.color_primaries,
|
138
|
-
cc.color_trc,
|
139
|
-
0 if v.bit_rate is None else v.bit_rate,
|
140
|
-
v.language,
|
141
|
-
),
|
142
|
-
)
|
143
|
-
|
144
|
-
for a in cont.streams.audio:
|
145
|
-
adur = 0.0
|
146
|
-
if a.duration is not None and a.time_base is not None:
|
147
|
-
adur = float(a.duration * a.time_base)
|
148
|
-
|
149
|
-
a_cc = a.codec_context
|
150
|
-
audios += (
|
151
|
-
AudioStream(
|
152
|
-
a_cc.codec.canonical_name,
|
153
|
-
0 if a_cc.sample_rate is None else a_cc.sample_rate,
|
154
|
-
a.layout.name,
|
155
|
-
a_cc.channels,
|
156
|
-
adur,
|
157
|
-
0 if a_cc.bit_rate is None else a_cc.bit_rate,
|
158
|
-
a.language,
|
159
|
-
),
|
160
|
-
)
|
161
|
-
|
162
|
-
for s in cont.streams.subtitles:
|
163
|
-
codec = s.codec_context.name
|
164
|
-
sub_exts = {"mov_text": "srt", "ass": "ass", "webvtt": "vtt"}
|
165
|
-
ext = sub_exts.get(codec, "vtt")
|
166
|
-
subtitles += (SubtitleStream(codec, ext, s.language),)
|
167
|
-
|
168
|
-
desc = cont.metadata.get("description", None)
|
169
|
-
bitrate = 0 if cont.bit_rate is None else cont.bit_rate
|
170
|
-
dur = 0 if cont.duration is None else cont.duration / av.time_base
|
171
|
-
|
172
|
-
cont.close()
|
173
|
-
|
174
|
-
return FileInfo(Path(path), bitrate, dur, desc, videos, audios, subtitles)
|
auto_editor/formats/fcp11.py
CHANGED
@@ -59,13 +59,6 @@ def fcp11_write_xml(
|
|
59
59
|
return "0s"
|
60
60
|
return f"{val * tl.tb.denominator}/{tl.tb.numerator}s"
|
61
61
|
|
62
|
-
src = tl.src
|
63
|
-
assert src is not None
|
64
|
-
|
65
|
-
proj_name = src.path.stem
|
66
|
-
src_dur = int(src.duration * tl.tb)
|
67
|
-
tl_dur = src_dur if resolve else tl.out_len()
|
68
|
-
|
69
62
|
if version == 11:
|
70
63
|
ver_str = "1.11"
|
71
64
|
elif version == 10:
|
@@ -76,7 +69,16 @@ def fcp11_write_xml(
|
|
76
69
|
fcpxml = Element("fcpxml", version=ver_str)
|
77
70
|
resources = SubElement(fcpxml, "resources")
|
78
71
|
|
72
|
+
src_dur = 0
|
73
|
+
tl_dur = 0 if resolve else tl.out_len()
|
74
|
+
|
79
75
|
for i, one_src in enumerate(tl.unique_sources()):
|
76
|
+
if i == 0:
|
77
|
+
proj_name = one_src.path.stem
|
78
|
+
src_dur = int(one_src.duration * tl.tb)
|
79
|
+
if resolve:
|
80
|
+
tl_dur = src_dur
|
81
|
+
|
80
82
|
SubElement(
|
81
83
|
resources,
|
82
84
|
"format",
|
@@ -113,7 +115,7 @@ def fcp11_write_xml(
|
|
113
115
|
format="r1",
|
114
116
|
tcStart="0s",
|
115
117
|
tcFormat="NDF",
|
116
|
-
audioLayout=
|
118
|
+
audioLayout=tl.T.layout,
|
117
119
|
audioRate="44.1k" if tl.sr == 44100 else "48k",
|
118
120
|
)
|
119
121
|
spine = SubElement(sequence, "spine")
|
auto_editor/formats/fcp7.py
CHANGED
@@ -7,8 +7,8 @@ from math import ceil
|
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
from xml.etree.ElementTree import Element
|
9
9
|
|
10
|
-
from auto_editor.ffwrapper import FileInfo
|
11
|
-
from auto_editor.timeline import ASpace, TlAudio, TlVideo, VSpace, v3
|
10
|
+
from auto_editor.ffwrapper import FileInfo
|
11
|
+
from auto_editor.timeline import ASpace, Template, TlAudio, TlVideo, VSpace, v3
|
12
12
|
|
13
13
|
from .utils import Validator, show
|
14
14
|
|
@@ -282,7 +282,7 @@ def fcp7_read_xml(path: str, log: Log) -> v3:
|
|
282
282
|
fileobj = valid.parse(clipitem["file"], {"pathurl": str})
|
283
283
|
|
284
284
|
if "pathurl" in fileobj:
|
285
|
-
sources[file_id] =
|
285
|
+
sources[file_id] = FileInfo.init(
|
286
286
|
uri_to_path(fileobj["pathurl"]),
|
287
287
|
log,
|
288
288
|
)
|
@@ -317,7 +317,7 @@ def fcp7_read_xml(path: str, log: Log) -> v3:
|
|
317
317
|
file_id = clipitem["file"].attrib["id"]
|
318
318
|
if file_id not in sources:
|
319
319
|
fileobj = valid.parse(clipitem["file"], {"pathurl": str})
|
320
|
-
sources[file_id] =
|
320
|
+
sources[file_id] = FileInfo.init(
|
321
321
|
uri_to_path(fileobj["pathurl"]), log
|
322
322
|
)
|
323
323
|
|
@@ -336,10 +336,8 @@ def fcp7_read_xml(path: str, log: Log) -> v3:
|
|
336
336
|
)
|
337
337
|
)
|
338
338
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
return v3(primary_src, tb, sr, res, "#000", vobjs, aobjs, v1=None)
|
339
|
+
T = Template.init(sources[next(iter(sources))], sr, res=res)
|
340
|
+
return v3(tb, "#000", T, vobjs, aobjs, v1=None)
|
343
341
|
|
344
342
|
|
345
343
|
def media_def(
|
@@ -424,13 +422,14 @@ def resolve_write_audio(audio: Element, make_filedef, tl: v3) -> None:
|
|
424
422
|
clipitem.append(speedup(aclip.speed * 100))
|
425
423
|
|
426
424
|
|
427
|
-
def premiere_write_audio(audio: Element, make_filedef,
|
425
|
+
def premiere_write_audio(audio: Element, make_filedef, tl: v3) -> None:
|
428
426
|
ET.SubElement(audio, "numOutputChannels").text = "2"
|
429
427
|
aformat = ET.SubElement(audio, "format")
|
430
428
|
aschar = ET.SubElement(aformat, "samplecharacteristics")
|
431
429
|
ET.SubElement(aschar, "depth").text = DEPTH
|
432
430
|
ET.SubElement(aschar, "samplerate").text = f"{tl.sr}"
|
433
431
|
|
432
|
+
has_video = tl.v and tl.v[0]
|
434
433
|
t = 0
|
435
434
|
for aclips in tl.a:
|
436
435
|
for channelcount in range(0, 2): # Because "stereo" is hardcoded.
|
@@ -442,7 +441,7 @@ def premiere_write_audio(audio: Element, make_filedef, src: FileInfo, tl: v3) ->
|
|
442
441
|
premiereTrackType="Stereo",
|
443
442
|
)
|
444
443
|
|
445
|
-
if
|
444
|
+
if has_video:
|
446
445
|
ET.SubElement(track, "outputchannelindex").text = f"{channelcount + 1}"
|
447
446
|
|
448
447
|
for j, aclip in enumerate(aclips):
|
@@ -453,7 +452,7 @@ def premiere_write_audio(audio: Element, make_filedef, src: FileInfo, tl: v3) ->
|
|
453
452
|
_in = f"{aclip.offset}"
|
454
453
|
_out = f"{aclip.offset + aclip.dur}"
|
455
454
|
|
456
|
-
if not
|
455
|
+
if not has_video:
|
457
456
|
clip_item_num = j + 1
|
458
457
|
else:
|
459
458
|
clip_item_num = len(aclips) + 1 + j + (t * len(aclips))
|
@@ -579,7 +578,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
|
579
578
|
if resolve:
|
580
579
|
resolve_write_audio(audio, make_filedef, tl)
|
581
580
|
else:
|
582
|
-
premiere_write_audio(audio, make_filedef,
|
581
|
+
premiere_write_audio(audio, make_filedef, tl)
|
583
582
|
|
584
583
|
tree = ET.ElementTree(xmeml)
|
585
584
|
ET.indent(tree, space=" ", level=0)
|