Skip to content

Commit e5d0acc

Browse files
author
pytorchbot
committed
2026-05-18 nightly release (11a4d98)
1 parent b07864e commit e5d0acc

10 files changed

Lines changed: 843 additions & 306 deletions

File tree

docs/source/api_ref_encoders.rst

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,32 @@ 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+
--------------------
1112

13+
For a tutorial, see: :ref:`sphx_glr_generated_examples_encoding_multi_stream_encoding.py`.
14+
15+
.. autosummary::
16+
:toctree: generated/
17+
:nosignatures:
18+
:template: class.rst
19+
20+
Encoder
21+
VideoStream
22+
AudioStream
23+
24+
25+
Single-stream encoders
26+
----------------------
27+
28+
For a video encoder tutorial, see: :ref:`sphx_glr_generated_examples_encoding_video_encoding.py`.
29+
30+
For an audio encoder tutorial, see: :ref:`sphx_glr_generated_examples_encoding_audio_encoding.py`.
1231

1332
.. autosummary::
1433
:toctree: generated/
1534
:nosignatures:
1635
:template: class.rst
1736

18-
AudioEncoder
1937
VideoEncoder
38+
AudioEncoder

docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def __call__(self, filename):
8585
order = [
8686
"audio_encoding.py",
8787
"video_encoding.py",
88+
"multi_stream_encoding.py",
8889
]
8990

