Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions av/container/core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -86,7 +93,6 @@ class Container:
open_timeout: Real | None
read_timeout: Real | None
flags: int

def __enter__(self) -> Container: ...
def __exit__(
self,
Expand All @@ -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(
Expand Down
18 changes: 17 additions & 1 deletion av/container/core.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions include/libavformat/avformat.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
41 changes: 41 additions & 0 deletions tests/test_chapters.py
Original file line number Diff line number Diff line change
@@ -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
Loading