Repository: PyAV-Org/PyAV (language: python)

Issue:
Support `av_find_best_stream`
## Overview

Add support for [`av_find_best_stream`](https://ffmpeg.org/doxygen/trunk/group__lavf__decoding.html#ga757780d38f482deb4d809c6c521fbcc2) to [StreamContainer](https://pyav.org/docs/stable/api/stream.html#av.container.streams.StreamContainer)


## Desired Behavior

Using the C libav APIs I would usually first find the "best" video stream, and then find the "best" related audio stream:
```c
int video_stream_index = av_find_best_stream(format_context, AVMEDIA_TYPE_VIDEO, -1, -1, &video_codec, 0);
int audio_stream_index = av_find_best_stream(format_context, AVMEDIA_TYPE_AUDIO, -1, video_stream_index, &audio_codec, 0);
```

## Example API

[StreamContainer.get](https://pyav.org/docs/stable/api/stream.html#av.container.streams.StreamContainer.get) is pretty overloaded, maybe a new API like `StreamContainer.best(type: str, related: Stream | None) -> Stream` where `type` is [Stream.type](https://pyav.org/docs/stable/api/stream.html#av.stream.Stream.type) ?



Discussion / hints:
makes sense to me

Fix patch:
diff --git a/av/container/streams.pxd b/av/container/streams.pxd
index 43f7f3e4f..097176e10 100644
--- a/av/container/streams.pxd
+++ b/av/container/streams.pxd
@@ -1,8 +1,11 @@
+cimport libav as lib
+
 from av.stream cimport Stream
 
+from .core cimport Container
 
-cdef class StreamContainer:
 
+cdef class StreamContainer:
     cdef list _streams
 
     # For the different types.
@@ -14,3 +17,5 @@ cdef class StreamContainer:
     cdef readonly tuple other
 
     cdef add_stream(self, Stream stream)
+    cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept
+
diff --git a/av/container/streams.pyi b/av/container/streams.pyi
index 9f67e7981..5d8647afe 100644
--- a/av/container/streams.pyi
+++ b/av/container/streams.pyi
@@ -1,4 +1,4 @@
-from typing import Iterator, overload
+from typing import Iterator, Literal, overload
 
 from av.attachments.stream import AttachmentStream
 from av.audio.stream import AudioStream
@@ -30,3 +30,9 @@ class StreamContainer:
         *args: int | Stream | dict[str, int | tuple[int, ...]],
         **kwargs: int | tuple[int, ...],
     ) -> list[Stream]: ...
+    def best(
+        self,
+        type: Literal["video", "audio", "subtitle", "data", "attachment"],
+        /,
+        related: Stream | None = None,
+    ) -> Stream | None: ...
diff --git a/av/container/streams.pyx b/av/container/streams.pyx
index 6071bc811..48830278c 100644
--- a/av/container/streams.pyx
+++ b/av/container/streams.pyx
@@ -1,4 +1,3 @@
-
 cimport libav as lib
 
 
@@ -10,6 +9,19 @@ def _flatten(input_):
         else:
             yield x
 
+cdef lib.AVMediaType _get_media_type_enum(str type):
+    if type == "video":
+        return lib.AVMEDIA_TYPE_VIDEO
+    elif type == "audio":
+        return lib.AVMEDIA_TYPE_AUDIO
+    elif type == "subtitle":
+        return lib.AVMEDIA_TYPE_SUBTITLE
+    elif type == "attachment":
+        return lib.AVMEDIA_TYPE_ATTACHMENT
+    elif type == "data":
+        return lib.AVMEDIA_TYPE_DATA
+    else:
+        raise ValueError(f"Invalid stream type: {type}")
 
 cdef class StreamContainer:
     """
@@ -121,3 +133,36 @@ cdef class StreamContainer:
                 raise TypeError("Argument must be Stream or int.", type(x))
 
         return selection or self._streams[:]
+
+    cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept:
+        cdef int stream_index
+
+        if related is None:
+            stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, -1, NULL, 0)
+        else:
+            stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, related.ptr.index, NULL, 0)
+
+        return stream_index
+
+    def best(self, str type, /, Stream related = None):
+        """best(type: Literal["video", "audio", "subtitle", "attachment", "data"], /, related: Stream | None)
+        Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream`
+
+        :param type: The type of stream to find
+        :param related: A related stream to use as a reference (optional)
+        :return: The best stream of the specified type
+        :rtype: Stream | None
+        """
+        cdef type_enum = _get_media_type_enum(type)
+
+        if len(self._streams) == 0:
+            return None
+
+        cdef container = self._streams[0].container
+
+        cdef int stream_index = self._get_best_stream_index(container, type_enum, related)
+
+        if stream_index < 0:
+            return None
+
+        return self._streams[stream_index]
diff --git a/docs/api/attachments.rst b/docs/api/attachments.rst
new file mode 100644
index 000000000..6e59a202b
--- /dev/null
+++ b/docs/api/attachments.rst
@@ -0,0 +1,8 @@
+
+Attachments
+===========
+
+.. automodule:: av.attachments.stream
+
+    .. autoclass:: AttachmentStream
+        :members:
diff --git a/docs/api/stream.rst b/docs/api/stream.rst
index a6bc1fc8b..99a30b136 100644
--- a/docs/api/stream.rst
+++ b/docs/api/stream.rst
@@ -16,6 +16,8 @@ Dynamic Slicing
 
 .. automethod:: StreamContainer.get
 
+.. automethod:: StreamContainer.best
+
 
 Typed Collections
 ~~~~~~~~~~~~~~~~~
@@ -35,6 +37,10 @@ dynamic capabilities of :meth:`.get`:
 
     A tuple of :class:`SubtitleStream`.
 
+.. attribute:: StreamContainer.attachments
+
+    A tuple of :class:`AttachmentStream`.
+
 .. attribute:: StreamContainer.data
 
     A tuple of :class:`DataStream`.
diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd
index 29a892176..195134970 100644
--- a/include/libavformat/avformat.pxd
+++ b/include/libavformat/avformat.pxd
@@ -28,7 +28,6 @@ cdef extern from "libavformat/avformat.h" nogil:
         AVMEDIA_TYPE_NB
 
     cdef struct AVStream:
-
         int index
         int id
 
@@ -164,6 +163,15 @@ cdef extern from "libavformat/avformat.h" nogil:
         unsigned int max_probe_size
     )
 
+    cdef int av_find_best_stream(
+        AVFormatContext *ic,
+        AVMediaType type,
+        int wanted_stream_nb,
+        int related_stream,
+        AVCodec **decoder_ret,
+        int flags
+    ) 
+
     cdef AVInputFormat* av_find_input_format(const char *name)
 
     # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html


Test patch:
diff --git a/tests/test_streams.py b/tests/test_streams.py
index a4e3eae5e..c146abfb1 100644
--- a/tests/test_streams.py
+++ b/tests/test_streams.py
@@ -14,16 +14,26 @@ def test_stream_tuples(self):
             audio_streams = tuple([s for s in container.streams if s.type == "audio"])
             self.assertEqual(audio_streams, container.streams.audio)
 
-    def test_selection(self):
-        container = av.open(fate_suite("h264/interlaced_crop.mp4"))
+    def test_selection(self) -> None:
+        container = av.open(
+            fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv")
+        )
         video = container.streams.video[0]
-        # audio_stream = container.streams.audio[0]
-        # audio_streams = list(container.streams.audio[0:2])
+        audio = container.streams.audio[0]
 
         self.assertEqual([video], container.streams.get(video=0))
         self.assertEqual([video], container.streams.get(video=(0,)))
 
-        # TODO: Find something in the fate suite with video, audio, and subtitles.
+        self.assertEqual(video, container.streams.best("video"))
+        self.assertEqual(audio, container.streams.best("audio"))
+
+        container = av.open(fate_suite("sub/MovText_capability_tester.mp4"))
+        subtitle = container.streams.subtitles[0]
+        self.assertEqual(subtitle, container.streams.best("subtitle"))
+
+        container = av.open(fate_suite("mxf/track_01_v02.mxf"))
+        data = container.streams.data[0]
+        self.assertEqual(data, container.streams.best("data"))
 
     def test_noside_data(self):
         container = av.open(fate_suite("h264/interlaced_crop.mp4"))