diff --git a/Makefile b/Makefile index 135d091ad..4858add5b 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U ruff isort pillow numpy mypy==1.16.1 pytest + $(PIP) install -U ruff isort pillow numpy mypy==1.17.1 pytest ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests diff --git a/av/container/core.pyi b/av/container/core.pyi index d61d07110..8cd2a9dc5 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -2,7 +2,7 @@ from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, ClassVar, Literal, Type, cast, overload +from typing import Any, Callable, ClassVar, Literal, Type, TypedDict, cast, overload from av.codec.hwaccel import HWAccel from av.format import ContainerFormat @@ -67,6 +67,13 @@ class AudioCodec(IntEnum): pcm_u8 = cast(int, ...) pcm_vidc = cast(int, ...) +class _Chapter(TypedDict): + id: int + start: int + end: int + time_base: Fraction | None + metadata: dict[str, str] + class Container: writeable: bool name: str @@ -86,7 +93,6 @@ class Container: open_timeout: Real | None read_timeout: Real | None flags: int - def __enter__(self) -> Container: ... def __exit__( self, @@ -96,6 +102,7 @@ class Container: ) -> bool: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... + def chapters(self) -> list[_Chapter]: ... @overload def open( diff --git a/av/container/core.pyx b/av/container/core.pyx index d8950c76f..de7a07dda 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -15,7 +15,7 @@ from av.container.output cimport OutputContainer from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil from av.error cimport err_check, stash_exception from av.format cimport build_container_format -from av.utils cimport avdict_to_dict +from av.utils cimport avdict_to_dict, avrational_to_fraction from av.dictionary import Dictionary from av.logging import Capture as LogCapture @@ -330,6 +330,22 @@ cdef class Container: self._assert_open() self.ptr.flags = value + def chapters(self): + self._assert_open() + cdef list result = [] + cdef int i + + for i in range(self.ptr.nb_chapters): + ch = self.ptr.chapters[i] + result.append({ + "id": ch.id, + "start": ch.start, + "end": ch.end, + "time_base": avrational_to_fraction(&ch.time_base), + "metadata": avdict_to_dict(ch.metadata, self.metadata_encoding, self.metadata_errors), + }) + return result + def open( file, mode=None, diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 09eede855..3816b46fa 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -44,6 +44,14 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational r_frame_rate AVRational sample_aspect_ratio + cdef struct AVChapter: + int id + int64_t start + int64_t end + AVRational time_base + AVDictionary *metadata + + # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer @@ -173,6 +181,9 @@ cdef extern from "libavformat/avformat.h" nogil: unsigned int nb_streams AVStream **streams + unsigned int nb_chapters + AVChapter **chapters + AVInputFormat *iformat AVOutputFormat *oformat diff --git a/tests/test_chapters.py b/tests/test_chapters.py new file mode 100644 index 000000000..6a3d371b2 --- /dev/null +++ b/tests/test_chapters.py @@ -0,0 +1,41 @@ +from fractions import Fraction + +import av + +from .common import fate_suite + + +def test_chapters() -> None: + expected = [ + { + "id": 1, + "start": 0, + "end": 5000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "start"}, + }, + { + "id": 2, + "start": 5000, + "end": 10500, + "time_base": Fraction(1, 1000), + "metadata": {"title": "Five Seconds"}, + }, + { + "id": 3, + "start": 10500, + "end": 15000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "Ten point 5 seconds"}, + }, + { + "id": 4, + "start": 15000, + "end": 19849, + "time_base": Fraction(1, 1000), + "metadata": {"title": "15 sec - over soon"}, + }, + ] + path = fate_suite("vorbis/vorbis_chapter_extension_demo.ogg") + with av.open(path) as container: + assert container.chapters() == expected