Skip to content

Commit 6a0cf68

Browse files
skeskinenWyattBlue
authored andcommitted
Add subtitle encoding support
1 parent 4c4a54c commit 6a0cf68

File tree

8 files changed

+365
-7
lines changed

8 files changed

+365
-7
lines changed

av/subtitles/codeccontext.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ from av.packet cimport Packet
33

44

55
cdef class SubtitleCodecContext(CodecContext):
6+
cdef bint subtitle_header_set
67
cpdef decode2(self, Packet packet)

av/subtitles/codeccontext.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,108 @@
11
import cython
22
from cython.cimports import libav as lib
3+
from cython.cimports.av.bytesource import ByteSource, bytesource
34
from cython.cimports.av.error import err_check
45
from cython.cimports.av.packet import Packet
56
from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet
7+
from cython.cimports.cpython.bytes import PyBytes_FromStringAndSize
8+
from cython.cimports.libc.string import memcpy, strlen
69

710

811
@cython.cclass
912
class SubtitleCodecContext(CodecContext):
13+
@property
14+
def subtitle_header(self) -> bytes | None:
15+
"""Get the subtitle header data (ASS/SSA format for text subtitles)."""
16+
if (
17+
self.ptr.subtitle_header == cython.NULL
18+
or self.ptr.subtitle_header_size <= 0
19+
):
20+
return None
21+
return PyBytes_FromStringAndSize(
22+
cython.cast(cython.p_char, self.ptr.subtitle_header),
23+
self.ptr.subtitle_header_size,
24+
)
25+
26+
@subtitle_header.setter
27+
def subtitle_header(self, data: bytes | None) -> None:
28+
"""Set the subtitle header data."""
29+
source: ByteSource
30+
if data is None:
31+
lib.av_freep(cython.address(self.ptr.subtitle_header))
32+
self.ptr.subtitle_header_size = 0
33+
else:
34+
source = bytesource(data)
35+
self.ptr.subtitle_header = cython.cast(
36+
cython.p_uchar,
37+
lib.av_realloc(
38+
self.ptr.subtitle_header,
39+
source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE,
40+
),
41+
)
42+
if not self.ptr.subtitle_header:
43+
raise MemoryError("Cannot allocate subtitle_header")
44+
memcpy(self.ptr.subtitle_header, source.ptr, source.length)
45+
self.ptr.subtitle_header_size = source.length
46+
self.subtitle_header_set = True
47+
48+
def __dealloc__(self) -> None:
49+
if self.ptr and self.subtitle_header_set:
50+
lib.av_freep(cython.address(self.ptr.subtitle_header))
51+
52+
def encode_subtitle(self, subtitle: SubtitleSet) -> Packet:
53+
"""
54+
Encode a SubtitleSet into a Packet.
55+
56+
Args:
57+
subtitle: The SubtitleSet to encode
58+
59+
Returns:
60+
A Packet containing the encoded subtitle data
61+
"""
62+
if not self.codec.ptr:
63+
raise ValueError("Cannot encode with unknown codec")
64+
65+
self.open(strict=False)
66+
67+
# Calculate buffer size from subtitle text length
68+
buf_size: cython.size_t = 0
69+
i: cython.uint
70+
for i in range(subtitle.proxy.struct.num_rects):
71+
rect = subtitle.proxy.struct.rects[i]
72+
if rect.ass != cython.NULL:
73+
buf_size += strlen(rect.ass)
74+
if rect.text != cython.NULL:
75+
buf_size += strlen(rect.text)
76+
buf_size += 1024 # padding for format overhead
77+
78+
buf: cython.p_uchar = cython.cast(cython.p_uchar, lib.av_malloc(buf_size))
79+
if buf == cython.NULL:
80+
raise MemoryError("Failed to allocate subtitle encode buffer")
81+
82+
ret: cython.int = lib.avcodec_encode_subtitle(
83+
self.ptr,
84+
buf,
85+
buf_size,
86+
cython.address(subtitle.proxy.struct),
87+
)
88+
89+
if ret < 0:
90+
lib.av_free(buf)
91+
err_check(ret, "avcodec_encode_subtitle()")
92+
93+
packet: Packet = Packet(ret)
94+
memcpy(packet.ptr.data, buf, ret)
95+
lib.av_free(buf)
96+
97+
packet.ptr.pts = subtitle.proxy.struct.pts
98+
packet.ptr.dts = subtitle.proxy.struct.pts
99+
packet.ptr.duration = (
100+
subtitle.proxy.struct.end_display_time
101+
- subtitle.proxy.struct.start_display_time
102+
)
103+
104+
return packet
105+
10106
@cython.cfunc
11107
def _send_packet_and_recv(self, packet: Packet | None):
12108
if packet is None:

