Skip to content

Commit a615d79

Browse files
authored
Add docstrings for Encoder (#1417)
1 parent 240c6f6 commit a615d79

3 files changed

Lines changed: 170 additions & 9 deletions

File tree

docs/source/api_ref_encoders.rst

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,28 @@ torchcodec.encoders
77
.. currentmodule:: torchcodec.encoders
88

99

10-
For an audio decoder tutorial, see: :ref:`sphx_glr_generated_examples_encoding_audio_encoding.py`.
10+
Multi-stream encoder
11+
--------------------
12+
13+
.. autosummary::
14+
:toctree: generated/
15+
:nosignatures:
16+
:template: class.rst
1117

18+
Encoder
19+
VideoStream
20+
AudioStream
21+
22+
23+
Single-stream encoders
24+
----------------------
25+
26+
For an audio decoder tutorial, see: :ref:`sphx_glr_generated_examples_encoding_audio_encoding.py`.
1227

1328
.. autosummary::
1429
:toctree: generated/
1530
:nosignatures:
1631
:template: class.rst
1732

18-
AudioEncoder
1933
VideoEncoder
34+
AudioEncoder
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from ._audio_encoder import AudioEncoder # noqa
2-
from ._multi_stream_encoder import Encoder # noqa
2+
from ._multi_stream_encoder import AudioStream, Encoder, VideoStream # noqa
33
from ._video_encoder import VideoEncoder # noqa

src/torchcodec/encoders/_multi_stream_encoder.py

Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,84 @@
1212
# confusing, and we should definitely not expose this one as-is. We should either:
1313
# - keep it private but rename it to something that's not stream_index
1414
# - make it absolute per container, if we ever want to expose it.
15-
class _VideoStream:
15+
class VideoStream:
16+
"""A video stream within an :class:`Encoder`.
17+
18+
Returned by :meth:`Encoder.add_video`. Use :meth:`add_frames` to feed
19+
video frames into this stream.
20+
"""
21+
1622
def __init__(self, encoder_tensor: Tensor, stream_index: int):
1723
self._encoder_tensor = encoder_tensor
1824
self._stream_index = stream_index
1925

2026
def add_frames(self, frames: Tensor) -> None:
27+
"""Add video frames to this stream.
28+
29+
Args:
30+
frames (``torch.Tensor``): The frames to encode. This must be a 4D
31+
tensor of shape ``(N, C, H, W)`` where N is the number of
32+
frames, C is 3 channels (RGB), H is height, and W is width.
33+
Values must be uint8 in the range ``[0, 255]``. The device of
34+
the tensor must match the ``device`` passed to
35+
:meth:`Encoder.add_video`.
36+
"""
2137
_core.streaming_encoder_add_frames(
2238
self._encoder_tensor, frames, self._stream_index
2339
)
2440

2541

26-
class _AudioStream:
42+
class AudioStream:
43+
"""An audio stream within an :class:`Encoder`.
44+
45+
Returned by :meth:`Encoder.add_audio`. Use :meth:`add_samples` to feed
46+
audio samples into this stream.
47+
"""
48+
2749
def __init__(self, encoder_tensor: Tensor, stream_index: int):
2850
self._encoder_tensor = encoder_tensor
2951
self._stream_index = stream_index
3052

3153
def add_samples(self, samples: Tensor) -> None:
54+
"""Add audio samples to this stream.
55+
56+
Args:
57+
samples (``torch.Tensor``): The samples to encode. This must be a
58+
2D tensor of shape ``(num_channels, num_samples)``. Values must
59+
be float values in ``[-1, 1]``. The number of channels must
60+
match the ``num_channels`` passed to :meth:`Encoder.add_audio`.
61+
"""
3262
_core.streaming_encoder_add_samples(
3363
self._encoder_tensor, samples, self._stream_index
3464
)
3565

3666

3767
class Encoder:
68+
"""A multi-stream encoder for encoding video and/or audio into a file or file-like object.
69+
70+
Unlike :class:`VideoEncoder` and :class:`AudioEncoder` which encode a
71+
single stream in one shot, ``Encoder`` supports multiple streams and
72+
incremental (streaming) encoding. Frames and samples can be added
73+
progressively, which is useful when data is generated on-the-fly or when
74+
encoding both audio and video into the same container.
75+
76+
Use :meth:`add_video` and :meth:`add_audio` to configure output streams,
77+
then open an output destination with :meth:`open_file` or
78+
:meth:`open_file_like`, feed data via the returned stream objects, and
79+
finally call :meth:`close` (or use the encoder as a context manager).
80+
81+
Example:
82+
83+
.. code-block:: python
84+
85+
with Encoder() as encoder:
86+
video_stream = encoder.add_video(height=256, width=256, frame_rate=30)
87+
audio_stream = encoder.add_audio(sample_rate=16000, num_channels=1)
88+
encoder.open_file("output.mp4")
89+
video_stream.add_frames(frames_tensor)
90+
audio_stream.add_samples(samples_tensor)
91+
"""
92+
3893
def __init__(self):
3994
self._encoder_tensor = _core.create_streaming_encoder()
4095

@@ -50,7 +105,43 @@ def add_video(
50105
crf: int | float | None = None,
51106
preset: str | int | None = None,
52107
extra_options: dict[str, Any] | None = None,
53-
) -> _VideoStream:
108+
) -> VideoStream:
109+
"""Add a video stream to the encoder.
110+
111+
Must be called before :meth:`open_file` or :meth:`open_file_like`.
112+
113+
Args:
114+
height (int): The height of the **input** video frames.
115+
width (int): The width of the **input** video frames.
116+
frame_rate (float): The frame rate of the **input** video frames.
117+
Also defines the encoded **output** frame rate.
118+
device (str, optional): The device to use for encoding, e.g.
119+
``"cpu"`` or ``"cuda"``. Default: ``"cpu"``.
120+
codec (str, optional): The codec to use for encoding (e.g.,
121+
``"libx264"``). If not specified, the default codec for the
122+
container format will be used.
123+
See :ref:`codec_selection` for details.
124+
pixel_format (str, optional): The pixel format for encoding (e.g.,
125+
``"yuv420p"``). If not specified, uses codec's default format.
126+
Must be left as ``None`` when encoding on CUDA.
127+
See :ref:`pixel_format` for details.
128+
crf (int or float, optional): Constant Rate Factor for encoding
129+
quality. Lower values mean better quality. Valid range depends
130+
on the encoder (e.g. 0-51 for libx264). Defaults to None (which
131+
will use encoder's default). See :ref:`crf` for details.
132+
preset (str or int, optional): Encoder option that controls the
133+
tradeoff between encoding speed and compression (output size).
134+
Commonly a string: ``"fast"``, ``"medium"``, ``"slow"``.
135+
Defaults to None (which will use encoder's default).
136+
See :ref:`preset` for details.
137+
extra_options (dict[str, Any], optional): A dictionary of additional
138+
encoder options to pass, e.g. ``{"qp": 5, "tune": "film"}``.
139+
See :ref:`extra_options` for details.
140+
141+
Returns:
142+
A video stream object. Use its :meth:`~VideoStream.add_frames`
143+
method to feed frames into the stream.
144+
"""
54145
preset = str(preset) if isinstance(preset, int) else preset
55146
stream_index = _core.streaming_encoder_add_video_stream(
56147
self._encoder_tensor,
@@ -66,7 +157,7 @@ def add_video(
66157
str(x) for k, v in (extra_options or {}).items() for x in (k, v)
67158
],
68159
)
69-
return _VideoStream(self._encoder_tensor, stream_index)
160+
return VideoStream(self._encoder_tensor, stream_index)
70161

