add read me

This commit is contained in:
2026-01-09 10:28:44 +11:00
commit edaf914b73
13417 changed files with 2952119 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from .core import Container, Flags, open
from .input import InputContainer
from .output import OutputContainer

View File

@@ -0,0 +1,3 @@
from .core import *
from .input import *
from .output import *

View File

@@ -0,0 +1,51 @@
cimport libav as lib
from av.codec.hwaccel cimport HWAccel
from av.container.pyio cimport PyIOFile
from av.container.streams cimport StreamContainer
from av.dictionary cimport _Dictionary
from av.format cimport ContainerFormat
from av.stream cimport Stream
# Interrupt callback information, times are in seconds.
ctypedef struct timeout_info:
double start_time
double timeout
cdef class Container:
cdef readonly bint writeable
cdef lib.AVFormatContext *ptr
cdef readonly object name
cdef readonly str metadata_encoding
cdef readonly str metadata_errors
cdef readonly PyIOFile file
cdef int buffer_size
cdef bint input_was_opened
cdef readonly object io_open
cdef readonly object open_files
cdef readonly ContainerFormat format
cdef readonly dict options
cdef readonly dict container_options
cdef readonly list stream_options
cdef HWAccel hwaccel
cdef readonly StreamContainer streams
cdef readonly dict metadata
# Private API.
cdef _assert_open(self)
cdef int err_check(self, int value) except -1
# Timeouts
cdef readonly object open_timeout
cdef readonly object read_timeout
cdef timeout_info interrupt_callback_info
cdef set_timeout(self, object)
cdef start_timeout(self)

View File

