Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Features:

- Add ``options`` parameter to ``AudioResampler`` for passing ``libswresample`` options (e.g. ``resampler``, ``filter_size``, ``cutoff``) by :gh-user:`WyattBlue` (:issue:`2262`).
- Support ``yuv420p10le`` in ``VideoFrame.to_ndarray`` and ``VideoFrame.from_ndarray`` by :gh-user:`WyattBlue` (:issue:`1981`).
- Add ``at`` parameter to ``Graph.push`` and ``Graph.vpush`` to push a frame to a single buffer source by index, for multi-input filters like ``overlay`` by :gh-user:`WyattBlue`.

Fixes:

Expand Down
2 changes: 1 addition & 1 deletion av/container/core.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ cdef class Container:
cdef HWAccel hwaccel

cdef readonly StreamContainer streams
cdef readonly dict metadata
cdef dict _metadata

# Private API.
cdef uint8_t _myflag # enum: writeable, input_was_opened, started, done, extradata_planned
Expand Down
8 changes: 8 additions & 0 deletions av/container/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,14 @@ def flags(self, value: cython.int):
def input_was_opened(self):
return self._myflag & 2

@property
def metadata(self) -> dict:
# Lazily created so output containers that never touch metadata don't
# allocate a dict. Input containers populate ``_metadata`` eagerly.
if self._metadata is None:
self._metadata = {}
return self._metadata

def chapters(self):
self._assert_open()
result: list = []
Expand Down
2 changes: 1 addition & 1 deletion av/container/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def __cinit__(self, *args, **kwargs):
"Hardware accelerated decode requested but no stream is compatible"
)

self.metadata = avdict_to_dict(
self._metadata = avdict_to_dict(
self.ptr.metadata, self.metadata_encoding, self.metadata_errors
)

Expand Down
1 change: 0 additions & 1 deletion av/container/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def close_output(self: OutputContainer):
class OutputContainer(Container):
def __cinit__(self, *args, **kwargs):
self.streams = StreamContainer()
self.metadata = {}
self._extradata_bsfs = {}
self._buffered_packets = []
with cython.nogil:
Expand Down
23 changes: 20 additions & 3 deletions av/filter/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def set_audio_frame_size(self, frame_size):
cython.cast(FilterContext, sink).ptr, frame_size
)

def push(self, frame):
def push(self, frame, at: cython.int = -1):
if frame is None:
contexts = self._get_context_by_type("buffer") + self._get_context_by_type(
"abuffer"
Expand All @@ -246,12 +246,29 @@ def push(self, frame):
f"can only AudioFrame, VideoFrame or None; got {type(frame)}"
)

if at >= 0:
if at >= len(contexts):
raise IndexError(
f"buffer source index {at} out of range; found {len(contexts)}"
)
contexts[at].push(frame)
return

for ctx in contexts:
ctx.push(frame)

def vpush(self, frame: VideoFrame | None):
def vpush(self, frame: VideoFrame | None, at: cython.int = -1):
"""Like `push`, but only for VideoFrames."""
for ctx in self._get_context_by_type("buffer"):
contexts = self._get_context_by_type("buffer")
if at >= 0:
if at >= len(contexts):
raise IndexError(
f"buffer source index {at} out of range; found {len(contexts)}"
)
contexts[at].push(frame)
return

for ctx in contexts:
ctx.push(frame)

# TODO: Test complex filter graphs, add `at: int = 0` arg to pull() and vpull().
Expand Down
4 changes: 2 additions & 2 deletions av/filter/graph.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class Graph:
time_base: Fraction | None = None,
) -> FilterContext: ...
def set_audio_frame_size(self, frame_size: int) -> None: ...
def push(self, frame: None | AudioFrame | VideoFrame) -> None: ...
def push(self, frame: None | AudioFrame | VideoFrame, at: int = -1) -> None: ...
def pull(self) -> VideoFrame | AudioFrame: ...
def vpush(self, frame: VideoFrame | None) -> None: ...
def vpush(self, frame: VideoFrame | None, at: int = -1) -> None: ...
def vpull(self) -> VideoFrame: ...
2 changes: 1 addition & 1 deletion scripts/build-deps
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ echo ./configure
--disable-bsfs \
--enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \
--disable-filters \
--enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,loudnorm,lutrgb,palettegen,scale,testsrc,vflip,volume \
--enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,loudnorm,lutrgb,overlay,palettegen,scale,testsrc,vflip,volume \
--enable-sse \
--enable-avx \
--enable-avx2 \
Expand Down
44 changes: 44 additions & 0 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,50 @@ def test_EOF(self) -> None:
assert palette_frame.width == 16
assert palette_frame.height == 16

def test_push_at_index(self) -> None:
# overlay has two video buffer sources; `at` targets a single one,
# instead of broadcasting the same frame to both (like auto-editor's
# pushIdx/flushIdx).
width, height = 16, 16

base = VideoFrame(width, height, "yuv420p")
for plane in base.planes:
plane.update(bytes(plane.buffer_size))
base.pts = 0
base.time_base = Fraction(1, 30)

top = VideoFrame(width, height, "yuv420p")
for i, plane in enumerate(top.planes):
plane.update(bytes([200 if i == 0 else 128]) * plane.buffer_size)
top.pts = 0
top.time_base = Fraction(1, 30)

graph = Graph()
b0 = graph.add_buffer(
width=width, height=height, format=base.format, time_base=base.time_base
)
b1 = graph.add_buffer(
width=width, height=height, format=top.format, time_base=top.time_base
)
overlay = graph.add("overlay", "x=0:y=0")
sink = graph.add("buffersink")
b0.link_to(overlay, 0, 0)
b1.link_to(overlay, 0, 1)
overlay.link_to(sink)
graph.configure()

graph.push(base, at=0)
graph.push(top, at=1)
graph.push(None, at=0)
graph.push(None, at=1)

out = graph.vpull()
assert isinstance(out, av.VideoFrame)
assert (out.width, out.height) == (width, height)

with self.assertRaises(IndexError):
graph.push(base, at=2)

def test_graph_threads(self) -> None:
graph = Graph()
assert graph.threads == 0
Expand Down
Loading