Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bitstream filters #1375

Merged
merged 4 commits into from
Apr 9, 2024
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
1 change: 1 addition & 0 deletions av/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from av.audio.frame import AudioFrame
from av.audio.layout import AudioLayout
from av.audio.resampler import AudioResampler
from av.bitstream import BitStreamFilterContext, bitstream_filters_available
from av.codec.codec import Codec, codecs_available
from av.codec.context import CodecContext
from av.container import open
Expand Down
10 changes: 10 additions & 0 deletions av/bitstream.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
cimport libav as lib

from av.packet cimport Packet


cdef class BitStreamFilterContext:

cdef const lib.AVBSFContext *ptr

cpdef filter(self, Packet packet=?)
10 changes: 10 additions & 0 deletions av/bitstream.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .packet import Packet
from .stream import Stream

class BitStreamFilterContext:
def __init__(
self, filter_description: str | bytes, stream: Stream | None = None
): ...
def filter(self, packet: Packet | None) -> list[Packet]: ...

bitstream_filters_available: set[str]
82 changes: 82 additions & 0 deletions av/bitstream.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
cimport libav as lib
from libc.errno cimport EAGAIN

from av.error cimport err_check
from av.packet cimport Packet
from av.stream cimport Stream


cdef class BitStreamFilterContext:
"""
Initializes a bitstream filter: a way to directly modify packet data.

Wraps :ffmpeg:`AVBSFContext`
"""
def __cinit__(self, filter_description, Stream stream=None):
cdef int res
cdef char *filter_str = filter_description

with nogil:
res = lib.av_bsf_list_parse_str(filter_str, &self.ptr)
err_check(res)

if stream is not None:
with nogil:
res = lib.avcodec_parameters_copy(self.ptr.par_in, stream.ptr.codecpar)
err_check(res)
with nogil:
res = lib.avcodec_parameters_copy(self.ptr.par_out, stream.ptr.codecpar)
err_check(res)

with nogil:
res = lib.av_bsf_init(self.ptr)
err_check(res)

def __dealloc__(self):
if self.ptr:
lib.av_bsf_free(&self.ptr)

cpdef filter(self, Packet packet=None):
"""
Processes a packet based on the filter_description set during initialization.
Multiple packets may be created.

:type: list[Packet]
"""
cdef int res
cdef Packet new_packet

with nogil:
res = lib.av_bsf_send_packet(self.ptr, packet.ptr if packet is not None else NULL)
err_check(res)

output = []
while True:
new_packet = Packet()
with nogil:
res = lib.av_bsf_receive_packet(self.ptr, new_packet.ptr)

if res == -EAGAIN or res == lib.AVERROR_EOF:
return output

err_check(res)
if res:
return output

output.append(new_packet)


cdef get_filter_names():
names = set()
cdef const lib.AVBitStreamFilter *ptr
cdef void *opaque = NULL
while True:
ptr = lib.av_bsf_iterate(&opaque)
if ptr:
names.add(ptr.name)
else:
break

return names

bitstream_filters_available = get_filter_names()
6 changes: 3 additions & 3 deletions av/packet.pyi
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from fractions import Fraction
from typing import Iterator
from typing import Buffer, Iterator

from av.subtitles.subtitle import SubtitleSet

from .stream import Stream

class Packet:
class Packet(Buffer):
stream: Stream
stream_index: int
time_base: Fraction
Expand All @@ -20,5 +20,5 @@ class Packet:
is_trusted: bool
is_disposable: bool

def __init__(self, input: int | None = None) -> None: ...
def __init__(self, input: int | bytes | None = None) -> None: ...
def decode(self) -> Iterator[SubtitleSet]: ...
9 changes: 9 additions & 0 deletions docs/api/bitstream.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

Bitstream Filters
=================

.. automodule:: av.bitstream

.. autoclass:: BitStreamFilterContext
:members:

1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Currently we provide:
- ``libavcodec``:
:class:`.Codec`,
:class:`.CodecContext`,
:class:`.BitStreamFilterContext`,
audio/video :class:`frames <.Frame>`,
:class:`data planes <.Plane>`,
:class:`subtitles <.Subtitle>`;
Expand Down
1 change: 1 addition & 0 deletions include/libav.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ include "libavutil/samplefmt.pxd"
include "libavutil/motion_vector.pxd"

include "libavcodec/avcodec.pxd"
include "libavcodec/bsf.pxd"
include "libavdevice/avdevice.pxd"
include "libavformat/avformat.pxd"
include "libswresample/swresample.pxd"
Expand Down
36 changes: 36 additions & 0 deletions include/libavcodec/bsf.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

cdef extern from "libavcodec/bsf.h" nogil:

cdef struct AVBitStreamFilter:
const char *name
AVCodecID *codec_ids

cdef struct AVCodecParameters:
pass

cdef struct AVBSFContext:
const AVBitStreamFilter *filter
const AVCodecParameters *par_in
const AVCodecParameters *par_out

cdef const AVBitStreamFilter* av_bsf_get_by_name(const char *name)

cdef int av_bsf_list_parse_str(
const char *str,
AVBSFContext **bsf
)

cdef int av_bsf_init(AVBSFContext *ctx)
cdef void av_bsf_free(AVBSFContext **ctx)

cdef AVBitStreamFilter* av_bsf_iterate(void **opaque)

cdef int av_bsf_send_packet(
AVBSFContext *ctx,
AVPacket *pkt
)

cdef int av_bsf_receive_packet(
AVBSFContext *ctx,
AVPacket *pkt
)
62 changes: 62 additions & 0 deletions tests/test_bitstream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import av
from av import Packet
from av.bitstream import BitStreamFilterContext, bitstream_filters_available

from .common import TestCase, fate_suite


class TestBitStreamFilters(TestCase):
def test_filters_availible(self) -> None:
self.assertIn("h264_mp4toannexb", bitstream_filters_available)

def test_filter_chomp(self) -> None:
ctx = BitStreamFilterContext("chomp")

src_packets: tuple[Packet, None] = (Packet(b"\x0012345\0\0\0"), None)
self.assertEqual(bytes(src_packets[0]), b"\x0012345\0\0\0")

result_packets = []
for p in src_packets:
result_packets.extend(ctx.filter(p))

self.assertEqual(len(result_packets), 1)
self.assertEqual(bytes(result_packets[0]), b"\x0012345")

def test_filter_setts(self) -> None:
ctx = BitStreamFilterContext("setts=pts=N")

ctx2 = BitStreamFilterContext(b"setts=pts=N")
del ctx2

p1 = Packet(b"\0")
p1.pts = 42
p2 = Packet(b"\0")
p2.pts = 50
src_packets = [p1, p2, None]

result_packets: list[Packet] = []
for p in src_packets:
result_packets.extend(ctx.filter(p))

self.assertEqual(len(result_packets), 2)
self.assertEqual(result_packets[0].pts, 0)
self.assertEqual(result_packets[1].pts, 1)

def test_filter_h264_mp4toannexb(self) -> None:
def is_annexb(packet: Packet) -> bool:
data = bytes(packet)
return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01"

with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container:
stream = container.streams.video[0]
ctx = BitStreamFilterContext("h264_mp4toannexb", stream)

res_packets = []
for p in container.demux(stream):
self.assertFalse(is_annexb(p))
res_packets.extend(ctx.filter(p))

self.assertEqual(len(res_packets), stream.frames)

for p in res_packets:
self.assertTrue(is_annexb(p))
Loading