@@ -0,0 +1,167 @@
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, TypedDict, cast, overload
from av.codec.hwaccel import HWAccel
from av.format import ContainerFormat
from .input import InputContainer
from .output import OutputContainer
from .streams import StreamContainer
Real = int | float | Fraction
class Flags(Flag):
gen_pts = cast(ClassVar[Flags], ...)
ign_idx = cast(ClassVar[Flags], ...)
non_block = cast(ClassVar[Flags], ...)
ign_dts = cast(ClassVar[Flags], ...)
no_fillin = cast(ClassVar[Flags], ...)
no_parse = cast(ClassVar[Flags], ...)
no_buffer = cast(ClassVar[Flags], ...)
custom_io = cast(ClassVar[Flags], ...)
discard_corrupt = cast(ClassVar[Flags], ...)
flush_packets = cast(ClassVar[Flags], ...)
bitexact = cast(ClassVar[Flags], ...)
sort_dts = cast(ClassVar[Flags], ...)
fast_seek = cast(ClassVar[Flags], ...)
shortest = cast(ClassVar[Flags], ...)
auto_bsf = cast(ClassVar[Flags], ...)
class AudioCodec(IntEnum):
none = cast(int, ...)
pcm_alaw = cast(int, ...)
pcm_bluray = cast(int, ...)
pcm_dvd = cast(int, ...)
pcm_f16le = cast(int, ...)
pcm_f24le = cast(int, ...)
pcm_f32be = cast(int, ...)
pcm_f32le = cast(int, ...)
pcm_f64be = cast(int, ...)
pcm_f64le = cast(int, ...)
pcm_lxf = cast(int, ...)
pcm_mulaw = cast(int, ...)
pcm_s16be = cast(int, ...)
pcm_s16be_planar = cast(int, ...)
pcm_s16le = cast(int, ...)
pcm_s16le_planar = cast(int, ...)
pcm_s24be = cast(int, ...)
pcm_s24daud = cast(int, ...)
pcm_s24le = cast(int, ...)
pcm_s24le_planar = cast(int, ...)
pcm_s32be = cast(int, ...)
pcm_s32le = cast(int, ...)
pcm_s32le_planar = cast(int, ...)
pcm_s64be = cast(int, ...)
pcm_s64le = cast(int, ...)
pcm_s8 = cast(int, ...)
pcm_s8_planar = cast(int, ...)
pcm_u16be = cast(int, ...)
pcm_u16le = cast(int, ...)
pcm_u24be = cast(int, ...)
pcm_u24le = cast(int, ...)
pcm_u32be = cast(int, ...)
pcm_u32le = cast(int, ...)
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
metadata_encoding: str
metadata_errors: str
file: Any
buffer_size: int
input_was_opened: bool
io_open: Any
open_files: Any
format: ContainerFormat
options: dict[str, str]
container_options: dict[str, str]
stream_options: list[dict[str, str]]
streams: StreamContainer
metadata: dict[str, str]
open_timeout: Real | None
read_timeout: Real | None
flags: int
def __enter__(self) -> Container: ...
def __exit__(
self,
exc_type: Type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> 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: ...
@overload
def open(
file: Any,
mode: Literal["r"],
format: str | None = None,
options: dict[str, str] | None = None,
container_options: dict[str, str] | None = None,
stream_options: list[str] | None = None,
metadata_encoding: str = "utf-8",
metadata_errors: str = "strict",
buffer_size: int = 32768,
timeout: Real | None | tuple[Real | None, Real | None] = None,
io_open: Callable[..., Any] | None = None,
hwaccel: HWAccel | None = None,
) -> InputContainer: ...
@overload
def open(
file: str | Path,
mode: Literal["r"] | None = None,
format: str | None = None,
options: dict[str, str] | None = None,
container_options: dict[str, str] | None = None,
stream_options: list[str] | None = None,
metadata_encoding: str = "utf-8",
metadata_errors: str = "strict",
buffer_size: int = 32768,
timeout: Real | None | tuple[Real | None, Real | None] = None,
io_open: Callable[..., Any] | None = None,
hwaccel: HWAccel | None = None,
) -> InputContainer: ...
@overload
def open(
file: Any,
mode: Literal["w"],
format: str | None = None,
options: dict[str, str] | None = None,
container_options: dict[str, str] | None = None,
stream_options: list[str] | None = None,
metadata_encoding: str = "utf-8",
metadata_errors: str = "strict",
buffer_size: int = 32768,
timeout: Real | None | tuple[Real | None, Real | None] = None,
io_open: Callable[..., Any] | None = None,
hwaccel: HWAccel | None = None,
) -> OutputContainer: ...
@overload
def open(
file: Any,
mode: Literal["r", "w"] | None = None,
format: str | None = None,
options: dict[str, str] | None = None,
container_options: dict[str, str] | None = None,
stream_options: list[str] | None = None,
metadata_encoding: str = "utf-8",
metadata_errors: str = "strict",
buffer_size: int = 32768,
timeout: Real | None | tuple[Real | None, Real | None] = None,
io_open: Callable[..., Any] | None = None,
hwaccel: HWAccel | None = None,
) -> InputContainer | OutputContainer: ...

View File

@@ -0,0 +1,494 @@
from cython.operator cimport dereference
from libc.stdint cimport int64_t
import os
import time
from enum import Flag, IntEnum
from pathlib import Path
cimport libav as lib
from av.codec.hwaccel cimport HWAccel
from av.container.core cimport timeout_info
from av.container.input cimport InputContainer
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,
dict_to_avdict,
to_avrational,
)
from av.dictionary import Dictionary
from av.logging import Capture as LogCapture
cdef object _cinit_sentinel = object()
# We want to use the monotonic clock if it is available.
cdef object clock = getattr(time, "monotonic", time.time)
cdef int interrupt_cb (void *p) noexcept nogil:
cdef timeout_info info = dereference(<timeout_info*> p)
if info.timeout < 0: # timeout < 0 means no timeout
return 0
cdef double current_time
with gil:
current_time = clock()
# Check if the clock has been changed.
if current_time < info.start_time:
# Raise this when we get back to Python.
stash_exception((RuntimeError, RuntimeError("Clock has been changed to before timeout start"), None))
return 1
if current_time > info.start_time + info.timeout:
return 1
return 0
cdef int pyav_io_open(lib.AVFormatContext *s,
lib.AVIOContext **pb,
const char *url,
int flags,
lib.AVDictionary **options) noexcept nogil:
with gil:
return pyav_io_open_gil(s, pb, url, flags, options)
cdef int pyav_io_open_gil(lib.AVFormatContext *s,
lib.AVIOContext **pb,
const char *url,
int flags,
lib.AVDictionary **options) noexcept:
cdef Container container
cdef object file
cdef PyIOFile pyio_file
try:
container = <Container>dereference(s).opaque
if options is not NULL:
options_dict = avdict_to_dict(
dereference(<lib.AVDictionary**>options),
encoding=container.metadata_encoding,
errors=container.metadata_errors
)
else:
options_dict = {}
file = container.io_open(
<str>url if url is not NULL else "",
flags,
options_dict
)
pyio_file = PyIOFile(
file,
container.buffer_size,
(flags & lib.AVIO_FLAG_WRITE) != 0
)
# Add it to the container to avoid it being deallocated
container.open_files[<int64_t>pyio_file.iocontext.opaque] = pyio_file
pb[0] = pyio_file.iocontext
return 0
except Exception:
return stash_exception()
cdef int pyav_io_close(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept nogil:
with gil:
return pyav_io_close_gil(s, pb)
cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept:
cdef Container container
cdef int result = 0
try:
container = <Container>dereference(s).opaque
if container.open_files is not None and <int64_t>pb.opaque in container.open_files:
result = pyio_close_custom_gil(pb)
# Remove it from the container so that it can be deallocated
del container.open_files[<int64_t>pb.opaque]
else:
result = pyio_close_gil(pb)
except Exception:
stash_exception()
result = lib.AVERROR_UNKNOWN # Or another appropriate error code
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(<void **>&ctx.chapters[i])
lib.av_freep(<void **>&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
ign_idx: "Ignore index." = lib.AVFMT_FLAG_IGNIDX
non_block: "Do not block when reading packets from input." = lib.AVFMT_FLAG_NONBLOCK
ign_dts: "Ignore DTS on frames that contain both DTS & PTS." = lib.AVFMT_FLAG_IGNDTS
no_fillin: "Do not infer any values from other values, just return what is stored in the container." = lib.AVFMT_FLAG_NOFILLIN
no_parse: "Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fill in code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled." = lib.AVFMT_FLAG_NOPARSE
no_buffer: "Do not buffer frames when possible." = lib.AVFMT_FLAG_NOBUFFER
custom_io: "The caller has supplied a custom AVIOContext, don't avio_close() it." = lib.AVFMT_FLAG_CUSTOM_IO
discard_corrupt: "Discard frames marked corrupted." = lib.AVFMT_FLAG_DISCARD_CORRUPT
flush_packets: "Flush the AVIOContext every packet." = lib.AVFMT_FLAG_FLUSH_PACKETS
bitexact: "When muxing, try to avoid writing any random/volatile data to the output. This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing." = lib.AVFMT_FLAG_BITEXACT
sort_dts: "Try to interleave outputted packets by dts (using this flag can slow demuxing down)." = lib.AVFMT_FLAG_SORT_DTS
fast_seek: "Enable fast, but inaccurate seeks for some formats." = lib.AVFMT_FLAG_FAST_SEEK
auto_bsf: "Add bitstream filters as requested by the muxer." = lib.AVFMT_FLAG_AUTO_BSF
class AudioCodec(IntEnum):
"""Enumeration for audio codec IDs."""
none = lib.AV_CODEC_ID_NONE # No codec.
pcm_alaw = lib.AV_CODEC_ID_PCM_ALAW # PCM A-law.
pcm_bluray = lib.AV_CODEC_ID_PCM_BLURAY # PCM Blu-ray.
pcm_dvd = lib.AV_CODEC_ID_PCM_DVD # PCM DVD.
pcm_f16le = lib.AV_CODEC_ID_PCM_F16LE # PCM F16 little-endian.
pcm_f24le = lib.AV_CODEC_ID_PCM_F24LE # PCM F24 little-endian.
pcm_f32be = lib.AV_CODEC_ID_PCM_F32BE # PCM F32 big-endian.
pcm_f32le = lib.AV_CODEC_ID_PCM_F32LE # PCM F32 little-endian.
pcm_f64be = lib.AV_CODEC_ID_PCM_F64BE # PCM F64 big-endian.
pcm_f64le = lib.AV_CODEC_ID_PCM_F64LE # PCM F64 little-endian.
pcm_lxf = lib.AV_CODEC_ID_PCM_LXF # PCM LXF.
pcm_mulaw = lib.AV_CODEC_ID_PCM_MULAW # PCM μ-law.
pcm_s16be = lib.AV_CODEC_ID_PCM_S16BE # PCM signed 16-bit big-endian.
pcm_s16be_planar = lib.AV_CODEC_ID_PCM_S16BE_PLANAR # PCM signed 16-bit big-endian planar.
pcm_s16le = lib.AV_CODEC_ID_PCM_S16LE # PCM signed 16-bit little-endian.
pcm_s16le_planar = lib.AV_CODEC_ID_PCM_S16LE_PLANAR # PCM signed 16-bit little-endian planar.
pcm_s24be = lib.AV_CODEC_ID_PCM_S24BE # PCM signed 24-bit big-endian.
pcm_s24daud = lib.AV_CODEC_ID_PCM_S24DAUD # PCM signed 24-bit D-Cinema audio.
pcm_s24le = lib.AV_CODEC_ID_PCM_S24LE # PCM signed 24-bit little-endian.
pcm_s24le_planar = lib.AV_CODEC_ID_PCM_S24LE_PLANAR # PCM signed 24-bit little-endian planar.
pcm_s32be = lib.AV_CODEC_ID_PCM_S32BE # PCM signed 32-bit big-endian.
pcm_s32le = lib.AV_CODEC_ID_PCM_S32LE # PCM signed 32-bit little-endian.
pcm_s32le_planar = lib.AV_CODEC_ID_PCM_S32LE_PLANAR # PCM signed 32-bit little-endian planar.
pcm_s64be = lib.AV_CODEC_ID_PCM_S64BE # PCM signed 64-bit big-endian.
pcm_s64le = lib.AV_CODEC_ID_PCM_S64LE # PCM signed 64-bit little-endian.
pcm_s8 = lib.AV_CODEC_ID_PCM_S8 # PCM signed 8-bit.
pcm_s8_planar = lib.AV_CODEC_ID_PCM_S8_PLANAR # PCM signed 8-bit planar.
pcm_u16be = lib.AV_CODEC_ID_PCM_U16BE # PCM unsigned 16-bit big-endian.
pcm_u16le = lib.AV_CODEC_ID_PCM_U16LE # PCM unsigned 16-bit little-endian.
pcm_u24be = lib.AV_CODEC_ID_PCM_U24BE # PCM unsigned 24-bit big-endian.
pcm_u24le = lib.AV_CODEC_ID_PCM_U24LE # PCM unsigned 24-bit little-endian.
pcm_u32be = lib.AV_CODEC_ID_PCM_U32BE # PCM unsigned 32-bit big-endian.
pcm_u32le = lib.AV_CODEC_ID_PCM_U32LE # PCM unsigned 32-bit little-endian.
pcm_u8 = lib.AV_CODEC_ID_PCM_U8 # PCM unsigned 8-bit.
pcm_vidc = lib.AV_CODEC_ID_PCM_VIDC # PCM VIDC.
cdef class Container:
def __cinit__(self, sentinel, file_, format_name, options,
container_options, stream_options, hwaccel,
metadata_encoding, metadata_errors,
buffer_size, open_timeout, read_timeout,
io_open):
if sentinel is not _cinit_sentinel:
raise RuntimeError("cannot construct base Container")
self.writeable = isinstance(self, OutputContainer)
if not self.writeable and not isinstance(self, InputContainer):
raise RuntimeError("Container cannot be directly extended.")
if isinstance(file_, str):
self.name = file_
else:
self.name = str(getattr(file_, "name", "<none>"))
self.options = dict(options or ())
self.container_options = dict(container_options or ())
self.stream_options = [dict(x) for x in stream_options or ()]
self.hwaccel = hwaccel
self.metadata_encoding = metadata_encoding
self.metadata_errors = metadata_errors
self.open_timeout = open_timeout
self.read_timeout = read_timeout
self.buffer_size = buffer_size
self.io_open = io_open
acodec = None # no audio codec specified
if format_name is not None:
if ":" in format_name:
format_name, acodec = format_name.split(":")
self.format = ContainerFormat(format_name)
self.input_was_opened = False
cdef int res
cdef bytes name_obj = os.fsencode(self.name)
cdef char *name = name_obj
cdef lib.AVOutputFormat *ofmt
if self.writeable:
ofmt = self.format.optr if self.format else lib.av_guess_format(NULL, name, NULL)
if ofmt == NULL:
raise ValueError("Could not determine output format")
with nogil:
# This does not actually open the file.
res = lib.avformat_alloc_output_context2(
&self.ptr,
ofmt,
NULL,
name,
)
self.err_check(res)
else:
# We need the context before we open the input AND setup Python IO.
self.ptr = lib.avformat_alloc_context()
# Setup interrupt callback
if self.open_timeout is not None or self.read_timeout is not None:
self.ptr.interrupt_callback.callback = interrupt_cb
self.ptr.interrupt_callback.opaque = &self.interrupt_callback_info
if acodec is not None:
self.ptr.audio_codec_id = getattr(AudioCodec, acodec)
self.ptr.flags |= lib.AVFMT_FLAG_GENPTS
self.ptr.opaque = <void*>self
# Setup Python IO.
self.open_files = {}
if not isinstance(file_, basestring):
self.file = PyIOFile(file_, buffer_size, self.writeable)
self.ptr.pb = self.file.iocontext
if io_open is not None:
self.ptr.io_open = pyav_io_open
self.ptr.io_close2 = pyav_io_close
self.ptr.flags |= lib.AVFMT_FLAG_CUSTOM_IO
cdef lib.AVInputFormat *ifmt
cdef _Dictionary c_options
if not self.writeable:
ifmt = self.format.iptr if self.format else NULL
c_options = Dictionary(self.options, self.container_options)
self.set_timeout(self.open_timeout)
self.start_timeout()
with nogil:
res = lib.avformat_open_input(&self.ptr, name, ifmt, &c_options.ptr)
self.set_timeout(None)
self.err_check(res)
self.input_was_opened = True
if format_name is None:
self.format = build_container_format(self.ptr.iformat, self.ptr.oformat)
def __dealloc__(self):
with nogil:
lib.avformat_free_context(self.ptr)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def __repr__(self):
return f"<av.{self.__class__.__name__} {self.file or self.name!r}>"
cdef int err_check(self, int value) except -1:
return err_check(value, filename=self.name)
def dumps_format(self):
self._assert_open()
with LogCapture() as logs:
lib.av_dump_format(self.ptr, 0, "", isinstance(self, OutputContainer))
return "".join(log[2] for log in logs)
cdef set_timeout(self, timeout):
if timeout is None:
self.interrupt_callback_info.timeout = -1.0
else:
self.interrupt_callback_info.timeout = timeout
cdef start_timeout(self):
self.interrupt_callback_info.start_time = clock()
cdef _assert_open(self):
if self.ptr == NULL:
raise AssertionError("Container is not open")
@property
def flags(self):
self._assert_open()
return self.ptr.flags
@flags.setter
def flags(self, int value):
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 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.AVChapter **>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.AVChapter *>lib.av_malloc(sizeof(lib.AVChapter))
if ch == NULL:
raise MemoryError("av_malloc failed for chapter")
ch.id = entry["id"]
ch.start = <int64_t>entry["start"]
ch.end = <int64_t>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,
format=None,
options=None,
container_options=None,
stream_options=None,
metadata_encoding="utf-8",
metadata_errors="strict",
buffer_size=32768,
timeout=None,
io_open=None,
hwaccel=None
):
"""open(file, mode='r', **kwargs)
Main entrypoint to opening files/streams.
:param str file: The file to open, which can be either a string or a file-like object.
:param str mode: ``"r"`` for reading and ``"w"`` for writing.
:param str format: Specific format to use. Defaults to autodect.
:param dict options: Options to pass to the container and all streams.
:param dict container_options: Options to pass to the container.
:param list stream_options: Options to pass to each stream.
:param str metadata_encoding: Encoding to use when reading or writing file metadata.
Defaults to ``"utf-8"``.
:param str metadata_errors: Specifies how to handle encoding errors; behaves like
``str.encode`` parameter. Defaults to ``"strict"``.
:param int buffer_size: Size of buffer for Python input/output operations in bytes.
Honored only when ``file`` is a file-like object. Defaults to 32768 (32k).
:param timeout: How many seconds to wait for data before giving up, as a float, or a
``(open timeout, read timeout)`` tuple.
:param callable io_open: Custom I/O callable for opening files/streams.
This option is intended for formats that need to open additional
file-like objects to ``file`` using custom I/O.
The callable signature is ``io_open(url: str, flags: int, options: dict)``, where
``url`` is the url to open, ``flags`` is a combination of AVIO_FLAG_* and
``options`` is a dictionary of additional options. The callable should return a
file-like object.
:param HWAccel hwaccel: Optional settings for hardware-accelerated decoding.
:rtype: Container
For devices (via ``libavdevice``), pass the name of the device to ``format``,
e.g.::
>>> # Open webcam on MacOS.
>>> av.open('0', format='avfoundation') # doctest: +SKIP
For DASH and custom I/O using ``io_open``, add a protocol prefix to the ``file`` to
prevent the DASH encoder defaulting to the file protocol and using temporary files.
The custom I/O callable can be used to remove the protocol prefix to reveal the actual
name for creating the file-like object. E.g.::
>>> av.open("customprotocol://manifest.mpd", "w", io_open=custom_io) # doctest: +SKIP
.. seealso:: :ref:`garbage_collection`
More information on using input and output devices is available on the
`FFmpeg website <https://www.ffmpeg.org/ffmpeg-devices.html>`_.
"""
if not (mode is None or (isinstance(mode, str) and mode == "r" or mode == "w")):
raise ValueError(f"mode must be 'r', 'w', or None, got: {mode}")
if isinstance(file, str):
pass
elif isinstance(file, Path):
file = f"{file}"
elif mode is None:
mode = getattr(file, "mode", None)
if mode is None:
mode = "r"
if isinstance(timeout, tuple):
if not len(timeout) == 2:
raise ValueError("timeout must be `float` or `tuple[float, float]`")
open_timeout, read_timeout = timeout
else:
open_timeout = timeout
read_timeout = timeout
if mode.startswith("r"):
return InputContainer(_cinit_sentinel, file, format, options,
container_options, stream_options, hwaccel, metadata_encoding, metadata_errors,
buffer_size, open_timeout, read_timeout, io_open,
)
if stream_options:
raise ValueError(
"Provide stream options via Container.add_stream(..., options={})."
)
return OutputContainer(_cinit_sentinel, file, format, options,
container_options, stream_options, None, metadata_encoding, metadata_errors,
buffer_size, open_timeout, read_timeout, io_open,
)

View File

@@ -0,0 +1,9 @@
cimport libav as lib
from av.container.core cimport Container
from av.stream cimport Stream
cdef class InputContainer(Container):
cdef flush_buffers(self)

View File

@@ -0,0 +1,49 @@
from typing import Any, Iterator, overload
from av.audio.frame import AudioFrame
from av.audio.stream import AudioStream
from av.packet import Packet
from av.stream import Stream
from av.subtitles.stream import SubtitleStream
from av.subtitles.subtitle import SubtitleSet
from av.video.frame import VideoFrame
from av.video.stream import VideoStream
from .core import Container
class InputContainer(Container):
start_time: int
duration: int | None
bit_rate: int
size: int
def __enter__(self) -> InputContainer: ...
def close(self) -> None: ...
def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet]: ...
@overload
def decode(self, video: int) -> Iterator[VideoFrame]: ...
@overload
def decode(self, audio: int) -> Iterator[AudioFrame]: ...
@overload
def decode(self, subtitles: int) -> Iterator[SubtitleSet]: ...
@overload
def decode(self, *args: VideoStream) -> Iterator[VideoFrame]: ...
@overload
def decode(self, *args: AudioStream) -> Iterator[AudioFrame]: ...
@overload
def decode(self, *args: SubtitleStream) -> Iterator[SubtitleSet]: ...
@overload
def decode(
self, *args: Any, **kwargs: Any
) -> Iterator[VideoFrame | AudioFrame | SubtitleSet]: ...
def seek(
self,
offset: int,
*,
backward: bool = True,
any_frame: bool = False,
stream: Stream | VideoStream | AudioStream | None = None,
unsupported_frame_offset: bool = False,
unsupported_byte_offset: bool = False,
) -> None: ...
def flush_buffers(self) -> None: ...

