Repository: just-work/fffw

Issue:
Vector should have metadata property
Current stream metadata may be used to compute some parameters for next filters, but Vector instance does not have such property.

Vector.metadata should return Meta instance if there is single stream in this vector and throw an error if vector length is more than one.

Discussion:

Patch:
diff --git a/fffw/encoding/vector.py b/fffw/encoding/vector.py
index 75ba0be..337da32 100644
--- a/fffw/encoding/vector.py
+++ b/fffw/encoding/vector.py
@@ -1,6 +1,6 @@
 from typing import *
 
-from fffw.graph.meta import AUDIO, VIDEO, StreamType
+from fffw.graph.meta import AUDIO, VIDEO, StreamType, Meta
 from fffw.encoding import filters, inputs, outputs, FFMPEG
 
 Group = Dict[int, Set[int]]
@@ -75,6 +75,7 @@ def prepare_dst_clones(destinations: Dict[int, Outgoing],
         dst = destinations[dst_id]
         # Clone destination filter for each source that will be connected
         # to it.
+        # noinspection PyTypeChecker
         clones = cast(List[Outgoing], dst.clone(len(src_set)))
         for src_id, c in zip(src_set, clones):
             dst_clones[dst_id, src_id] = c
@@ -123,10 +124,12 @@ def map_sources_to_destinations(
 
         if key not in links:
             # connect same src to same dst only once
+            # noinspection PyTypeChecker
             links[key] = cast(Outgoing, split.connect_dest(clone))
 
         # add destination node to results
         results.append(links[key])
+    # noinspection PyTypeChecker
     return Vector(cast(Union[List[filters.Filter], List[outputs.Codec]],
                        results))
 
@@ -146,6 +149,7 @@ def init_filter_vector(filter_class: Type[filters.Filter],
         each vector is instance of filter_class.
     """
     vector = []
+    # noinspection PyTypeChecker
     factory = cast(Callable[..., filters.Filter], filter_class)
     seen_filters: Dict[str, filters.Filter] = dict()
     for param in params:
@@ -248,10 +252,22 @@ class Vector(tuple):
     @property
     def kind(self) -> StreamType:
         """
-        :returns a kind of streams in vector.
+        :returns: a kind of streams in vector.
         """
+        kinds = {s.kind for s in self}
+        if len(kinds) != 1:
+            raise RuntimeError("multiple kind of streams in vector")
         return self[0].kind
 
+    @property
+    def metadata(self) -> Meta:
+        """
+        :returns: metadata for a stream in vector.
+        """
+        if len(self) != 1:
+            raise RuntimeError("not a scalar")
+        return self[0].metadata
+
     @overload
     def connect(self, dst: filters.Filter, mask: Optional[List[bool]] = None
                 ) -> "Vector":
@@ -410,6 +426,7 @@ class SIMD:
         elif isinstance(other, inputs.Input):
             self.add_input(other)
         else:
+            # noinspection PyTypeChecker
             return NotImplemented
 
     def __or__(self, other: filters.Filter) -> Vector:


Test patch:
diff --git a/tests/test_vector.py b/tests/test_vector.py
index 157eab0..f3da108 100644
--- a/tests/test_vector.py
+++ b/tests/test_vector.py
@@ -1,10 +1,10 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, replace
 from typing import cast, Tuple
 
 from fffw.encoding import *
 from fffw.encoding.vector import SIMD, Vector
 from fffw.graph import *
-from fffw.wrapper import ensure_binary, param
+from fffw.wrapper import param
 from tests.base import BaseTestCase
 from tests.test_ffmpeg import Volume
 
@@ -28,7 +28,7 @@ class AnotherFilter(VideoFilter):
 class VectorTestCase(BaseTestCase):
     def setUp(self) -> None:
         super().setUp()
-        self.video_meta = video_meta_data()
+        self.video_meta = video_meta_data(width=1920, height=1080)
         self.audio_meta = audio_meta_data()
         self.source = input_file('input.mp4',
                                  Stream(VIDEO, self.video_meta),
@@ -48,6 +48,30 @@ class VectorTestCase(BaseTestCase):
     def ffmpeg(self):
         return self.simd.ffmpeg
 
+    def test_vector_kind(self):
+        """
+        Checks that vector does not return kind if it contains audio and video
+        streams.
+        """
+        v = Vector([VideoFilter(), AudioFilter()])
+        self.assertRaises(RuntimeError, getattr, v, 'kind')
+
+    def test_vector_metadata(self):
+        """
+        Checks that vector outputs metadata for a single stream in it.
+        """
+        v = self.simd.video | Scale(1280, 720)
+        expected = replace(self.video_meta, width=1280, height=720)
+        self.assertEqual(v.metadata, expected)
+
+    def test_vector_metadata_for_multiple_streams(self):
+        """
+        Checks that vector does not return metadata if it contains multiple
+        streams.
+        """
+        v = Vector([VideoFilter(), VideoFilter()])
+        self.assertRaises(RuntimeError, getattr, v, 'metadata')
+
     def test_no_filter_graph(self):
         """ Checks that vector works correctly without filter graph."""
         self.assert_simd_args(
@@ -177,6 +201,7 @@ class VectorTestCase(BaseTestCase):
         all connected inputs.
         """
         v = self.simd.video | Vector((SomeFilter(), AnotherFilter()))
+        # noinspection PyTypeChecker
         some, other = cast(Tuple[VideoFilter, VideoFilter], v)
         v1 = Vector(some).connect(Scale, params=[(1280, 720), (640, 360)])