Skip to content
Open
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
4 changes: 1 addition & 3 deletions src/ffmpeg/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from typing import Any

from .dag.nodes import FilterableStream, FilterNode, InputNode, MergeOutputsNode, OutputNode, OutputStream
from .streams.audio import AudioStream
from .streams.av import AVStream
from .streams.video import VideoStream
from .streams import AudioStream, AVStream, VideoStream


def input(filename: str, **kwargs: Any) -> AVStream:
Expand Down
51 changes: 14 additions & 37 deletions src/ffmpeg/dag/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os.path
import shlex
import subprocess
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

Expand All @@ -14,9 +15,7 @@
from .schema import Node, Stream

if TYPE_CHECKING:
from ..streams.audio import AudioStream
from ..streams.av import AVStream
from ..streams.video import VideoStream
from ..streams import AudioStream, AVStream, VideoStream
from .context import DAGContext


Expand Down Expand Up @@ -48,7 +47,7 @@ def video(self, index: int) -> "VideoStream":
Returns:
the video stream at the specified index
"""
from ..streams.video import VideoStream
from ..streams import VideoStream

video_outputs = [i for i, k in enumerate(self.output_typings) if k == StreamType.video]
if not len(video_outputs) > index:
Expand All @@ -65,7 +64,7 @@ def audio(self, index: int) -> "AudioStream":
Returns:
the audio stream at the specified index
"""
from ..streams.audio import AudioStream
from ..streams import AudioStream

audio_outputs = [i for i, k in enumerate(self.output_typings) if k == StreamType.audio]
if not len(audio_outputs) > index:
Expand All @@ -74,8 +73,8 @@ def audio(self, index: int) -> "AudioStream":
return AudioStream(node=self, index=audio_outputs[index])

def __post_init__(self) -> None:
from ..streams.audio import AudioStream
from ..streams.video import VideoStream
from ..streams.audio import AudioFilter
from ..streams.video import VideoFilter

super().__post_init__()

Expand All @@ -87,10 +86,10 @@ def __post_init__(self) -> None:

for i, (stream, expected_type) in enumerate(zip(self.inputs, self.input_typings)):
if expected_type == StreamType.video:
if not isinstance(stream, VideoStream):
if not isinstance(stream, VideoFilter):
raise ValueError(f"Expected input {i} to have video component, got {stream.__class__.__name__}")
if expected_type == StreamType.audio:
if not isinstance(stream, AudioStream):
if not isinstance(stream, AudioFilter):
raise ValueError(f"Expected input {i} to have audio component, got {stream.__class__.__name__}")

@override
Expand Down Expand Up @@ -124,13 +123,11 @@ def get_args(self, context: DAGContext = None) -> list[str]:


@dataclass(frozen=True, kw_only=True)
class FilterableStream(Stream):
class FilterableStream(Stream, ABC):
"""
A stream that can be used as input to a filter
"""

node: "FilterNode | InputNode"