View File

@@ -0,0 +1,290 @@
from libc.stdint cimport int64_t
from libc.stdlib cimport free, malloc
from av.codec.context cimport CodecContext, wrap_codec_context
from av.container.streams cimport StreamContainer
from av.dictionary cimport _Dictionary
from av.error cimport err_check
from av.packet cimport Packet
from av.stream cimport Stream, wrap_stream
from av.utils cimport avdict_to_dict
from av.dictionary import Dictionary
cdef close_input(InputContainer self):
self.streams = StreamContainer()
if self.input_was_opened:
with nogil:
# This causes `self.ptr` to be set to NULL.
lib.avformat_close_input(&self.ptr)
self.input_was_opened = False
cdef class InputContainer(Container):
def __cinit__(self, *args, **kwargs):
cdef CodecContext py_codec_context
cdef unsigned int i
cdef lib.AVStream *stream
cdef lib.AVCodec *codec
cdef lib.AVCodecContext *codec_context
# If we have either the global `options`, or a `stream_options`, prepare
# a mashup of those options for each stream.
cdef lib.AVDictionary **c_options = NULL
cdef _Dictionary base_dict, stream_dict
if self.options or self.stream_options:
base_dict = Dictionary(self.options)
c_options = <lib.AVDictionary**>malloc(self.ptr.nb_streams * sizeof(void*))
for i in range(self.ptr.nb_streams):
c_options[i] = NULL
if i < len(self.stream_options) and self.stream_options:
stream_dict = base_dict.copy()
stream_dict.update(self.stream_options[i])
lib.av_dict_copy(&c_options[i], stream_dict.ptr, 0)
else:
lib.av_dict_copy(&c_options[i], base_dict.ptr, 0)
self.set_timeout(self.open_timeout)
self.start_timeout()
with nogil:
# This peeks are the first few frames to:
# - set stream.disposition from codec.audio_service_type (not exposed);
# - set stream.codec.bits_per_coded_sample;
# - set stream.duration;
# - set stream.start_time;
# - set stream.r_frame_rate to average value;
# - open and closes codecs with the options provided.
ret = lib.avformat_find_stream_info(
self.ptr,
c_options
)
self.set_timeout(None)
self.err_check(ret)
# Cleanup all of our options.
if c_options:
for i in range(self.ptr.nb_streams):
lib.av_dict_free(&c_options[i])
free(c_options)
at_least_one_accelerated_context = False
self.streams = StreamContainer()
for i in range(self.ptr.nb_streams):
stream = self.ptr.streams[i]
codec = lib.avcodec_find_decoder(stream.codecpar.codec_id)
if codec:
# allocate and initialise decoder
codec_context = lib.avcodec_alloc_context3(codec)
err_check(lib.avcodec_parameters_to_context(codec_context, stream.codecpar))
codec_context.pkt_timebase = stream.time_base
py_codec_context = wrap_codec_context(codec_context, codec, self.hwaccel)
if py_codec_context.is_hwaccel:
at_least_one_accelerated_context = True
else:
# no decoder is available
py_codec_context = None
self.streams.add_stream(wrap_stream(self, stream, py_codec_context))
if self.hwaccel and not self.hwaccel.allow_software_fallback and not at_least_one_accelerated_context:
raise RuntimeError("Hardware accelerated decode requested but no stream is compatible")
self.metadata = avdict_to_dict(self.ptr.metadata, self.metadata_encoding, self.metadata_errors)
def __dealloc__(self):
close_input(self)
@property
def start_time(self):
self._assert_open()
if self.ptr.start_time != lib.AV_NOPTS_VALUE:
return self.ptr.start_time
@property
def duration(self):
self._assert_open()
if self.ptr.duration != lib.AV_NOPTS_VALUE:
return self.ptr.duration
@property
def bit_rate(self):
self._assert_open()
return self.ptr.bit_rate
@property
def size(self):
self._assert_open()
return lib.avio_size(self.ptr.pb)
def close(self):
close_input(self)
def demux(self, *args, **kwargs):
"""demux(streams=None, video=None, audio=None, subtitles=None, data=None)
Yields a series of :class:`.Packet` from the given set of :class:`.Stream`::
for packet in container.demux():
# Do something with `packet`, often:
for frame in packet.decode():
# Do something with `frame`.
.. seealso:: :meth:`.StreamContainer.get` for the interpretation of
the arguments.
.. note:: The last packets are dummy packets that when decoded will flush the buffers.
"""
self._assert_open()
# For whatever reason, Cython does not like us directly passing kwargs
# from one method to another. Without kwargs, it ends up passing a
# NULL reference, which segfaults. So we force it to do something with it.
# This is likely a bug in Cython; see https://github.com/cython/cython/issues/2166
# (and others).
id(kwargs)
streams = self.streams.get(*args, **kwargs)
cdef bint *include_stream = <bint*>malloc(self.ptr.nb_streams * sizeof(bint))
if include_stream == NULL:
raise MemoryError()
cdef unsigned int i
cdef Packet packet
cdef int ret
self.set_timeout(self.read_timeout)
try:
for i in range(self.ptr.nb_streams):
include_stream[i] = False
for stream in streams:
i = stream.index
if i >= self.ptr.nb_streams:
raise ValueError(f"stream index {i} out of range")
include_stream[i] = True
while True:
packet = Packet()
try:
self.start_timeout()
with nogil:
ret = lib.av_read_frame(self.ptr, packet.ptr)
self.err_check(ret)
except EOFError:
break
if include_stream[packet.ptr.stream_index]:
# If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams
# may also appear in av_read_frame().
# http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html
# TODO: find better way to handle this
if packet.ptr.stream_index < len(self.streams):
packet._stream = self.streams[packet.ptr.stream_index]
# Keep track of this so that remuxing is easier.
packet.ptr.time_base = packet._stream.ptr.time_base
yield packet
# Flush!
for i in range(self.ptr.nb_streams):
if include_stream[i]:
packet = Packet()
packet._stream = self.streams[i]
packet.ptr.time_base = packet._stream.ptr.time_base
yield packet
finally:
self.set_timeout(None)
free(include_stream)
def decode(self, *args, **kwargs):
"""decode(streams=None, video=None, audio=None, subtitles=None, data=None)
Yields a series of :class:`.Frame` from the given set of streams::
for frame in container.decode():
# Do something with `frame`.
.. seealso:: :meth:`.StreamContainer.get` for the interpretation of
the arguments.
"""
self._assert_open()
id(kwargs) # Avoid Cython bug; see demux().
for packet in self.demux(*args, **kwargs):
for frame in packet.decode():
yield frame
def seek(
self, offset, *, bint backward=True, bint any_frame=False, Stream stream=None,
bint unsupported_frame_offset=False, bint unsupported_byte_offset=False
):
"""seek(offset, *, backward=True, any_frame=False, stream=None)
Seek to a (key)frame nearsest to the given timestamp.
:param int offset: Time to seek to, expressed in``stream.time_base`` if ``stream``
is given, otherwise in :data:`av.time_base`.
:param bool backward: If there is not a (key)frame at the given offset,
look backwards for it.
:param bool any_frame: Seek to any frame, not just a keyframe.
:param Stream stream: The stream who's ``time_base`` the ``offset`` is in.
:param bool unsupported_frame_offset: ``offset`` is a frame
index instead of a time; not supported by any known format.
:param bool unsupported_byte_offset: ``offset`` is a byte
location in the file; not supported by any known format.
After seeking, packets that you demux should correspond (roughly) to
the position you requested.
In most cases, the defaults of ``backwards = True`` and ``any_frame = False``
are the best course of action, followed by you demuxing/decoding to
the position that you want. This is because to properly decode video frames
you need to start from the previous keyframe.
.. seealso:: :ffmpeg:`avformat_seek_file` for discussion of the flags.
"""
self._assert_open()
# We used to take floats here and assume they were in seconds. This
# was super confusing, so lets go in the complete opposite direction
# and reject non-ints.
if not isinstance(offset, int):
raise TypeError("Container.seek only accepts integer offset.", type(offset))
cdef int64_t c_offset = offset
cdef int flags = 0
cdef int ret
if backward:
flags |= lib.AVSEEK_FLAG_BACKWARD
if any_frame:
flags |= lib.AVSEEK_FLAG_ANY
# If someone really wants (and to experiment), expose these.
if unsupported_frame_offset:
flags |= lib.AVSEEK_FLAG_FRAME
if unsupported_byte_offset:
flags |= lib.AVSEEK_FLAG_BYTE
cdef int stream_index = stream.index if stream else -1
with nogil:
ret = lib.av_seek_frame(self.ptr, stream_index, c_offset, flags)
err_check(ret)
self.flush_buffers()
cdef flush_buffers(self):
self._assert_open()
cdef Stream stream
cdef CodecContext codec_context
for stream in self.streams:
codec_context = stream.codec_context
if codec_context:
codec_context.flush_buffers()

