Skip to content

Commit 2fa6148

Browse files
committed
Check for experimental codecs
1 parent 9fd986e commit 2fa6148

File tree

5 files changed

+85
-53
lines changed

5 files changed

+85
-53
lines changed

auto_editor/analyze.py

+62-49
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
from auto_editor.utils.log import Log
2828

2929

30+
__all__ = ("LevelError", "Levels", "iter_audio", "iter_motion")
31+
32+
3033
class LevelError(Exception):
3134
pass
3235

@@ -69,45 +72,39 @@ def mut_remove_large(
6972
active = False
7073

7174

72-
def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[np.float32]:
75+
def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float32]:
7376
fifo = AudioFifo()
74-
try:
75-
container = av.open(src.path, "r")
76-
audio_stream = container.streams.audio[stream]
77-
sample_rate = audio_stream.rate
77+
sr = audio_stream.rate
7878

79-
exact_size = (1 / tb) * sample_rate
80-
accumulated_error = 0
79+
exact_size = (1 / tb) * sr
80+
accumulated_error = Fraction(0)
8181

82-
# Resample so that audio data is between [-1, 1]
83-
resampler = av.AudioResampler(
84-
av.AudioFormat("flt"), audio_stream.layout, sample_rate
85-
)
82+
# Resample so that audio data is between [-1, 1]
83+
resampler = av.AudioResampler(av.AudioFormat("flt"), audio_stream.layout, sr)
8684

87-
for frame in container.decode(audio=stream):
88-
frame.pts = None # Skip time checks
85+
container = audio_stream.container
86+
assert isinstance(container, av.container.InputContainer)
8987

90-
for reframe in resampler.resample(frame):
91-
fifo.write(reframe)
88+
for frame in container.decode(audio_stream):
89+
frame.pts = None # Skip time checks
9290

93-
while fifo.samples >= ceil(exact_size):
94-
size_with_error = exact_size + accumulated_error
95-
current_size = round(size_with_error)
96-
accumulated_error = size_with_error - current_size
91+
for reframe in resampler.resample(frame):
92+
fifo.write(reframe)
9793

98-
audio_chunk = fifo.read(current_size)
99-
assert audio_chunk is not None
100-
arr = audio_chunk.to_ndarray().flatten()
101-
yield np.max(np.abs(arr))
102-
103-
finally:
104-
container.close()
94+
while fifo.samples >= ceil(exact_size):
95+
size_with_error = exact_size + accumulated_error
96+
current_size = round(size_with_error)
97+
accumulated_error = size_with_error - current_size
10598

99+
audio_chunk = fifo.read(current_size)
100+
assert audio_chunk is not None
101+
arr = audio_chunk.to_ndarray().flatten()
102+
yield np.max(np.abs(arr))
106103

107-
def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.float32]:
108-
container = av.open(src.path, "r")
109104

110-
video = container.streams.video[stream]
105+
def iter_motion(
106+
video: av.VideoStream, tb: Fraction, blur: int, width: int
107+
) -> Iterator[np.float32]:
111108
video.thread_type = "AUTO"
112109

113110
prev_frame = None
@@ -125,6 +122,9 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
125122
graph.add("buffersink"),
126123
).configure()
127124

125+
container = video.container
126+
assert isinstance(container, av.container.InputContainer)
127+
128128
for unframe in container.decode(video):
129129
if unframe.pts is None:
130130
continue
@@ -151,8 +151,6 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
151151
prev_frame = current_frame
152152
prev_index = index
153153

154-
container.close()
155-
156154

157155
def obj_tag(path: Path, kind: str, tb: Fraction, obj: Sequence[object]) -> str:
158156
mod_time = int(path.stat().st_mtime)
@@ -175,7 +173,11 @@ def media_length(self) -> int:
175173
if (arr := self.read_cache("audio", (0,))) is not None:
176174
return len(arr)
177175

178-
result = sum(1 for _ in iter_audio(self.src, self.tb, 0))
176+
with av.open(self.src.path, "r") as container:
177+
audio_stream = container.streams.audio[0]
178+
self.log.experimental(audio_stream.codec)
179+
result = sum(1 for _ in iter_audio(audio_stream, self.tb))
180+
179181
self.log.debug(f"Audio Length: {result}")
180182
return result
181183

@@ -239,21 +241,26 @@ def audio(self, stream: int) -> NDArray[np.float32]:
239241
if (arr := self.read_cache("audio", (stream,))) is not None:
240242
return arr
241243

242-
with av.open(self.src.path, "r") as container:
243-
audio = container.streams.audio[stream]
244-
if audio.duration is not None and audio.time_base is not None:
245-
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
246-
elif container.duration is not None:
247-
inaccurate_dur = int(container.duration / av.time_base * self.tb)
248-
else:
249-
inaccurate_dur = 1024
244+
container = av.open(self.src.path, "r")
245+
audio = container.streams.audio[stream]
246+
247+
if audio.codec.experimental:
248+
self.log.error(f"`{audio.codec.name}` is an experimental codec")
249+
250+
if audio.duration is not None and audio.time_base is not None:
251+
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
252+
elif container.duration is not None:
253+
inaccurate_dur = int(container.duration / av.time_base * self.tb)
254+
else:
255+
inaccurate_dur = 1024
250256