def vfilter(
self,
*streams: "FilterableStream",
Expand Down Expand Up @@ -216,6 +213,7 @@ def output(self, *streams: "FilterableStream", filename: str, **kwargs: Any) ->
"""
return OutputNode(kwargs=tuple(kwargs.items()), inputs=(self, *streams), filename=filename).stream()

@abstractmethod
def label(self, context: DAGContext = None) -> str:
"""
Return the label for this stream
Expand All @@ -226,28 +224,7 @@ def label(self, context: DAGContext = None) -> str:
Returns:
the label for this stream
"""
from ..streams.audio import AudioStream
from ..streams.av import AVStream
from ..streams.video import VideoStream
from .context import DAGContext

if not context:
context = DAGContext.build(self.node)

if isinstance(self.node, InputNode):
if isinstance(self, AVStream):
return f"{context.get_node_label(self.node)}"
elif isinstance(self, VideoStream):
return f"{context.get_node_label(self.node)}:v"
elif isinstance(self, AudioStream):
return f"{context.get_node_label(self.node)}:a"
raise ValueError(f"Unknown stream type: {self.__class__.__name__}") # pragma: no cover

if isinstance(self.node, FilterNode):
if len(self.node.output_typings) > 1:
return f"{context.get_node_label(self.node)}#{self.index}"
return f"{context.get_node_label(self.node)}"
raise ValueError(f"Unknown node type: {self.node.__class__.__name__}") # pragma: no cover
raise NotImplementedError()

def __post_init__(self) -> None:
if isinstance(self.node, InputNode):
Expand Down Expand Up @@ -316,7 +293,7 @@ def video(self) -> "VideoStream":
Returns:
the video stream
"""
from ..streams.video import VideoStream
from ..streams import VideoStream

return VideoStream(node=self)

Expand All @@ -328,7 +305,7 @@ def audio(self) -> "AudioStream":
Returns:
the audio stream
"""
from ..streams.audio import AudioStream
from ..streams import AudioStream

return AudioStream(node=self)

Expand All @@ -339,7 +316,7 @@ def stream(self) -> "AVStream":
Returns:
the output stream
"""
from ..streams.av import AVStream
from ..streams import AVStream

return AVStream(node=self)

Expand Down
13 changes: 2 additions & 11 deletions src/ffmpeg/dag/tests/__snapshots__/test_nodes.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,10 @@
])
# ---
# name: test_filter_node_with_inputs.1
<ExceptionInfo ValueError('Expected input 0 to have audio component, got VideoStream') tblen=3>
<ExceptionInfo ValueError('Expected input 0 to have audio component, got VideoIStream') tblen=3>
# ---
# name: test_filter_node_with_inputs.2
<ExceptionInfo ValueError('Expected input 0 to have video component, got AudioStream') tblen=3>
# ---
# name: test_filterable_stream
'1'
# ---
# name: test_filterable_stream.1
'0'
# ---
# name: test_filterable_stream.2
's0#0'
<ExceptionInfo ValueError('Expected input 0 to have video component, got AudioIStream') tblen=3>
# ---
# name: test_node_prop[filter-node][__repr__]
"FilterNode(args=(), kwargs=(('w', '1920'), ('h', '1080'), ('true', True), ('false', False)), inputs=(), name='scale', input_typings=(), output_typings=())"
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"FilterNode(args=(), kwargs=(('w', '1920'), ('h', '1080')), inputs=(VideoStream(node=InputNode(args=(), kwargs=(), inputs=(), filename='test.mp4'), index=None), AudioStream(node=InputNode(args=(), kwargs=(), inputs=(), filename='test.mp4'), index=None)), name='scale', input_typings=(<StreamType.video: 'video'>, <StreamType.audio: 'audio'>), output_typings=())"
"FilterNode(args=(), kwargs=(('w', '1920'), ('h', '1080')), inputs=(VideoIStream(node=InputNode(args=(), kwargs=(), inputs=(), filename='test.mp4'), index=None), AudioIStream(node=InputNode(args=(), kwargs=(), inputs=(), filename='test.mp4'), index=None)), name='scale', input_typings=(<StreamType.video: 'video'>, <StreamType.audio: 'audio'>), output_typings=())"

This file was deleted.

This file was deleted.