71162
def add_audio(
72163
self,
@@ -76,7 +167,27 @@ def add_audio(
76167
bit_rate: int | None = None,
77168
out_num_channels: int | None = None,
78169
out_sample_rate: int | None = None,
79-
) -> _AudioStream:
170+
) -> AudioStream:
171+
"""Add an audio stream to the encoder.
172+
173+
Must be called before :meth:`open_file` or :meth:`open_file_like`.
174+
175+
Args:
176+
sample_rate (int): The sample rate of the **input** samples.
177+
num_channels (int): The number of channels of the **input** samples.
178+
bit_rate (int, optional): The output bit rate. Encoders typically
179+
support a finite set of bit rate values, so ``bit_rate`` will be
180+
matched to one of those supported values. The default is chosen
181+
by FFmpeg.
182+
out_num_channels (int, optional): The number of channels of the
183+
encoded output. By default, the input ``num_channels`` is used.
184+
out_sample_rate (int, optional): The sample rate of the encoded
185+
output. By default, the input ``sample_rate`` is used.
186+
187+
Returns:
188+
An audio stream object. Use its :meth:`~AudioStream.add_samples`
189+
method to feed samples into the stream.
190+
"""
80191
stream_index = _core.streaming_encoder_add_audio_stream(
81192
self._encoder_tensor,
82193
sample_rate=sample_rate,
@@ -85,17 +196,52 @@ def add_audio(
85196
output_num_channels=out_num_channels,
86197
output_sample_rate=out_sample_rate,
87198
)
88-
return _AudioStream(self._encoder_tensor, stream_index)
199+
return AudioStream(self._encoder_tensor, stream_index)
89200

90201
def open_file(self, dest: str | Path) -> "Encoder":
202+
"""Open a file for writing the encoded output.
203+
204+
Must be called after all streams have been added via :meth:`add_video`
205+
and/or :meth:`add_audio`. The file extension determines the container
206+
format (e.g. ``.mp4``, ``.mkv``).
207+
208+
Args:
209+
dest (str or ``pathlib.Path``): The path to the output file.
210+
211+
Returns:
212+
Encoder: Returns ``self`` for method chaining.
213+
"""
91214
_core.streaming_encoder_open_file(self._encoder_tensor, str(dest))
92215
return self
93216

94217
def open_file_like(self, dest, *, format: str) -> "Encoder":
218+
"""Open a file-like object for writing the encoded output.
219+
220+
Must be called after all streams have been added via :meth:`add_video`
221+
and/or :meth:`add_audio`.
222+
223+
Args:
224+
dest: A file-like object that supports ``write()`` and ``seek()``
225+
methods, such as ``io.BytesIO()``, an open file in binary write
226+
mode, etc. Methods must have the following signature:
227+
``write(data: bytes) -> int`` and ``seek(offset: int, whence:
228+
int = 0) -> int``.
229+
format (str): The container format of the encoded output, e.g.
230+
``"mp4"``, ``"mov"``, ``"mkv"``, ``"avi"``, ``"webm"``, etc.
231+
232+
Returns:
233+
Encoder: Returns ``self`` for method chaining.
234+
"""
95235
_core.streaming_encoder_open_file_like(self._encoder_tensor, format, dest)
96236
return self
97237

98238
def close(self) -> None:
239+
"""Flush all remaining data and close the encoder.
240+
241+
This must be called when encoding is complete to ensure all buffered
242+
data is written. Using the encoder as a context manager (``with``
243+
statement) calls this automatically.
244+
"""
99245
_core.streaming_encoder_close(self._encoder_tensor)
100246

101247
def __enter__(self) -> "Encoder":

0 commit comments

Comments
 (0)