From 8c2f1d712027901b8f4eb57f38084aeab63c7014 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Fri, 29 Aug 2025 17:43:51 +0200 Subject: [PATCH 1/4] Implemented set_chapters method --- av/container/core.pyi | 1 + av/container/core.pyx | 51 +++++++++++++++++++++++++++++++++++++++++- tests/test_chapters.py | 18 +++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index 8cd2a9dc5..5fe56343f 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -103,6 +103,7 @@ class Container: def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... def chapters(self) -> list[_Chapter]: ... + def set_chapters(self, chapters: list[_Chapter]) -> None: ... @overload def open( diff --git a/av/container/core.pyx b/av/container/core.pyx index 076faea15..299ac6ed3 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -15,7 +15,12 @@ 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, avrational_to_fraction +from av.utils cimport ( + avdict_to_dict, + avrational_to_fraction, + dict_to_avdict, + to_avrational, +) from av.dictionary import Dictionary from av.logging import Capture as LogCapture @@ -123,6 +128,17 @@ cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept return result +cdef void _free_chapters(lib.AVFormatContext *ctx) noexcept nogil: + cdef int i + if ctx.chapters != NULL: + for i in range(ctx.nb_chapters): + if ctx.chapters[i] != NULL: + if ctx.chapters[i].metadata != NULL: + lib.av_dict_free(&ctx.chapters[i].metadata) + lib.av_freep(&ctx.chapters[i]) + lib.av_freep(&ctx.chapters) + ctx.nb_chapters = 0 + class Flags(Flag): gen_pts: "Generate missing pts even if it requires parsing future frames." = lib.AVFMT_FLAG_GENPTS @@ -346,6 +362,39 @@ cdef class Container: }) return result + def set_chapters(self, chapters): + self._assert_open() + + cdef int count = len(chapters) + cdef int i + cdef lib.AVChapter **ch_array + cdef lib.AVChapter *ch + cdef dict entry + + with nogil: + _free_chapters(self.ptr) + + ch_array = lib.av_malloc(count * sizeof(lib.AVChapter *)) + if ch_array == NULL: + raise MemoryError("av_malloc failed for chapters") + + for i in range(count): + entry = chapters[i] + ch = lib.av_malloc(sizeof(lib.AVChapter)) + if ch == NULL: + raise MemoryError("av_malloc failed for chapter") + ch.id = entry["id"] + ch.start = entry["start"] + ch.end = entry["end"] + to_avrational(entry["time_base"], &ch.time_base) + ch.metadata = NULL + if "metadata" in entry: + dict_to_avdict(&ch.metadata, entry["metadata"], self.metadata_encoding, self.metadata_errors) + ch_array[i] = ch + + self.ptr.nb_chapters = count + self.ptr.chapters = ch_array + def open( file, mode=None, diff --git a/tests/test_chapters.py b/tests/test_chapters.py index 6a3d371b2..488a70fed 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -1,6 +1,7 @@ from fractions import Fraction import av +from av.container.core import _Chapter from .common import fate_suite @@ -39,3 +40,20 @@ def test_chapters() -> None: path = fate_suite("vorbis/vorbis_chapter_extension_demo.ogg") with av.open(path) as container: assert container.chapters() == expected + + +def test_set_chapters() -> None: + chapters: list[_Chapter] = [ + { + "id": 1, + "start": 0, + "end": 5000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "start"}, + } + ] + + path = fate_suite("h264/interlaced_crop.mp4") + with av.open(path) as container: + container.set_chapters(chapters) + assert container.chapters() == chapters From 82314b421a5662504793c77b61e3a786e8c8eeec Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Tue, 2 Sep 2025 09:05:37 +0200 Subject: [PATCH 2/4] Can't import _Chapter in test module. Mypy still fails... --- tests/test_chapters.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_chapters.py b/tests/test_chapters.py index 488a70fed..ab327f34b 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -1,7 +1,6 @@ from fractions import Fraction import av -from av.container.core import _Chapter from .common import fate_suite @@ -43,7 +42,7 @@ def test_chapters() -> None: def test_set_chapters() -> None: - chapters: list[_Chapter] = [ + chapters = [ { "id": 1, "start": 0, From 90f4ffab2eca82733e3e885e0d4c3716ed285d09 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Thu, 4 Sep 2025 07:59:55 +0200 Subject: [PATCH 3/4] Fixed mypy --- av/__init__.py | 3 ++- av/container/__init__.py | 2 +- av/container/core.pyi | 6 +++--- tests/test_chapters.py | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index e2f9e5a6d..50f30b749 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -18,7 +18,7 @@ from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext from av.codec.hwaccel import HWConfig -from av.container import open +from av.container import open, Chapter from av.format import ContainerFormat, formats_available from av.packet import Packet from av.error import * # noqa: F403; This is limited to exception types. @@ -41,6 +41,7 @@ "AudioStream", "BitStreamFilterContext", "bitstream_filters_available", + "Chapter", "Codec", "codecs_available", "CodecContext", diff --git a/av/container/__init__.py b/av/container/__init__.py index 98d49dd4e..18455c2f3 100644 --- a/av/container/__init__.py +++ b/av/container/__init__.py @@ -1,3 +1,3 @@ -from .core import Container, Flags, open +from .core import Chapter, Container, Flags, open from .input import InputContainer from .output import OutputContainer diff --git a/av/container/core.pyi b/av/container/core.pyi index 5fe56343f..f923ba01e 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -67,7 +67,7 @@ class AudioCodec(IntEnum): pcm_u8 = cast(int, ...) pcm_vidc = cast(int, ...) -class _Chapter(TypedDict): +class Chapter(TypedDict): id: int start: int end: int @@ -102,8 +102,8 @@ class Container: ) -> bool: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... - def chapters(self) -> list[_Chapter]: ... - def set_chapters(self, chapters: list[_Chapter]) -> None: ... + def chapters(self) -> list[Chapter]: ... + def set_chapters(self, chapters: list[Chapter]) -> None: ... @overload def open( diff --git a/tests/test_chapters.py b/tests/test_chapters.py index ab327f34b..3ad8e61b1 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -1,6 +1,7 @@ from fractions import Fraction import av +from av import Chapter from .common import fate_suite @@ -42,7 +43,7 @@ def test_chapters() -> None: def test_set_chapters() -> None: - chapters = [ + chapters: list[Chapter] = [ { "id": 1, "start": 0, From 668cc98215f2367a57e85b9140b2ed3feea50855 Mon Sep 17 00:00:00 2001 From: Max Christoph Date: Thu, 4 Sep 2025 08:24:12 +0200 Subject: [PATCH 4/4] Fixed import --- av/__init__.py | 3 +-- av/container/__init__.py | 2 +- tests/test_chapters.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index 50f30b749..e2f9e5a6d 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -18,7 +18,7 @@ from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext from av.codec.hwaccel import HWConfig -from av.container import open, Chapter +from av.container import open from av.format import ContainerFormat, formats_available from av.packet import Packet from av.error import * # noqa: F403; This is limited to exception types. @@ -41,7 +41,6 @@ "AudioStream", "BitStreamFilterContext", "bitstream_filters_available", - "Chapter", "Codec", "codecs_available", "CodecContext", diff --git a/av/container/__init__.py b/av/container/__init__.py index 18455c2f3..98d49dd4e 100644 --- a/av/container/__init__.py +++ b/av/container/__init__.py @@ -1,3 +1,3 @@ -from .core import Chapter, Container, Flags, open +from .core import Container, Flags, open from .input import InputContainer from .output import OutputContainer diff --git a/tests/test_chapters.py b/tests/test_chapters.py index 3ad8e61b1..c8a3626ce 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -1,7 +1,6 @@ from fractions import Fraction import av -from av import Chapter from .common import fate_suite @@ -43,7 +42,7 @@ def test_chapters() -> None: def test_set_chapters() -> None: - chapters: list[Chapter] = [ + chapters: list[av.container.Chapter] = [ { "id": 1, "start": 0,