View File

@@ -0,0 +1,12 @@
cimport libav as lib
from av.container.core cimport Container
from av.stream cimport Stream
cdef class OutputContainer(Container):
cdef bint _started
cdef bint _done
cdef lib.AVPacket *packet_ptr
cpdef start_encoding(self)

View File

@@ -0,0 +1,474 @@
import os
from fractions import Fraction
import cython
from cython.cimports import libav as lib
from cython.cimports.av.codec.codec import Codec
from cython.cimports.av.codec.context import CodecContext, wrap_codec_context
from cython.cimports.av.container.streams import StreamContainer
from cython.cimports.av.dictionary import _Dictionary
from cython.cimports.av.error import err_check
from cython.cimports.av.packet import Packet
from cython.cimports.av.stream import Stream, wrap_stream
from cython.cimports.av.utils import dict_to_avdict, to_avrational
from av.dictionary import Dictionary
@cython.cfunc
def close_output(self: OutputContainer):
self.streams = StreamContainer()
if self._started and not self._done:
# We must only ever call av_write_trailer *once*, otherwise we get a
# segmentation fault. Therefore no matter whether it succeeds or not
# we must absolutely set self._done.
try:
self.err_check(lib.av_write_trailer(self.ptr))
finally:
if self.file is None and not (self.ptr.oformat.flags & lib.AVFMT_NOFILE):
lib.avio_closep(cython.address(self.ptr.pb))
self._done = True
@cython.cclass
class OutputContainer(Container):
def __cinit__(self, *args, **kwargs):
self.streams = StreamContainer()
self.metadata = {}
with cython.nogil:
self.packet_ptr = lib.av_packet_alloc()
def __dealloc__(self):
close_output(self)
with cython.nogil:
lib.av_packet_free(cython.address(self.packet_ptr))
def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwargs):
"""add_stream(codec_name, rate=None)
Creates a new stream from a codec name and returns it.
Supports video, audio, and subtitle streams.
:param codec_name: The name of a codec.
:type codec_name: str
:param dict options: Stream options.
:param \\**kwargs: Set attributes for the stream.
:rtype: The new :class:`~av.stream.Stream`.
"""
codec_obj: Codec = Codec(codec_name, "w")
codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr
# Assert that this format supports the requested codec.
if not lib.avformat_query_codec(
self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL
):
raise ValueError(
f"{self.format.name!r} format does not support {codec_obj.name!r} codec"
)
# Create new stream in the AVFormatContext, set AVCodecContext values.
stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec)
ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(codec)
# Now lets set some more sane video defaults
if codec.type == lib.AVMEDIA_TYPE_VIDEO:
ctx.pix_fmt = lib.AV_PIX_FMT_YUV420P
ctx.width = kwargs.pop("width", 640)
ctx.height = kwargs.pop("height", 480)
ctx.bit_rate = kwargs.pop("bit_rate", 0)
ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000)
try:
to_avrational(kwargs.pop("time_base"), cython.address(ctx.time_base))
except KeyError:
pass
to_avrational(rate or 24, cython.address(ctx.framerate))
stream.avg_frame_rate = ctx.framerate
stream.time_base = ctx.time_base
# Some sane audio defaults
elif codec.type == lib.AVMEDIA_TYPE_AUDIO:
ctx.sample_fmt = codec.sample_fmts[0]
ctx.bit_rate = kwargs.pop("bit_rate", 0)
ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000)
try:
to_avrational(kwargs.pop("time_base"), cython.address(ctx.time_base))
except KeyError:
pass
if rate is None:
ctx.sample_rate = 48000
elif type(rate) is int:
ctx.sample_rate = rate
else:
raise TypeError("audio stream `rate` must be: int | None")
stream.time_base = ctx.time_base
lib.av_channel_layout_default(cython.address(ctx.ch_layout), 2)
# Some formats want stream headers to be separate
if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER:
ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER
# Initialise stream codec parameters to populate the codec type.
#
# Subsequent changes to the codec context will be applied just before
# encoding starts in `start_encoding()`.
err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx))
# Construct the user-land stream
py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None)
py_stream: Stream = wrap_stream(self, stream, py_codec_context)
self.streams.add_stream(py_stream)
if options:
py_stream.options.update(options)
for k, v in kwargs.items():
setattr(py_stream, k, v)
return py_stream
def add_stream_from_template(
self, template: Stream, opaque: bool | None = None, **kwargs
):
"""
Creates a new stream from a template. Supports video, audio, subtitle, data and attachment streams.
:param template: Copy codec from another :class:`~av.stream.Stream` instance.
:param opaque: If True, copy opaque data from the template's codec context.
:param \\**kwargs: Set attributes for the stream.
:rtype: The new :class:`~av.stream.Stream`.
"""
if opaque is None:
opaque = template.type != "video"
if template.codec_context is None:
return self._add_stream_without_codec_from_template(template, **kwargs)
codec_obj: Codec
if opaque: # Copy ctx from template.
codec_obj = template.codec_context.codec
else: # Construct new codec object.
codec_obj = Codec(template.codec_context.codec.name, "w")
codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr
# Assert that this format supports the requested codec.
if not lib.avformat_query_codec(
self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL
):
raise ValueError(
f"{self.format.name!r} format does not support {codec_obj.name!r} codec"
)
# Create new stream in the AVFormatContext, set AVCodecContext values.
stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec)
ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(codec)
err_check(lib.avcodec_parameters_to_context(ctx, template.ptr.codecpar))
# Reset the codec tag assuming we are remuxing.
ctx.codec_tag = 0
# Some formats want stream headers to be separate
if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER:
ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER
# Copy flags If we're creating a new codec object. This fixes some muxing issues.
# Overwriting `ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER` is intentional.
if not opaque:
ctx.flags = template.codec_context.flags
# Initialize stream codec parameters to populate the codec type. Subsequent changes to
# the codec context will be applied just before encoding starts in `start_encoding()`.
err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx))
# Construct the user-land stream
py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None)
py_stream: Stream = wrap_stream(self, stream, py_codec_context)
self.streams.add_stream(py_stream)
for k, v in kwargs.items():
setattr(py_stream, k, v)
return py_stream
def _add_stream_without_codec_from_template(
self, template: Stream, **kwargs
) -> Stream:
codec_type: cython.int = template.ptr.codecpar.codec_type
if codec_type not in {lib.AVMEDIA_TYPE_ATTACHMENT, lib.AVMEDIA_TYPE_DATA}:
raise ValueError(
f"template stream of type {template.type} has no codec context"
)
stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(
self.ptr, cython.NULL
)
if stream == cython.NULL:
raise MemoryError("Could not allocate stream")
err_check(lib.avcodec_parameters_copy(stream.codecpar, template.ptr.codecpar))
# Mirror basic properties that are not derived from a codec context.
stream.time_base = template.ptr.time_base
stream.start_time = template.ptr.start_time
stream.duration = template.ptr.duration
stream.disposition = template.ptr.disposition
py_stream: Stream = wrap_stream(self, stream, None)
self.streams.add_stream(py_stream)
py_stream.metadata = dict(template.metadata)
for k, v in kwargs.items():
setattr(py_stream, k, v)
return py_stream
def add_attachment(self, name: str, mimetype: str, data: bytes):
"""
Create an attachment stream and embed its payload into the container header.
- Only supported by formats that support attachments (e.g. Matroska).
- No per-packet muxing is required; attachments are written at header time.
"""
# Create stream with no codec (attachments are codec-less).
stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(
self.ptr, cython.NULL
)
if stream == cython.NULL:
raise MemoryError("Could not allocate stream")
stream.codecpar.codec_type = lib.AVMEDIA_TYPE_ATTACHMENT
stream.codecpar.codec_id = lib.AV_CODEC_ID_NONE
# Allocate and copy payload into codecpar.extradata.
payload_size: cython.size_t = len(data)
if payload_size:
buf = cython.cast(cython.p_uchar, lib.av_malloc(payload_size + 1))
if buf == cython.NULL:
raise MemoryError("Could not allocate attachment data")
# Copy bytes.
for i in range(payload_size):
buf[i] = data[i]
buf[payload_size] = 0
stream.codecpar.extradata = cython.cast(cython.p_uchar, buf)
stream.codecpar.extradata_size = payload_size
# Wrap as user-land stream.
meta_ptr = cython.address(stream.metadata)
err_check(lib.av_dict_set(meta_ptr, b"filename", name.encode(), 0))
mime_bytes = mimetype.encode()
err_check(lib.av_dict_set(meta_ptr, b"mimetype", mime_bytes, 0))
py_stream: Stream = wrap_stream(self, stream, None)
self.streams.add_stream(py_stream)
return py_stream
def add_data_stream(self, codec_name=None, options: dict | None = None):
"""add_data_stream(codec_name=None)
Creates a new data stream and returns it.
:param codec_name: Optional name of the data codec (e.g. 'klv')
:type codec_name: str | None
:param dict options: Stream options.
:rtype: The new :class:`~av.data.stream.DataStream`.
"""
codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL
if codec_name is not None:
codec = lib.avcodec_find_encoder_by_name(codec_name.encode())
if codec == cython.NULL:
raise ValueError(f"Unknown data codec: {codec_name}")
# Assert that this format supports the requested codec
if not lib.avformat_query_codec(
self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL
):
raise ValueError(
f"{self.format.name!r} format does not support {codec_name!r} codec"
)
# Create new stream in the AVFormatContext
stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec)
if stream == cython.NULL:
raise MemoryError("Could not allocate stream")
# Set up codec context if we have a codec
ctx: cython.pointer[lib.AVCodecContext] = cython.NULL
if codec != cython.NULL:
ctx = lib.avcodec_alloc_context3(codec)
if ctx == cython.NULL:
raise MemoryError("Could not allocate codec context")
# Some formats want stream headers to be separate
if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER:
ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER
# Initialize stream codec parameters
err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx))
else:
# For raw data streams, just set the codec type
stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA
# Construct the user-land stream
py_codec_context: CodecContext | None = None
if ctx != cython.NULL:
py_codec_context = wrap_codec_context(ctx, codec, None)
py_stream: Stream = wrap_stream(self, stream, py_codec_context)
self.streams.add_stream(py_stream)
if options:
py_stream.options.update(options)
return py_stream
@cython.ccall
def start_encoding(self):
"""Write the file header! Called automatically."""
if self._started:
return
# TODO: This does NOT handle options coming from 3 sources.
# This is only a rough approximation of what would be cool to do.
used_options: set = set()
stream: Stream
# Finalize and open all streams.
for stream in self.streams:
ctx = stream.codec_context
# Skip codec context handling for streams without codecs (e.g. data/attachments).
if ctx is None:
if stream.type not in {"data", "attachment"}:
raise ValueError(f"Stream {stream.index} has no codec context")
else:
if not ctx.is_open:
for k, v in self.options.items():
ctx.options.setdefault(k, v)
ctx.open()
# Track option consumption.
for k in self.options:
if k not in ctx.options:
used_options.add(k)
stream._finalize_for_output()
# Open the output file, if needed.
name_obj: bytes = os.fsencode(self.name if self.file is None else "")
name: cython.p_char = name_obj
if self.ptr.pb == cython.NULL and not self.ptr.oformat.flags & lib.AVFMT_NOFILE:
err_check(
lib.avio_open(cython.address(self.ptr.pb), name, lib.AVIO_FLAG_WRITE)
)
# Copy the metadata dict.
dict_to_avdict(
cython.address(self.ptr.metadata),
self.metadata,
encoding=self.metadata_encoding,
errors=self.metadata_errors,
)
all_options: _Dictionary = Dictionary(self.options, self.container_options)
options: _Dictionary = all_options.copy()
self.err_check(lib.avformat_write_header(self.ptr, cython.address(options.ptr)))
# Track option usage...
for k in all_options:
if k not in options:
used_options.add(k)
# ... and warn if any weren't used.
unused_options = {
k: v for k, v in self.options.items() if k not in used_options
}
if unused_options:
import logging
log = logging.getLogger(__name__)
log.warning("Some options were not used: %s" % unused_options)
self._started = True
@property
def supported_codecs(self):
"""
Returns a set of all codecs this format supports.
"""
result: set = set()
codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL
opaque: cython.p_void = cython.NULL
while True:
codec = lib.av_codec_iterate(cython.address(opaque))
if codec == cython.NULL:
break
if (
lib.avformat_query_codec(
self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL
)
== 1
):
result.add(codec.name)
return result
@property
def default_video_codec(self):
"""
Returns the default video codec this container recommends.
"""
return lib.avcodec_get_name(self.format.optr.video_codec)
@property
def default_audio_codec(self):
"""
Returns the default audio codec this container recommends.
"""
return lib.avcodec_get_name(self.format.optr.audio_codec)
@property
def default_subtitle_codec(self):
"""
Returns the default subtitle codec this container recommends.
"""
return lib.avcodec_get_name(self.format.optr.subtitle_codec)
def close(self):
close_output(self)
def mux(self, packets):
# We accept either a Packet, or a sequence of packets. This should smooth out
# the transition to the new encode API which returns a sequence of packets.
if isinstance(packets, Packet):
self.mux_one(packets)
else:
for packet in packets:
self.mux_one(packet)
def mux_one(self, packet: Packet):
self.start_encoding()
# Assert the packet is in stream time.
if (
packet.ptr.stream_index < 0
or cython.cast(cython.uint, packet.ptr.stream_index) >= self.ptr.nb_streams
):
raise ValueError("Bad Packet stream_index.")
stream: cython.pointer[lib.AVStream] = self.ptr.streams[packet.ptr.stream_index]
packet._rebase_time(stream.time_base)
# Make another reference to the packet, as `av_interleaved_write_frame()`
# takes ownership of the reference.
self.err_check(lib.av_packet_ref(self.packet_ptr, packet.ptr))
with cython.nogil:
ret: cython.int = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr)
self.err_check(ret)