4 changes: 2 additions & 2 deletions src/ffmpeg/dag/tests/__snapshots__/test_serialize.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
'{"__class__": "ffmpeg.dag.tests.test_serialize.Person", "name": "John Doe", "age": 30, "address": {"__class__": "ffmpeg.dag.tests.test_serialize.Address", "street": "123 Main St", "city": "Anytown"}}'
# ---
# name: test_load_and_dump_mixed_type[serialized]
'[{"__class__": "ffmpeg.streams.av.AVStream", "node": {"__class__": "ffmpeg.dag.nodes.InputNode", "args": [], "kwargs": [], "inputs": [], "filename": "input.mp4"}, "index": null}, true]'
'[{"__class__": "ffmpeg.streams.input.AVStream", "node": {"__class__": "ffmpeg.dag.nodes.InputNode", "args": [], "kwargs": [], "inputs": [], "filename": "input.mp4"}, "index": null}, true]'
# ---
# name: test_load_and_dump_on_complex_filter[serialized]
'{"__class__": "ffmpeg.dag.nodes.OutputStream", "node": {"__class__": "ffmpeg.dag.nodes.OutputNode", "args": [], "kwargs": [], "inputs": [{"__class__": "ffmpeg.streams.video.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [["x", "50"], ["y", "50"], ["width", "120"], ["height", "120"], ["color", "red"], ["thickness", "5"]], "inputs": [{"__class__": "ffmpeg.streams.video.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [], "inputs": [{"__class__": "ffmpeg.streams.video.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [], "inputs": [{"__class__": "ffmpeg.streams.video.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [["start_frame", 10], ["end_frame", 20]], "inputs": [{"__class__": "ffmpeg.streams.av.AVStream", "node": {"__class__": "ffmpeg.dag.nodes.InputNode", "args": [], "kwargs": [], "inputs": [], "filename": "input.mp4"}, "index": null}], "name": "trim", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}, {"__class__": "ffmpeg.streams.video.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [["start_frame", 30], ["end_frame", 40]], "inputs": [{"__class__": "ffmpeg.streams.av.AVStream", "node": {"__class__": "ffmpeg.dag.nodes.InputNode", "args": [], "kwargs": [], "inputs": [], "filename": "input.mp4"}, "index": null}], "name": "trim", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}], "name": "concat", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}, {"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}, {"__class__": "ffmpeg.streams.video.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [], "inputs": [{"__class__": "ffmpeg.streams.av.AVStream", "node": {"__class__": "ffmpeg.dag.nodes.InputNode", "args": [], "kwargs": [], "inputs": [], "filename": "overlay.png"}, "index": null}], "name": "hflip", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}], "name": "overlay", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}, {"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}], "name": "drawbox", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}], "filename": "out.mp4"}, "index": null}'
'{"__class__": "ffmpeg.dag.nodes.OutputStream", "node": {"__class__": "ffmpeg.dag.nodes.OutputNode", "args": [], "kwargs": [], "inputs": [{"__class__": "ffmpeg.streams.filter.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [["x", "50"], ["y", "50"], ["width", "120"], ["height", "120"], ["color", "red"], ["thickness", "5"]], "inputs": [{"__class__": "ffmpeg.streams.filter.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [], "inputs": [{"__class__": "ffmpeg.streams.filter.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [], "inputs": [{"__class__": "ffmpeg.streams.filter.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [["start_frame", 10], ["end_frame", 20]], "inputs": [{"__class__": "ffmpeg.streams.input.AVStream", "node": {"__class__": "ffmpeg.dag.nodes.InputNode", "args": [], "kwargs": [], "inputs": [], "filename": "input.mp4"}, "index": null}], "name": "trim", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}, {"__class__": "ffmpeg.streams.filter.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [["start_frame", 30], ["end_frame", 40]], "inputs": [{"__class__": "ffmpeg.streams.input.AVStream", "node": {"__class__": "ffmpeg.dag.nodes.InputNode", "args": [], "kwargs": [], "inputs": [], "filename": "input.mp4"}, "index": null}], "name": "trim", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}], "name": "concat", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}, {"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}, {"__class__": "ffmpeg.streams.filter.VideoStream", "node": {"__class__": "ffmpeg.dag.nodes.FilterNode", "args": [], "kwargs": [], "inputs": [{"__class__": "ffmpeg.streams.input.AVStream", "node": {"__class__": "ffmpeg.dag.nodes.InputNode", "args": [], "kwargs": [], "inputs": [], "filename": "overlay.png"}, "index": null}], "name": "hflip", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}], "name": "overlay", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}, {"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}], "name": "drawbox", "input_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}], "output_typings": [{"__class__": "ffmpeg.schema.StreamType", "value": "video"}]}, "index": 0}], "filename": "out.mp4"}, "index": null}'
# ---
Loading