av/subtitles/codeccontext.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ from av.subtitles.subtitle import SubtitleSet
66

77
class SubtitleCodecContext(CodecContext):
88
type: Literal["subtitle"]
9+
subtitle_header: bytes | None
910
def decode2(self, packet: Packet) -> SubtitleSet | None: ...
11+
def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: ...

av/subtitles/subtitle.py

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cython
22
from cython.cimports.cpython import PyBuffer_FillInfo, PyBytes_FromString
3-
from cython.cimports.libc.stdint import uint64_t
3+
from cython.cimports.libc.stdint import int64_t, uint64_t
4+
from cython.cimports.libc.string import memcpy, strlen
45

56

67
@cython.cclass
@@ -9,6 +10,9 @@ def __dealloc__(self):
910
lib.avsubtitle_free(cython.address(self.struct))
1011

1112

13+
_cinit_bypass_sentinel = object()
14+
15+
1216
@cython.cclass
1317
class SubtitleSet:
1418
"""
@@ -17,11 +21,94 @@ class SubtitleSet:
1721
Wraps :ffmpeg:`AVSubtitle`.
1822
"""
1923

20-
def __cinit__(self, proxy: SubtitleProxy):
21-
self.proxy = proxy
22-
self.rects = tuple(
23-
build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)
24+
def __cinit__(self, proxy_or_sentinel=None):
25+
if proxy_or_sentinel is _cinit_bypass_sentinel:
26+
# Creating empty SubtitleSet for encoding
27+
self.proxy = SubtitleProxy()
28+
self.rects = ()
29+
elif isinstance(proxy_or_sentinel, SubtitleProxy):
30+
# Creating from decoded subtitle
31+
self.proxy = proxy_or_sentinel
32+
self.rects = tuple(
33+
build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)
34+
)
35+
else:
36+
raise TypeError(
37+
"SubtitleSet requires a SubtitleProxy or use SubtitleSet.create()"
38+
)
39+
40+
@staticmethod
41+
def create(
42+
text: bytes,
43+
start: int,
44+
end: int,
45+
pts: int = 0,
46+
subtitle_format: int = 1,
47+
) -> "SubtitleSet":
48+
"""
49+
Create a SubtitleSet for encoding.
50+
51+
Args:
52+
text: The subtitle text in ASS dialogue format
53+
(e.g. b"0,0,Default,,0,0,0,,Hello World")
54+
start: Start display time as offset from pts (typically 0)
55+
end: End display time as offset from pts (i.e., duration)
56+
pts: Presentation timestamp in stream time_base units
57+
subtitle_format: Subtitle format (default 1 for text)
58+
59+
Note:
60+
All timing values should be in stream time_base units.
61+
For MKV (time_base=1/1000), units are milliseconds.
62+
For MP4 (time_base=1/1000000), units are microseconds.
63+
64+
Returns:
65+
A SubtitleSet ready for encoding
66+
"""
67+
subset: SubtitleSet = SubtitleSet(_cinit_bypass_sentinel)
68+
69+
subset.proxy.struct.format = subtitle_format
70+
subset.proxy.struct.start_display_time = start
71+
subset.proxy.struct.end_display_time = end
72+
subset.proxy.struct.pts = pts
73+
74+
subset.proxy.struct.num_rects = 1
75+
subset.proxy.struct.rects = cython.cast(
76+
cython.pointer[cython.pointer[lib.AVSubtitleRect]],
77+
lib.av_mallocz(cython.sizeof(cython.pointer[lib.AVSubtitleRect])),
78+
)
79+
if subset.proxy.struct.rects == cython.NULL:
80+
raise MemoryError("Failed to allocate subtitle rects array")
81+
82+
rect: cython.pointer[lib.AVSubtitleRect] = cython.cast(
83+
cython.pointer[lib.AVSubtitleRect],
84+
lib.av_mallocz(cython.sizeof(lib.AVSubtitleRect)),
2485
)
86+
if rect == cython.NULL:
87+
lib.av_free(subset.proxy.struct.rects)
88+
subset.proxy.struct.rects = cython.NULL
89+
raise MemoryError("Failed to allocate subtitle rect")
90+
91+
subset.proxy.struct.rects[0] = rect
92+
93+
rect.x = 0
94+
rect.y = 0
95+
rect.w = 0
96+
rect.h = 0
97+
rect.nb_colors = 0
98+
rect.type = lib.SUBTITLE_ASS
99+
rect.text = cython.NULL
100+
rect.flags = 0
101+
102+
text_len: cython.Py_ssize_t = len(text)
103+
rect.ass = cython.cast(cython.p_char, lib.av_malloc(text_len + 1))
104+
if rect.ass == cython.NULL:
105+
raise MemoryError("Failed to allocate subtitle text")
106+
memcpy(rect.ass, cython.cast(cython.p_char, text), text_len)
107+
rect.ass[text_len] = 0
108+
109+
subset.rects = (AssSubtitle(subset, 0),)
110+
111+
return subset
25112