9091
try:
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""
8+
=================================================
9+
Encoding audio and video streams with the Encoder
10+
=================================================
11+
12+
In this example, we'll learn how to encode multiple video and audio streams
13+
into a single container using the :class:`~torchcodec.encoders.Encoder` class.
14+
We'll also see how to feed audio samples and video frames incrementally, and how
15+
to mix CPU and CUDA video streams.
16+
17+
For details on video encoding parameters (codec, CRF, preset, etc.), see
18+
:ref:`sphx_glr_generated_examples_encoding_video_encoding.py`.
19+
"""
20+
21+
# %%
22+
# Video + audio encoding
23+
# -----------------------
24+
#
25+
# Let's start by encoding a video alongside an audio track into the same MP4
26+
# file. We'll decode some video frames from an existing video and generate a
27+
# simple sine-wave audio tone.
28+
29+
import subprocess
30+
import tempfile
31+
from pathlib import Path
32+
33+
import requests
34+
import torch
35+
from torchcodec.decoders import VideoDecoder
36+
from torchcodec.encoders import Encoder
37+
38+
# sphinx_gallery_thumbnail_path = '_static/thumbnails/not_grumps_encoding_video.jpg'
39+
40+
# Video source: https://www.pexels.com/video/adorable-cats-on-the-lawn-4977395/
41+
# Author: Altaf Shah.
42+
url = "https://videos.pexels.com/video-files/4977395/4977395-hd_1920_1080_24fps.mp4"
43+
44+
response = requests.get(url, headers={"User-Agent": ""})
45+
if response.status_code != 200:
46+
raise RuntimeError(f"Failed to download video. {response.status_code = }.")
47+
48+
decoder = VideoDecoder(response.content)
49+
frames = decoder.get_frames_in_range(0, 60).data
50+
frame_rate = decoder.metadata.average_fps
51+
52+
# Generate a 440 Hz sine wave that lasts as long as the video
53+
audio_sample_rate = 16000
54+
duration_seconds = len(frames) / frame_rate
55+
t = torch.linspace(
56+
0, duration_seconds, int(audio_sample_rate * duration_seconds),
57+
dtype=torch.float32,
58+
)
59+
audio_samples = torch.sin(2 * torch.pi * 440 * t).unsqueeze(0) # shape: (1, num_samples)
60+
61+
# %%
62+
# Now we create an :class:`~torchcodec.encoders.Encoder`, add one video stream
63+
# and one audio stream, and encode everything into a single file. Each call to
64+
# :meth:`~torchcodec.encoders.Encoder.add_video` or
65+
# :meth:`~torchcodec.encoders.Encoder.add_audio` returns a stream object that
66+
# we use to feed data.
67+
68+
output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
69+
encoder = Encoder()
70+
video_stream = encoder.add_video(
71+
height=frames.shape[2], width=frames.shape[3], frame_rate=frame_rate,
72+
)
73+
audio_stream = encoder.add_audio(sample_rate=audio_sample_rate, num_channels=1)
74+
75+
with encoder.open_file(output_path):
76+
video_stream.add_frames(frames)
77+
audio_stream.add_samples(audio_samples)
78+
79+
print(f"Encoded video + audio to {output_path}")
80+
print(f"Output size: {Path(output_path).stat().st_size} bytes")
81+
82+
# %%
83+
# Let's verify that both streams are present in the output file:
84+
85+
result = subprocess.run(
86+
[
87+
"ffprobe", "-v", "error",
88+
"-show_entries", "stream=index,codec_type,codec_name",
89+
"-of", "default=noprint_wrappers=1", output_path,
90+
],
91+
capture_output=True, text=True,
92+
)
93+
print(result.stdout)
94+
95+
# %%
96+
# Incremental encoding
97+
# ---------------------
98+
#
99+
# You don't need to have all your data ready upfront. You can call
100+
# :meth:`~torchcodec.encoders.VideoStream.add_frames` and
101+
# :meth:`~torchcodec.encoders.AudioStream.add_samples` multiple times to feed
102+
# data incrementally. This is useful when frames or samples are generated
103+
# on-the-fly (e.g. from a model or a processing pipeline).
104+
#
105+
# Here, we'll split our frames and audio into chunks and feed them one batch at
106+
# a time:
107+
108+
chunk_output = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
109+
encoder = Encoder()
110+
video_stream = encoder.add_video(
111+
height=frames.shape[2], width=frames.shape[3], frame_rate=frame_rate,
112+
)
113+
audio_stream = encoder.add_audio(sample_rate=audio_sample_rate, num_channels=1)
114+
115+
video_chunk_size = 10
116+
samples_per_video_chunk = int(audio_sample_rate / frame_rate * video_chunk_size)
117+
118+
with encoder.open_file(chunk_output):
119+
for i in range(0, len(frames), video_chunk_size):
120+
video_chunk = frames[i : i + video_chunk_size]
121+
video_stream.add_frames(video_chunk)
122+
123+
audio_start = int(i / frame_rate * audio_sample_rate)
124+
audio_chunk = audio_samples[:, audio_start : audio_start + samples_per_video_chunk]
125+
audio_stream.add_samples(audio_chunk)
126+
127+
print(f"Incrementally encoded to {chunk_output}")
128+
print(f"Output size: {Path(chunk_output).stat().st_size} bytes")
129+
130+
# %%
131+
# Multiple video streams, multiple audio streams
132+
# ------------------------------------------------
133+
#
134+
# You can add as many video and audio streams as you need. Each video stream can
135+
# independently target CPU or CUDA encoding — just pass the desired ``device``
136+
# to :meth:`~torchcodec.encoders.Encoder.add_video`. This means you can mix CPU
137+
# and CUDA video streams in the same container, for example encoding a
138+
# high-resolution stream on GPU for speed and a low-resolution stream on CPU.
139+
#
140+
# Similarly, you can add multiple audio streams with different settings (sample
141+
# rate, number of channels, bit rate, etc.).
142+
#
143+
# Here's an example with two video streams and two audio streams:
144+
#
145+
# .. code-block:: python
146+
#
147+
# encoder = Encoder()
148+
#
149+
# # Two video streams: one on CPU, one on CUDA
150+
# cpu_video = encoder.add_video(
151+
# height=1080, width=1920, frame_rate=30,
152+
# device="cpu",
153+
# )
154+
# cuda_video = encoder.add_video(
155+
# height=720, width=1280, frame_rate=30,
156+
# device="cuda",
157+
# )
158+
#
159+
# # Two audio streams with different settings
160+
# audio_en = encoder.add_audio(sample_rate=44100, num_channels=2)
161+
# audio_fr = encoder.add_audio(sample_rate=44100, num_channels=2)
162+
#
163+
# with encoder.open_file("multi_stream_output.mkv"):
164+
# cpu_video.add_frames(cpu_frames)
165+
# cuda_video.add_frames(cuda_frames)
166+
# audio_en.add_samples(english_samples)
167+
# audio_fr.add_samples(french_samples)
168+
169+
# %%
170+
# Encoding to a file-like object
171+
# --------------------------------
172+
#
173+
# Instead of encoding to a file path, you can encode to any file-like object
174+
# (e.g. ``io.BytesIO()``) using
175+
# :meth:`~torchcodec.encoders.Encoder.open_file_like`. In this case, you must
176+
# specify the container ``format`` explicitly since there is no file extension to
177+
# infer it from.
178+
179+
import io
180+
181+
buf = io.BytesIO()
182+
encoder = Encoder()
183+
video_stream = encoder.add_video(
184+
height=frames.shape[2], width=frames.shape[3], frame_rate=frame_rate,
185+
)
186+
audio_stream = encoder.add_audio(sample_rate=audio_sample_rate, num_channels=1)
187+
188+
with encoder.open_file_like(buf, format="mp4"):
189+
video_stream.add_frames(frames)
190+
audio_stream.add_samples(audio_samples)
191+
192+
encoded_bytes = buf.getvalue()
193+
print(f"Encoded to BytesIO, size: {len(encoded_bytes)} bytes")
194+
195+
# %%

0 commit comments

Comments
 (0)