View File

@@ -0,0 +1,62 @@
from fractions import Fraction
from typing import Sequence, TypeVar, Union, overload
from av.audio import _AudioCodecName
from av.audio.stream import AudioStream
from av.packet import Packet
from av.stream import AttachmentStream, DataStream, Stream
from av.subtitles.stream import SubtitleStream
from av.video import _VideoCodecName
from av.video.stream import VideoStream
from .core import Container
_StreamT = TypeVar("_StreamT", bound=Stream)
class OutputContainer(Container):
def __enter__(self) -> OutputContainer: ...
@overload
def add_stream(
self,
codec_name: _AudioCodecName,
rate: int | None = None,
options: dict[str, str] | None = None,
**kwargs,
) -> AudioStream: ...
@overload
def add_stream(
self,
codec_name: _VideoCodecName,
rate: Fraction | int | None = None,
options: dict[str, str] | None = None,
**kwargs,
) -> VideoStream: ...
@overload
def add_stream(
self,
codec_name: str,
rate: Fraction | int | None = None,
options: dict[str, str] | None = None,
**kwargs,
) -> VideoStream | AudioStream | SubtitleStream: ...
def add_stream_from_template(
self, template: _StreamT, opaque: bool | None = None, **kwargs
) -> _StreamT: ...
def add_attachment(
self, name: str, mimetype: str, data: bytes
) -> AttachmentStream: ...
def add_data_stream(
self, codec_name: str | None = None, options: dict[str, str] | None = None
) -> DataStream: ...
def start_encoding(self) -> None: ...
def close(self) -> None: ...
def mux(self, packets: Packet | Sequence[Packet]) -> None: ...
def mux_one(self, packet: Packet) -> None: ...
@property
def default_video_codec(self) -> str: ...
@property
def default_audio_codec(self) -> str: ...
@property
def default_subtitle_codec(self) -> str: ...
@property
def supported_codecs(self) -> set[str]: ...