26113
def __repr__(self):
27114
return (
@@ -32,19 +119,35 @@ def __repr__(self):
32119
def format(self):
33120
return self.proxy.struct.format
34121

122+
@format.setter
123+
def format(self, value: int):
124+
self.proxy.struct.format = value
125+
35126
@property
36127
def start_display_time(self):
37128
return self.proxy.struct.start_display_time
38129

130+
@start_display_time.setter
131+
def start_display_time(self, value: int):
132+
self.proxy.struct.start_display_time = value
133+
39134
@property
40135
def end_display_time(self):
41136
return self.proxy.struct.end_display_time
42137

138+
@end_display_time.setter
139+
def end_display_time(self, value: int):
140+
self.proxy.struct.end_display_time = value
141+
43142
@property
44143
def pts(self):
45144
"""Same as packet pts, in av.time_base."""
46145
return self.proxy.struct.pts
47146

147+
@pts.setter
148+
def pts(self, value: int):
149+
self.proxy.struct.pts = value
150+
48151
def __len__(self):
49152
return len(self.rects)
50153

av/subtitles/subtitle.pyi

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ class SubtitleSet:
55
start_display_time: int
66
end_display_time: int
77
pts: int
8-
rects: tuple[Subtitle]
8+
rects: tuple[Subtitle, ...]
99

10+
@staticmethod
11+
def create(
12+
text: bytes,
13+
start: int,
14+
end: int,
15+
pts: int = 0,
16+
subtitle_format: int = 1,
17+
) -> SubtitleSet: ...
1018
def __len__(self) -> int: ...
1119
def __iter__(self) -> Iterator[Subtitle]: ...
1220
def __getitem__(self, i: int) -> Subtitle: ...

include/libavcodec/avcodec.pxd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@ cdef extern from "libavcodec/avcodec.h" nogil:
289289
int extradata_size
290290
uint8_t *extradata
291291

292+
# Subtitle header (ASS/SSA format for text subtitles)
293+
uint8_t *subtitle_header
294+
int subtitle_header_size
295+
292296
int delay
293297

294298
AVCodec *codec

include/libavutil/avutil.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,11 @@ cdef extern from "libavutil/avutil.h" nogil:
110110
cdef double M_PI
111111

112112
cdef void* av_malloc(size_t size)
113+
cdef void* av_mallocz(size_t size)
113114
cdef void *av_calloc(size_t nmemb, size_t size)
114115
cdef void *av_realloc(void *ptr, size_t size)
115116

117+
cdef void av_free(void *ptr)
116118
cdef void av_freep(void *ptr)
117119

118120
cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt)

0 commit comments

Comments
 (0)