Skip to content

Commit 59dcf12

Browse files
committed
Add basic Python API for streaming encoder
1 parent c70d65d commit 59dcf12

2 files changed

Lines changed: 445 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from pathlib import Path
2+
from typing import Any
3+
4+
import torch
5+
from torch import Tensor
6+
7+
from torchcodec import _core
8+
9+
10+
class _VideoStream:
11+
def __init__(self, encoder_tensor: Tensor):
12+
self._encoder_tensor = encoder_tensor
13+
14+
def write(self, frames: Tensor) -> None:
15+
if frames.ndim == 3:
16+
frames = frames.unsqueeze(0)
17+
if frames.ndim != 4:
18+
raise ValueError(f"Expected 3D or 4D frames, got {frames.shape = }.")
19+
if frames.dtype != torch.uint8:
20+
raise ValueError(f"Expected uint8 frames, got {frames.dtype = }.")
21+
_core.streaming_encoder_add_frames(self._encoder_tensor, frames)
22+
23+
24+
class _AudioStream:
25+
def __init__(self, encoder_tensor: Tensor):
26+
self._encoder_tensor = encoder_tensor
27+
28+
def write(self, samples: Tensor) -> None:
29+
if samples.ndim == 1:
30+
samples = samples.unsqueeze(0)
31+
if samples.ndim != 2:
32+
raise ValueError(f"Expected 1D or 2D samples, got {samples.shape = }.")
33+
if samples.dtype != torch.float32:
34+
raise ValueError(f"Expected float32 samples, got {samples.dtype = }.")
35+
_core.streaming_encoder_add_samples(self._encoder_tensor, samples)
36+
37+
38+
class StreamingEncoder:
39+
def __init__(self, dest: str | Path):
40+
self._encoder_tensor = _core.create_streaming_encoder_to_file(str(dest))
41+
self._opened = False
42+
self._closed = False
43+
44+
def add_video(
45+
self,
46+
*,
47+
height: int,
48+
width: int,
49+
frame_rate: float,
50+
device: str = "cpu",
51+
codec: str | None = None,
52+
pixel_format: str | None = None,
53+
crf: int | float | None = None,
54+
preset: str | int | None = None,
55+
extra_options: dict[str, Any] | None = None,
56+
) -> _VideoStream:
57+
if self._opened:
58+
raise RuntimeError("Cannot add streams after open() has been called.")
59+
preset = str(preset) if isinstance(preset, int) else preset
60+
_core.streaming_encoder_add_video_stream(
61+
self._encoder_tensor,
62+
height=height,
63+
width=width,
64+
frame_rate=frame_rate,
65+
device=device,
66+
codec=codec,
67+
pixel_format=pixel_format,
68+
crf=crf,
69+
preset=preset,
70+
extra_options=[
71+
str(x) for k, v in (extra_options or {}).items() for x in (k, v)
72+
],
73+
)
74+
return _VideoStream(self._encoder_tensor)
75+
76+
def add_audio(
77+
self,
78+
*,
79+
sample_rate: int,
80+
num_channels: int,
81+
bit_rate: int | None = None,
82+
) -> _AudioStream:
83+
if self._opened:
84+
raise RuntimeError("Cannot add streams after open() has been called.")
85+
_core.streaming_encoder_add_audio_stream(
86+
self._encoder_tensor,
87+
sample_rate=sample_rate,
88+
num_channels=num_channels,
89+
bit_rate=bit_rate,
90+
)
91+
return _AudioStream(self._encoder_tensor)
92+
93+
def open(self) -> None:
94+
if self._opened:
95+
raise RuntimeError("Encoder is already open.")
96+
self._opened = True
97+
_core.streaming_encoder_open(self._encoder_tensor)
98+
99+
def close(self) -> None:
100+
if self._closed:
101+
return
102+
self._closed = True
103+
_core.streaming_encoder_close(self._encoder_tensor)

0 commit comments

Comments
 (0)