View File

@@ -0,0 +1,24 @@
cimport libav as lib
from libc.stdint cimport int64_t, uint8_t
cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil
cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil
cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil
cdef int pyio_close_gil(lib.AVIOContext *pb)
cdef int pyio_close_custom_gil(lib.AVIOContext *pb)
cdef class PyIOFile:
# File-like source.
cdef readonly object file
cdef object fread
cdef object fwrite
cdef object fseek
cdef object ftell
cdef object fclose
# Custom IO for above.
cdef lib.AVIOContext *iocontext
cdef unsigned char *buffer
cdef long pos
cdef bint pos_is_valid

View File

@@ -0,0 +1,169 @@
cimport libav as lib
from libc.string cimport memcpy
from av.error cimport stash_exception
ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil
cdef class PyIOFile:
def __cinit__(self, file, buffer_size, writeable=None):
self.file = file
cdef seek_func_t seek_func = NULL
readable = getattr(self.file, "readable", None)
writable = getattr(self.file, "writable", None)
seekable = getattr(self.file, "seekable", None)
self.fread = getattr(self.file, "read", None)
self.fwrite = getattr(self.file, "write", None)
self.fseek = getattr(self.file, "seek", None)
self.ftell = getattr(self.file, "tell", None)
self.fclose = getattr(self.file, "close", None)
# To be seekable the file object must have `seek` and `tell` methods.
# If it also has a `seekable` method, it must return True.
if (
self.fseek is not None
and self.ftell is not None
and (seekable is None or seekable())
):
seek_func = pyio_seek
if writeable is None:
writeable = self.fwrite is not None
if writeable:
if self.fwrite is None or (writable is not None and not writable()):
raise ValueError("File object has no write() method, or writable() returned False.")
else:
if self.fread is None or (readable is not None and not readable()):
raise ValueError("File object has no read() method, or readable() returned False.")
self.pos = 0
self.pos_is_valid = True
# This is effectively the maximum size of reads.
self.buffer = <unsigned char*>lib.av_malloc(buffer_size)
self.iocontext = lib.avio_alloc_context(
self.buffer,
buffer_size,
writeable,
<void*>self, # User data.
pyio_read,
pyio_write,
seek_func
)
if seek_func:
self.iocontext.seekable = lib.AVIO_SEEKABLE_NORMAL
self.iocontext.max_packet_size = buffer_size
def __dealloc__(self):
with nogil:
# FFmpeg will not release custom input, so it's up to us to free it.
# Do not touch our original buffer as it may have been freed and replaced.
if self.iocontext:
lib.av_freep(&self.iocontext.buffer)
lib.av_freep(&self.iocontext)
# We likely errored badly if we got here, and so are still
# responsible for our buffer.
else:
lib.av_freep(&self.buffer)
cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil:
with gil:
return pyio_read_gil(opaque, buf, buf_size)
cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept:
cdef PyIOFile self
cdef bytes res
try:
self = <PyIOFile>opaque
res = self.fread(buf_size)
memcpy(buf, <void*><char*>res, len(res))
self.pos += len(res)
if not res:
return lib.AVERROR_EOF
return len(res)
except Exception:
return stash_exception()
cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil:
with gil:
return pyio_write_gil(opaque, buf, buf_size)
cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept:
cdef PyIOFile self
cdef bytes bytes_to_write
cdef int bytes_written
try:
self = <PyIOFile>opaque
bytes_to_write = buf[:buf_size]
ret_value = self.fwrite(bytes_to_write)
bytes_written = ret_value if isinstance(ret_value, int) else buf_size
self.pos += bytes_written
return bytes_written
except Exception:
return stash_exception()
cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil:
# Seek takes the standard flags, but also a ad-hoc one which means that
# the library wants to know how large the file is. We are generally
# allowed to ignore this.
if whence == lib.AVSEEK_SIZE:
return -1
with gil:
return pyio_seek_gil(opaque, offset, whence)
cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence):
cdef PyIOFile self
try:
self = <PyIOFile>opaque
res = self.fseek(offset, whence)
# Track the position for the user.
if whence == 0:
self.pos = offset
elif whence == 1:
self.pos += offset
else:
self.pos_is_valid = False
if res is None:
if self.pos_is_valid:
res = self.pos
else:
res = self.ftell()
return res
except Exception:
return stash_exception()
cdef int pyio_close_gil(lib.AVIOContext *pb):
try:
return lib.avio_close(pb)
except Exception:
stash_exception()
cdef int pyio_close_custom_gil(lib.AVIOContext *pb):
cdef PyIOFile self
try:
self = <PyIOFile>pb.opaque
# Flush bytes in the AVIOContext buffers to the custom I/O
lib.avio_flush(pb)
if self.fclose is not None:
self.fclose()
return 0
except Exception:
stash_exception()

