Skip to content

Commit db0a147

Browse files
authored
Add FPS to get_frames_played_in_range() (#1148)
1 parent 38d286e commit db0a147

6 files changed

Lines changed: 269 additions & 47 deletions

File tree

src/torchcodec/_core/SingleStreamDecoder.cpp

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,8 @@ FrameBatchOutput SingleStreamDecoder::getFramesPlayedAt(
844844

845845
FrameBatchOutput SingleStreamDecoder::getFramesPlayedInRange(
846846
double startSeconds,
847-
double stopSeconds) {
847+
double stopSeconds,
848+
std::optional<double> fps) {
848849
validateActiveStream(AVMEDIA_TYPE_VIDEO);
849850
const auto& streamMetadata =
850851
containerMetadata_.allStreamMetadata[activeStreamIndex_];
@@ -906,36 +907,77 @@ FrameBatchOutput SingleStreamDecoder::getFramesPlayedInRange(
906907
std::to_string(maxSeconds.value()) + ").");
907908
}
908909

909-
// Note that we look at nextPts for a frame, and not its pts or duration.
910-
// Our abstract player displays frames starting at the pts for that frame
911-
// until the pts for the next frame. There are two consequences:
912-
//
913-
// 1. We ignore the duration for a frame. A frame is played until the
914-
// next frame replaces it. This model is robust to durations being 0 or
915-
// incorrect; our source of truth is the pts for frames. If duration is
916-
// accurate, the nextPts for a frame would be equivalent to pts +
917-
// duration.
918-
// 2. In order to establish if the start of an interval maps to a
919-
// particular frame, we need to figure out if it is ordered after the
920-
// frame's pts, but before the next frames's pts.
921-
922-
int64_t startFrameIndex = secondsToIndexLowerBound(startSeconds);
923-
int64_t stopFrameIndex = secondsToIndexUpperBound(stopSeconds);
924-
int64_t numFrames = stopFrameIndex - startFrameIndex;
910+
// Resample frames to match the target frame rate
911+
if (fps.has_value()) {
912+
TORCH_CHECK(
913+
fps.value() > 0,
914+
"fps must be positive, got " + std::to_string(fps.value()));
925915

926-
FrameBatchOutput frameBatchOutput(
927-
numFrames,
928-
resizedOutputDims_.value_or(metadataDims_),
929-
videoStreamOptions.device);
930-
for (int64_t i = startFrameIndex, f = 0; i < stopFrameIndex; ++i, ++f) {
931-
FrameOutput frameOutput =
932-
getFrameAtIndexInternal(i, frameBatchOutput.data[f]);
933-
frameBatchOutput.ptsSeconds[f] = frameOutput.ptsSeconds;
934-
frameBatchOutput.durationSeconds[f] = frameOutput.durationSeconds;
935-
}
936-
frameBatchOutput.data = maybePermuteHWC2CHW(frameBatchOutput.data);
916+
// TODO: add an early break if requested fps is the same as the current fps
937917

938-
return frameBatchOutput;
918+
double fpsVal = fps.value();
919+
double frameDurationSeconds = 1.0 / fpsVal;
920+
921+
double product = (stopSeconds - startSeconds) * fpsVal;
922+
int64_t numOutputFrames = static_cast<int64_t>(std::round(product));
923+
924+
FrameBatchOutput frameBatchOutput(
925+
numOutputFrames,
926+
resizedOutputDims_.value_or(metadataDims_),
927+
videoStreamOptions.device);
928+
929+
// Decode frames, reusing already-decoded frames for duplicates
930+
int64_t lastDecodedSourceIndex = -1;
931+
932+
for (int64_t i = 0; i < numOutputFrames; ++i) {
933+
double targetPtsSeconds = startSeconds + i * frameDurationSeconds;
934+
int64_t sourceIdx = secondsToIndexLowerBound(targetPtsSeconds);
935+
936+
if (sourceIdx == lastDecodedSourceIndex && lastDecodedSourceIndex >= 0) {
937+
frameBatchOutput.data[i].copy_(frameBatchOutput.data[i - 1]);
938+
} else {
939+
getFrameAtIndexInternal(sourceIdx, frameBatchOutput.data[i]);
940+
lastDecodedSourceIndex = sourceIdx;
941+
}
942+
943+
frameBatchOutput.ptsSeconds[i] = targetPtsSeconds;
944+
frameBatchOutput.durationSeconds[i] = frameDurationSeconds;
945+
}
946+
947+
frameBatchOutput.data = maybePermuteHWC2CHW(frameBatchOutput.data);
948+
return frameBatchOutput;
949+
} else {
950+
// Note that we look at nextPts for a frame, and not its pts or duration.
951+
// Our abstract player displays frames starting at the pts for that frame
952+
// until the pts for the next frame. There are two consequences:
953+
//
954+
// 1. We ignore the duration for a frame. A frame is played until the
955+
// next frame replaces it. This model is robust to durations being 0 or
956+
// incorrect; our source of truth is the pts for frames. If duration is
957+
// accurate, the nextPts for a frame would be equivalent to pts +
958+
// duration.
959+
// 2. In order to establish if the start of an interval maps to a
960+
// particular frame, we need to figure out if it is ordered after the
961+
// frame's pts, but before the next frames's pts.
962+
963+
int64_t startFrameIndex = secondsToIndexLowerBound(startSeconds);
964+
int64_t stopFrameIndex = secondsToIndexUpperBound(stopSeconds);
965+
int64_t numFrames = stopFrameIndex - startFrameIndex;
966+
967+
FrameBatchOutput frameBatchOutput(
968+
numFrames,
969+
resizedOutputDims_.value_or(metadataDims_),
970+
videoStreamOptions.device);
971+
for (int64_t i = startFrameIndex, f = 0; i < stopFrameIndex; ++i, ++f) {
972+
FrameOutput frameOutput =
973+
getFrameAtIndexInternal(i, frameBatchOutput.data[f]);
974+
frameBatchOutput.ptsSeconds[f] = frameOutput.ptsSeconds;
975+
frameBatchOutput.durationSeconds[f] = frameOutput.durationSeconds;
976+
}
977+
frameBatchOutput.data = maybePermuteHWC2CHW(frameBatchOutput.data);
978+
979+
return frameBatchOutput;
980+
}
939981
}
940982

941983
// Note [Audio Decoding Design]

src/torchcodec/_core/SingleStreamDecoder.h

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,13 @@ class SingleStreamDecoder {
147147
// Valid values for startSeconds and stopSeconds are:
148148
//
149149
// [beginStreamPtsSecondsFromContent, endStreamPtsSecondsFromContent)
150+
//
151+
// If fps is specified, frames are resampled to match the target frame
152+
// rate by duplicating or dropping frames as necessary.
150153
FrameBatchOutput getFramesPlayedInRange(
151154
double startSeconds,
152-
double stopSeconds);
155+
double stopSeconds,
156+
std::optional<double> fps = std::nullopt);
153157

154158
AudioFramesOutput getFramesPlayedInRangeAudio(
155159
double startSeconds,
@@ -273,11 +277,6 @@ class SingleStreamDecoder {
273277
UniqueAVFrame& avFrame,
274278
std::optional<torch::Tensor> preAllocatedOutputTensor = std::nullopt);
275279

276-
void convertAVFrameToFrameOutputOnCPU(
277-
UniqueAVFrame& avFrame,
278-
FrameOutput& frameOutput,
279-
std::optional<torch::Tensor> preAllocatedOutputTensor = std::nullopt);
280-
281280
// --------------------------------------------------------------------------
282281
// PTS <-> INDEX CONVERSIONS
283282
// --------------------------------------------------------------------------

src/torchcodec/_core/custom_ops.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ TORCH_LIBRARY(torchcodec_ns, m) {
6363
m.def(
6464
"get_frames_in_range(Tensor(a!) decoder, *, int start, int stop, int? step=None) -> (Tensor, Tensor, Tensor)");
6565
m.def(
66-
"get_frames_by_pts_in_range(Tensor(a!) decoder, *, float start_seconds, float stop_seconds) -> (Tensor, Tensor, Tensor)");
66+
"get_frames_by_pts_in_range(Tensor(a!) decoder, *, float start_seconds, float stop_seconds, float? fps=None) -> (Tensor, Tensor, Tensor)");
6767
m.def(
6868
"get_frames_by_pts_in_range_audio(Tensor(a!) decoder, *, float start_seconds, float? stop_seconds) -> (Tensor, Tensor)");
6969
m.def(
@@ -578,13 +578,16 @@ OpsFrameBatchOutput get_frames_by_pts(
578578
// Return the frames inside the range as a single stacked Tensor. The range is
579579
// defined as [start_seconds, stop_seconds). The frames are stacked in pts
580580
// order.
581+
// If fps is specified, frames are resampled to match the target frame
582+
// rate by duplicating or dropping frames as necessary.
581583
OpsFrameBatchOutput get_frames_by_pts_in_range(
582584
torch::Tensor& decoder,
583585
double start_seconds,
584-
double stop_seconds) {
586+
double stop_seconds,
587+
std::optional<double> fps = std::nullopt) {
585588
auto videoDecoder = unwrapTensorToGetDecoder(decoder);
586589
auto result =
587-
videoDecoder->getFramesPlayedInRange(start_seconds, stop_seconds);
590+
videoDecoder->getFramesPlayedInRange(start_seconds, stop_seconds, fps);
588591
return makeOpsFrameBatchOutput(result);
589592
}
590593

src/torchcodec/_core/ops.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ def get_frames_by_pts_in_range_abstract(
535535
*,
536536
start_seconds: float,
537537
stop_seconds: float,
538+
fps: float | None = None,
538539
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
539540
image_size = [get_ctx().new_dynamic_size() for _ in range(4)]
540541
return (

src/torchcodec/decoders/_video_decoder.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import torch
1717
from torch import device as torch_device, nn, Tensor
18-
1918
from torchcodec import _core as core, Frame, FrameBatch
2019
from torchcodec.decoders._decoder_utils import (
2120
_get_cuda_backend,
@@ -453,7 +452,7 @@ def get_frames_played_at(self, seconds: torch.Tensor | list[float]) -> FrameBatc
453452
)
454453

455454
def get_frames_played_in_range(
456-
self, start_seconds: float, stop_seconds: float
455+
self, start_seconds: float, stop_seconds: float, fps: float | None = None
457456
) -> FrameBatch:
458457
"""Returns multiple frames in the given range.
459458
@@ -462,23 +461,26 @@ def get_frames_played_in_range(
462461
range.
463462
464463
Args:
465-
start_seconds (float): Time, in seconds, of the start of the
466-
range.
467-
stop_seconds (float): Time, in seconds, of the end of the
468-
range. As a half open range, the end is excluded.
464+
start_seconds (float): Time, in seconds, of the start of the range.
465+
stop_seconds (float): Time, in seconds, of the end of the range.
466+
As a half open range, the end is excluded.
467+
fps (float, optional): If specified, resample output to this frame
468+
rate by duplicating or dropping frames as necessary. If None
469+
(default), returns frames at the source video's frame rate.
469470
470471
Returns:
471472
FrameBatch: The frames within the specified range.
472473
"""
473474
if not start_seconds <= stop_seconds:
474475
raise ValueError(
475-
f"Invalid start seconds: {start_seconds}. It must be less than or equal to stop seconds ({stop_seconds})."
476+
f"Invalid start seconds: {start_seconds}. "
477+
f"It must be less than or equal to stop seconds ({stop_seconds})."
476478
)
477479
if not self._begin_stream_seconds <= start_seconds < self._end_stream_seconds:
478480
raise ValueError(
479481
f"Invalid start seconds: {start_seconds}. "
480482
f"It must be greater than or equal to {self._begin_stream_seconds} "
481-
f"and less than or equal to {self._end_stream_seconds}."
483+
f"and less than {self._end_stream_seconds}."
482484
)
483485
if not stop_seconds <= self._end_stream_seconds:
484486
raise ValueError(
@@ -489,9 +491,27 @@ def get_frames_played_in_range(
489491
self._decoder,
490492
start_seconds=start_seconds,
491493
stop_seconds=stop_seconds,
494+
fps=fps,
492495
)
493496
return FrameBatch(*frames)
494497

498+
def get_all_frames(self, fps: float | None = None) -> FrameBatch:
499+
"""Returns all frames in the video.
500+
501+
Args:
502+
fps (float, optional): If specified, resample output to this frame
503+
rate by duplicating or dropping frames as necessary. If None
504+
(default), returns frames at the source video's frame rate.
505+
506+
Returns:
507+
FrameBatch: All frames in the video.
508+
"""
509+
return self.get_frames_played_in_range(
510+
start_seconds=self._begin_stream_seconds,
511+
stop_seconds=self._end_stream_seconds,
512+
fps=fps,
513+
)
514+
495515

496516
def _get_and_validate_stream_metadata(
497517
*,

0 commit comments

Comments
 (0)