251257
bar = self.bar
252258
bar.start(inaccurate_dur, "Analyzing audio volume")
253259

254260
result = np.zeros((inaccurate_dur), dtype=np.float32)
255261
index = 0
256-
for value in iter_audio(self.src, self.tb, stream):
262+
263+
for value in iter_audio(audio, self.tb):
257264
if index > len(result) - 1:
258265
result = np.concatenate(
259266
(result, np.zeros((len(result)), dtype=np.float32))
@@ -263,6 +270,7 @@ def audio(self, stream: int) -> NDArray[np.float32]:
263270
index += 1
264271

265272
bar.end()
273+
assert len(result) > 0
266274
return self.cache(result[:index], "audio", (stream,))
267275

268276
def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
@@ -273,20 +281,25 @@ def motion(self, stream: int, blur: int, width: int) -> NDArray[np.float32]:
273281
if (arr := self.read_cache("motion", mobj)) is not None:
274282
return arr
275283

276-
with av.open(self.src.path, "r") as container:
277-
video = container.streams.video[stream]
278-
inaccurate_dur = (
279-
1024
280-
if video.duration is None or video.time_base is None
281-
else int(video.duration * video.time_base * self.tb)
282-
)
284+
container = av.open(self.src.path, "r")
285+
video = container.streams.video[stream]
286+
287+
if video.codec.experimental:
288+
self.log.experimental(video.codec)
289+
290+
inaccurate_dur = (
291+
1024
292+
if video.duration is None or video.time_base is None
293+
else int(video.duration * video.time_base * self.tb)
294+
)
283295

284296
bar = self.bar
285297
bar.start(inaccurate_dur, "Analyzing motion")
286298

287299
result = np.zeros((inaccurate_dur), dtype=np.float32)
288300
index = 0
289-
for value in iter_motion(self.src, self.tb, stream, blur, width):
301+
302+
for value in iter_motion(video, self.tb, blur, width):
290303
if index > len(result) - 1:
291304
result = np.concatenate(
292305
(result, np.zeros((len(result)), dtype=np.float32))

auto_editor/make_layers.py

+2
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ def make_timeline(
169169
has_loud = concat((has_loud, result))
170170
src_index = concat((src_index, np.full(len(result), i, dtype=np.int32)))
171171

172+
assert len(has_loud) > 0
173+
172174
# Setup for handling custom speeds
173175
speed_index = has_loud.astype(np.uint)
174176
speed_map = [args.silent_speed, args.video_speed]

auto_editor/subcommands/levels.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from fractions import Fraction
66
from typing import TYPE_CHECKING
77

8+
import av
89
import numpy as np
910

10-
from auto_editor.analyze import LevelError, Levels, iter_audio, iter_motion
11+
from auto_editor.analyze import *
1112
from auto_editor.ffwrapper import initFileInfo
1213
from auto_editor.lang.palet import env
1314
from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
@@ -130,9 +131,19 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
130131
levels = Levels(src, tb, bar, False, log, strict=True)
131132
try:
132133
if method == "audio":
133-
print_arr_gen(iter_audio(src, tb, **obj))
134+
container = av.open(src.path, "r")
135+
audio_stream = container.streams.audio[obj["stream"]]
136+
log.experimental(audio_stream.codec)
137+
print_arr_gen(iter_audio(audio_stream, tb))
138+
container.close()
139+
134140
elif method == "motion":
135-
print_arr_gen(iter_motion(src, tb, **obj))
141+
container = av.open(src.path, "r")
142+
video_stream = container.streams.video[obj["stream"]]
143+
log.experimental(video_stream.codec)
144+
print_arr_gen(iter_motion(video_stream, tb, obj["blur"], obj["width"]))
145+
container.close()
146+
136147
elif method == "subtitle":
137148
print_arr(levels.subtitle(**obj))
138149
elif method == "none":

auto_editor/utils/log.py

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from time import perf_counter, sleep
88
from typing import NoReturn
99

10+
import av
11+
1012

1113
class Log:
1214
__slots__ = ("is_debug", "quiet", "machine", "no_color", "_temp", "_ut", "_s")
@@ -97,6 +99,10 @@ def stop_timer(self) -> None:
9799

98100
sys.stdout.write(f"Finished. took {second_len} seconds ({minute_len})\n")
99101

102+
def experimental(self, codec: av.Codec) -> None:
103+
if codec.experimental:
104+
self.error(f"`{codec.name}` is an experimental codec")
105+
100106
def error(self, message: str | Exception) -> NoReturn:
101107
if self.is_debug and isinstance(message, Exception):
102108
self.cleanup()

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ authors = [{ name = "WyattBlue", email = "wyattblue@auto-editor.com" }]
1010
requires-python = ">=3.10,<3.14"
1111
dependencies = [
1212
"numpy>=1.24,<3.0",
13-
"pyav==14.0.0rc4",
13+
"pyav==14.*",
1414
]
1515
keywords = [
1616
"video", "audio", "media", "editor", "editing",

0 commit comments

Comments
 (0)