View File

@@ -0,0 +1,21 @@
cimport libav as lib
from av.stream cimport Stream
from .core cimport Container
cdef class StreamContainer:
cdef list _streams
# For the different types.
cdef readonly tuple video
cdef readonly tuple audio
cdef readonly tuple subtitles
cdef readonly tuple attachments
cdef readonly tuple data
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

View File

@@ -0,0 +1,35 @@
from typing import Iterator, Literal, overload
from av.audio.stream import AudioStream
from av.stream import AttachmentStream, DataStream, Stream
from av.subtitles.stream import SubtitleStream
from av.video.stream import VideoStream
class StreamContainer:
video: tuple[VideoStream, ...]
audio: tuple[AudioStream, ...]
subtitles: tuple[SubtitleStream, ...]
attachments: tuple[AttachmentStream, ...]
data: tuple[DataStream, ...]
other: tuple[Stream, ...]
def __init__(self) -> None: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[Stream]: ...
@overload
def __getitem__(self, index: int) -> Stream: ...
@overload
def __getitem__(self, index: slice) -> list[Stream]: ...
@overload
def __getitem__(self, index: int | slice) -> Stream | list[Stream]: ...
def get(
self,
*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: ...

View File

@@ -0,0 +1,170 @@
cimport libav as lib
def _flatten(input_):
for x in input_:
if isinstance(x, (tuple, list)):
for y in _flatten(x):
yield y
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:
"""
A tuple-like container of :class:`Stream`.
::
# There are a few ways to pulling out streams.
first = container.streams[0]
video = container.streams.video[0]
audio = container.streams.get(audio=(0, 1))
"""
def __cinit__(self):
self._streams = []
self.video = ()
self.audio = ()
self.subtitles = ()
self.data = ()
self.attachments = ()
self.other = ()
cdef add_stream(self, Stream stream):
assert stream.ptr.index == len(self._streams)
self._streams.append(stream)
if stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO:
self.video = self.video + (stream, )
elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_AUDIO:
self.audio = self.audio + (stream, )
elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE:
self.subtitles = self.subtitles + (stream, )
elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT:
self.attachments = self.attachments + (stream, )
elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA:
self.data = self.data + (stream, )
else:
self.other = self.other + (stream, )
# Basic tuple interface.
def __len__(self):
return len(self._streams)
def __iter__(self):
return iter(self._streams)
def __getitem__(self, index):
if isinstance(index, int):
return self.get(index)[0]
else:
return self.get(index)
def get(self, *args, **kwargs):
"""get(streams=None, video=None, audio=None, subtitles=None, data=None)
Get a selection of :class:`.Stream` as a ``list``.
Positional arguments may be ``int`` (which is an index into the streams),
or ``list`` or ``tuple`` of those::
# Get the first channel.
streams.get(0)
# Get the first two audio channels.
streams.get(audio=(0, 1))
Keyword arguments (or dicts as positional arguments) as interpreted
as ``(stream_type, index_value_or_set)`` pairs::
# Get the first video channel.
streams.get(video=0)
# or
streams.get({'video': 0})
:class:`.Stream` objects are passed through untouched.
If nothing is selected, then all streams are returned.
"""
selection = []
for x in _flatten((args, kwargs)):
if x is None:
pass
elif isinstance(x, Stream):
selection.append(x)
elif isinstance(x, int):
selection.append(self._streams[x])
elif isinstance(x, dict):
for type_, indices in x.items():
if type_ == "streams": # For compatibility with the pseudo signature
streams = self._streams
else:
streams = getattr(self, type_)
if not isinstance(indices, (tuple, list)):
indices = [indices]
for i in indices:
selection.append(streams[i])
else:
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`. Example::
stream = container.streams.best("video")
: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]