Skip to content

Commit 7b982fa

Browse files
author
pytorchbot
committed
2026-05-15 nightly release (da5b1cb)
1 parent d7c88aa commit 7b982fa

10 files changed

Lines changed: 1546 additions & 203 deletions

File tree

.github/workflows/cpp_tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
strategy:
2020
fail-fast: false
2121
matrix:
22-
ffmpeg-version-for-tests: ['4.4.2', '5.1.2', '6.1.1', '7.0.1']
22+
ffmpeg-version-for-tests: ['7.0.1']
2323
steps:
2424
- name: Check out repo
2525
uses: actions/checkout@v6

.github/workflows/windows_wheel.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
with-rocm: disable
3737
with-cuda: disable
3838
build-python-only: "disable"
39-
python-versions: '["3.10", "3.11", "3.12", "3.13"]'
39+
python-versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
4040

4141

4242
build:

src/torchcodec/_core/Encoder.cpp

Lines changed: 109 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,7 @@ MultiStreamEncoder::MultiStreamEncoder() {
10601060
}
10611061

10621062
void MultiStreamEncoder::open(std::string_view fileName) {
1063+
STD_TORCH_CHECK(!closed_, "Cannot open after close() was called.");
10631064
STD_TORCH_CHECK(!headerWritten_, "open() was already called.");
10641065

10651066
AVFormatContext* avFormatContext = nullptr;
@@ -1089,6 +1090,7 @@ void MultiStreamEncoder::open(std::string_view fileName) {
10891090
void MultiStreamEncoder::open(
10901091
std::string_view formatName,
10911092
std::unique_ptr<AVIOContextHolder> avioContextHolder) {
1093+
STD_TORCH_CHECK(!closed_, "Cannot open after close() was called.");
10921094
STD_TORCH_CHECK(!headerWritten_, "open() was already called.");
10931095

10941096
avioContextHolder_ = std::move(avioContextHolder);
@@ -1113,7 +1115,7 @@ void MultiStreamEncoder::open(
11131115
openStreamsAndWriteHeader();
11141116
}
11151117

1116-
void MultiStreamEncoder::addVideoStream(
1118+
int MultiStreamEncoder::addVideoStream(
11171119
int height,
11181120
int width,
11191121
double frameRate,
@@ -1123,48 +1125,46 @@ void MultiStreamEncoder::addVideoStream(
11231125
std::optional<double> crf,
11241126
std::optional<std::string> preset,
11251127
std::optional<std::map<std::string, std::string>> extraOptions) {
1126-
STD_TORCH_CHECK(
1127-
!videoStream_.has_value(),
1128-
"A video stream has already been added. Cannot add another.");
11291128
STD_TORCH_CHECK(height > 0, "height must be > 0, got ", height);
11301129
STD_TORCH_CHECK(width > 0, "width must be > 0, got ", width);
11311130
STD_TORCH_CHECK(frameRate > 0, "frame_rate must be > 0, got ", frameRate);
1132-
videoStream_ = VideoStream{};
1131+
VideoStream videoStream;
11331132
StableDevice stableDevice(std::move(device));
1134-
// The NVDEC CUDA interface is decode-only; encoders need the FFmpeg-based
1135-
// one.
1136-
videoStream_->deviceInterface = createDeviceInterface(
1133+
videoStream.deviceInterface = createDeviceInterface(
11371134
stableDevice, stableDevice.type() == kStableCUDA ? "ffmpeg" : "default");
1138-
videoStream_->inHeight = height;
1139-
videoStream_->inWidth = width;
1140-
videoStream_->inFrameRate = frameRate;
1141-
videoStream_->options.codec = std::move(codec);
1142-
videoStream_->options.pixelFormat = std::move(pixelFormat);
1143-
videoStream_->options.crf = crf;
1144-
videoStream_->options.preset = std::move(preset);
1145-
videoStream_->options.extraOptions = std::move(extraOptions);
1135+
videoStream.inHeight = height;
1136+
videoStream.inWidth = width;
1137+
videoStream.inFrameRate = frameRate;
1138+
videoStream.options.codec = std::move(codec);
1139+
videoStream.options.pixelFormat = std::move(pixelFormat);
1140+
videoStream.options.crf = crf;
1141+
videoStream.options.preset = std::move(preset);
1142+
videoStream.options.extraOptions = std::move(extraOptions);
1143+
videoStreams_.push_back(std::move(videoStream));
1144+
return static_cast<int>(videoStreams_.size() - 1);
11461145
}
11471146

1148-
void MultiStreamEncoder::addAudioStream(
1147+
int MultiStreamEncoder::addAudioStream(
11491148
int sampleRate,
11501149
int numChannels,
1151-
std::optional<int> bitRate) {
1152-
STD_TORCH_CHECK(
1153-
!audioStream_.has_value(),
1154-
"An audio stream has already been added. Cannot add another.");
1150+
std::optional<int> bitRate,
1151+
std::optional<int> outNumChannels,
1152+
std::optional<int> outSampleRate) {
11551153
STD_TORCH_CHECK(sampleRate > 0, "sample_rate must be > 0, got ", sampleRate);
11561154
STD_TORCH_CHECK(
11571155
numChannels > 0, "num_channels must be > 0, got ", numChannels);
11581156

1159-
audioStream_ = AudioStream{};
1160-
audioStream_->inSampleRate = sampleRate;
1161-
audioStream_->inNumChannels = numChannels;
1162-
audioStream_->options.bitRate = bitRate;
1157+
AudioStream audioStream;
1158+
audioStream.inSampleRate = sampleRate;
1159+
audioStream.inNumChannels = numChannels;
1160+
audioStream.options.bitRate = bitRate;
1161+
audioStream.options.numChannels = outNumChannels;
1162+
audioStream.options.sampleRate = outSampleRate;
1163+
audioStreams_.push_back(std::move(audioStream));
1164+
return static_cast<int>(audioStreams_.size() - 1);
11631165
}
11641166

1165-
void MultiStreamEncoder::initializeVideoStream() {
1166-
// TODO MultiStreamEncoder: Iterate over all video streams
1167-
auto& videoStream = *videoStream_;
1167+
void MultiStreamEncoder::initializeVideoStream(VideoStream& videoStream) {
11681168
auto deviceType = videoStream.deviceInterface->device().type();
11691169

11701170
const AVCodec* avCodec = nullptr;
@@ -1318,8 +1318,7 @@ void MultiStreamEncoder::initializeVideoStream() {
13181318
getFFMPEGErrorStringFromErrorCode(status));
13191319
}
13201320

1321-
void MultiStreamEncoder::initializeAudioStream() {
1322-
auto& audioStream = *audioStream_;
1321+
void MultiStreamEncoder::initializeAudioStream(AudioStream& audioStream) {
13231322
// We use the AVFormatContext's default codec for that
13241323
// specific format/container.
13251324
const AVCodec* avCodec =
@@ -1340,15 +1339,18 @@ void MultiStreamEncoder::initializeAudioStream() {
13401339
// well when "-b:a" isn't specified.
13411340
audioStream.avCodecContext->bit_rate = desiredBitRate.value_or(0);
13421341

1343-
// TODO MultiStreamEncoder: support output numChannels and sampleRate
1344-
validateNumChannels(*avCodec, audioStream.inNumChannels);
1345-
setDefaultChannelLayout(
1346-
audioStream.avCodecContext, audioStream.inNumChannels);
1342+
int outNumChannels =
1343+
audioStream.options.numChannels.value_or(audioStream.inNumChannels);
1344+
audioStream.outNumChannels = outNumChannels;
1345+
validateNumChannels(*avCodec, outNumChannels);
1346+
setDefaultChannelLayout(audioStream.avCodecContext, outNumChannels);
13471347

1348-
validateSampleRate(*avCodec, audioStream.inSampleRate);
1349-
audioStream.avCodecContext->sample_rate = audioStream.inSampleRate;
1350-
audioStream.avCodecContext->time_base =
1351-
AVRational{1, audioStream.inSampleRate};
1348+
int outSampleRate =
1349+
audioStream.options.sampleRate.value_or(audioStream.inSampleRate);
1350+
audioStream.outSampleRate = outSampleRate;
1351+
validateSampleRate(*avCodec, outSampleRate);
1352+
audioStream.avCodecContext->sample_rate = outSampleRate;
1353+
audioStream.avCodecContext->time_base = AVRational{1, outSampleRate};
13521354

13531355
// Input samples are expected to be FLTP. Not all encoders support FLTP, so we
13541356
// may need to convert the samples into a supported output sample format,
@@ -1387,22 +1389,22 @@ void MultiStreamEncoder::initializeAudioStream() {
13871389
// sized batches.
13881390
auto avAudioFifo = av_audio_fifo_alloc(
13891391
audioStream.avCodecContext->sample_fmt,
1390-
audioStream.inNumChannels,
1392+
outNumChannels,
13911393
audioStream.frameSize * 2);
13921394
STD_TORCH_CHECK(avAudioFifo != nullptr, "Couldn't create AVAudioFifo.");
13931395
audioStream.avAudioFifo.reset(avAudioFifo);
13941396
}
13951397

13961398
void MultiStreamEncoder::openStreamsAndWriteHeader() {
13971399
STD_TORCH_CHECK(
1398-
videoStream_.has_value() || audioStream_.has_value(),
1400+
!videoStreams_.empty() || !audioStreams_.empty(),
13991401
"Call addVideoStream() or addAudioStream() before open().");
14001402

1401-
if (videoStream_.has_value()) {
1402-
initializeVideoStream();
1403+
for (auto& videoStream : videoStreams_) {
1404+
initializeVideoStream(videoStream);
14031405
}
1404-
if (audioStream_.has_value()) {
1405-
initializeAudioStream();
1406+
for (auto& audioStream : audioStreams_) {
1407+
initializeAudioStream(audioStream);
14061408
}
14071409

14081410
int status = avformat_write_header(
@@ -1414,49 +1416,60 @@ void MultiStreamEncoder::openStreamsAndWriteHeader() {
14141416
headerWritten_ = true;
14151417
}
14161418

1417-
void MultiStreamEncoder::addFrames(const torch::stable::Tensor& frames) {
1419+
void MultiStreamEncoder::addFrames(
1420+
const torch::stable::Tensor& frames,
1421+
int streamIndex) {
14181422
// TODO MultiStreamEncoder: Specify which video stream to add frames to
1423+
STD_TORCH_CHECK(!closed_, "Cannot add frames after close() was called.");
14191424
STD_TORCH_CHECK(headerWritten_, "Call open() before addFrames().");
1425+
STD_TORCH_CHECK(
1426+
streamIndex >= 0 && streamIndex < static_cast<int>(videoStreams_.size()),
1427+
"Invalid stream index ",
1428+
streamIndex,
1429+
". Number of video streams: ",
1430+
videoStreams_.size());
1431+
auto& videoStream = videoStreams_[streamIndex];
14201432
auto validatedFrames = validateFrames(
14211433
frames,
1422-
videoStream_->avCodecContext.get(),
1423-
videoStream_->deviceInterface.get());
1434+
videoStream.avCodecContext.get(),
1435+
videoStream.deviceInterface.get());
14241436

14251437
AutoAVPacket autoAVPacket;
14261438
// TODO MultiStreamEncoder: Consider using accessor for potential performance
14271439
// improvement
14281440
int numFrames = static_cast<int>(validatedFrames.sizes()[0]);
14291441
for (int i = 0; i < numFrames; ++i) {
14301442
torch::stable::Tensor currFrame = selectRow(validatedFrames, i);
1431-
int frameIndex = videoStream_->numEncodedFrames + i;
1443+
int frameIndex = videoStream.numEncodedFrames + i;
14321444
UniqueAVFrame avFrame =
1433-
videoStream_->deviceInterface->convertTensorToAVFrameForEncoding(
1434-
currFrame, frameIndex, videoStream_->avCodecContext.get());
1445+
videoStream.deviceInterface->convertTensorToAVFrameForEncoding(
1446+
currFrame, frameIndex, videoStream.avCodecContext.get());
14351447
STD_TORCH_CHECK(
14361448
avFrame != nullptr,
14371449
"convertTensorToAVFrameForEncoding failed for frame ",
14381450
frameIndex,
14391451
" on device: ",
14401452
deviceTypeName(validatedFrames.device().type()));
1441-
encodeVideoFrame(autoAVPacket, avFrame);
1453+
encodeVideoFrame(autoAVPacket, avFrame, videoStream);
14421454
}
1443-
videoStream_->numEncodedFrames += numFrames;
1455+
videoStream.numEncodedFrames += numFrames;
14441456
}
14451457

14461458
void MultiStreamEncoder::encodeVideoFrame(
14471459
AutoAVPacket& autoAVPacket,
1448-
const UniqueAVFrame& avFrame) {
1460+
const UniqueAVFrame& avFrame,
1461+
VideoStream& videoStream) {
14491462
auto status =
1450-
avcodec_send_frame(videoStream_->avCodecContext.get(), avFrame.get());
1463+
avcodec_send_frame(videoStream.avCodecContext.get(), avFrame.get());
14511464
STD_TORCH_CHECK(
14521465
status == AVSUCCESS,
14531466
"Error while sending frame: ",
14541467
getFFMPEGErrorStringFromErrorCode(status));
14551468

14561469
while (status >= 0) {
14571470
ReferenceAVPacket packet(autoAVPacket);
1458-
status = avcodec_receive_packet(
1459-
videoStream_->avCodecContext.get(), packet.get());
1471+
status =
1472+
avcodec_receive_packet(videoStream.avCodecContext.get(), packet.get());
14601473
if (status == AVERROR(EAGAIN) || status == AVERROR_EOF) {
14611474
if (status == AVERROR_EOF) {
14621475
// Flush remaining buffered packets
@@ -1482,9 +1495,9 @@ void MultiStreamEncoder::encodeVideoFrame(
14821495
}
14831496
av_packet_rescale_ts(
14841497
packet.get(),
1485-
videoStream_->avCodecContext->time_base,
1486-
videoStream_->avStream->time_base);
1487-
packet->stream_index = videoStream_->avStream->index;
1498+
videoStream.avCodecContext->time_base,
1499+
videoStream.avStream->time_base);
1500+
packet->stream_index = videoStream.avStream->index;
14881501

14891502
status = av_interleaved_write_frame(avFormatContext_.get(), packet.get());
14901503
STD_TORCH_CHECK(
@@ -1494,26 +1507,32 @@ void MultiStreamEncoder::encodeVideoFrame(
14941507
}
14951508
}
14961509

1497-
void MultiStreamEncoder::addSamples(const torch::stable::Tensor& samples) {
1510+
void MultiStreamEncoder::addSamples(
1511+
const torch::stable::Tensor& samples,
1512+
int streamIndex) {
1513+
STD_TORCH_CHECK(!closed_, "Cannot add samples after close() was called.");
14981514
STD_TORCH_CHECK(headerWritten_, "Call open() before addSamples().");
14991515
STD_TORCH_CHECK(
1500-
audioStream_.has_value(),
1501-
"No audio stream has been added. Call addAudioStream() first.");
1516+
streamIndex >= 0 && streamIndex < static_cast<int>(audioStreams_.size()),
1517+
"Invalid stream index ",
1518+
streamIndex,
1519+
". Number of audio streams: ",
1520+
audioStreams_.size());
1521+
auto& audioStream = audioStreams_[streamIndex];
15021522
auto validatedSamples = validateSamples(samples);
15031523
STD_TORCH_CHECK(
15041524
static_cast<int>(validatedSamples.sizes()[0]) ==
1505-
audioStream_->inNumChannels,
1525+
audioStream.inNumChannels,
15061526
"Expected ",
1507-
audioStream_->inNumChannels,
1527+
audioStream.inNumChannels,
15081528
" channels, got ",
15091529
validatedSamples.sizes()[0]);
1510-
encodeAudioSamples(validatedSamples);
1530+
encodeAudioSamples(validatedSamples, audioStream);
15111531
}
15121532

15131533
void MultiStreamEncoder::encodeAudioSamples(
1514-
const torch::stable::Tensor& samples) {
1515-
auto& audioStream = *audioStream_;
1516-
1534+
const torch::stable::Tensor& samples,
1535+
AudioStream& audioStream) {
15171536
UniqueAVFrame avFrame = allocateAVFrame(
15181537
audioStream.frameSize,
15191538
audioStream.inSampleRate,
@@ -1564,8 +1583,8 @@ UniqueAVFrame MultiStreamEncoder::maybeConvertAudioAVFrame(
15641583
AudioStream& audioStream) {
15651584
if (static_cast<AVSampleFormat>(avFrame->format) ==
15661585
audioStream.avCodecContext->sample_fmt &&
1567-
getNumChannels(avFrame) == audioStream.inNumChannels &&
1568-
avFrame->sample_rate == audioStream.inSampleRate) {
1586+
getNumChannels(avFrame) == audioStream.outNumChannels &&
1587+
avFrame->sample_rate == audioStream.outSampleRate) {
15691588
// Note: the clone references the same underlying data, it's a cheap copy.
15701589
return UniqueAVFrame(av_frame_clone(avFrame.get()));
15711590
}
@@ -1575,9 +1594,9 @@ UniqueAVFrame MultiStreamEncoder::maybeConvertAudioAVFrame(
15751594
static_cast<AVSampleFormat>(avFrame->format),
15761595
audioStream.avCodecContext->sample_fmt,
15771596
avFrame->sample_rate,
1578-
audioStream.inSampleRate,
1597+
audioStream.outSampleRate,
15791598
avFrame,
1580-
audioStream.inNumChannels));
1599+
audioStream.outNumChannels));
15811600
}
15821601
// convertAudioAVFrameSamples uses avFrame's extended_data field, so we ensure
15831602
// it's the same as data. This should always be the case since we validated
@@ -1589,10 +1608,10 @@ UniqueAVFrame MultiStreamEncoder::maybeConvertAudioAVFrame(
15891608
audioStream.swrContext,
15901609
avFrame,
15911610
audioStream.avCodecContext->sample_fmt,
1592-
audioStream.inSampleRate,
1593-
audioStream.inNumChannels);
1611+
audioStream.outSampleRate,
1612+
audioStream.outNumChannels);
15941613

1595-
if (avFrame->sample_rate == audioStream.inSampleRate) {
1614+
if (avFrame->sample_rate == audioStream.outSampleRate) {
15961615
STD_TORCH_CHECK(
15971616
convertedAVFrame->nb_samples == avFrame->nb_samples,
15981617
"convertedAVFrame->nb_samples=",
@@ -1629,7 +1648,7 @@ void MultiStreamEncoder::encodeAudioFrameThroughFifo(
16291648
UniqueAVFrame newavFrame = allocateAVFrame(
16301649
audioStream.frameSize,
16311650
audioStream.avCodecContext->sample_rate,
1632-
audioStream.inNumChannels,
1651+
audioStream.outNumChannels,
16331652
audioStream.avCodecContext->sample_fmt);
16341653

16351654
// Explaining the while bound:
@@ -1730,8 +1749,8 @@ void MultiStreamEncoder::maybeFlushSwrAndFifo(
17301749
if (numRemainingSamples > 0) {
17311750
swrFrame = allocateAVFrame(
17321751
numRemainingSamples,
1733-
audioStream.inSampleRate,
1734-
audioStream.inNumChannels,
1752+
audioStream.outSampleRate,
1753+
audioStream.outNumChannels,
17351754
audioStream.avCodecContext->sample_fmt);
17361755
int actualNumRemainingSamples = swr_convert(
17371756
audioStream.swrContext.get(),
@@ -1749,16 +1768,18 @@ void MultiStreamEncoder::maybeFlushSwrAndFifo(
17491768
}
17501769

17511770
void MultiStreamEncoder::flushBuffers() {
1752-
if (audioStream_.has_value() && audioStream_->avStream != nullptr) {
1753-
AutoAVPacket audioAVPacket;
1754-
auto& audioStream = *audioStream_;
1755-
maybeFlushSwrAndFifo(audioAVPacket, audioStream);
1756-
encodeAudioFrame(audioAVPacket, UniqueAVFrame(nullptr), audioStream);
1757-
}
1758-
if (videoStream_.has_value() && videoStream_->avStream != nullptr) {
1759-
AutoAVPacket videoAVPacket;
1760-
// Send null frame to signal end of input
1761-
encodeVideoFrame(videoAVPacket, UniqueAVFrame(nullptr));
1771+
for (auto& audioStream : audioStreams_) {
1772+
if (audioStream.avStream != nullptr) {
1773+
AutoAVPacket audioAVPacket;
1774+
maybeFlushSwrAndFifo(audioAVPacket, audioStream);
1775+
encodeAudioFrame(audioAVPacket, UniqueAVFrame(nullptr), audioStream);
1776+
}
1777+
}
1778+
for (auto& videoStream : videoStreams_) {
1779+
if (videoStream.avStream != nullptr) {
1780+
AutoAVPacket videoAVPacket;
1781+
encodeVideoFrame(videoAVPacket, UniqueAVFrame(nullptr), videoStream);
1782+
}
17621783
}
17631784
}
17641785

0 commit comments

Comments
 (0)