Skip to content

Commit bc1f739

Browse files
author
Jeremy Teboul
committed
Fix floating-point precision validation in timestamp checks
Resolves floating-point precision errors in video processing workflows that compute timestamps subject to small arithmetic precision loss. - Fixes validation failures 'frame pts is 0.033000; must be greater than or equal to 0.033000' Example: ChainedTransform: 5.00 % of reads+writes failed (5/100+0). Error reason counts (max 100): 'frame pts is 0.033000; must be greater than or equal to 0.033000.':2, 'frame pts is 0.033033; must be greater than or equal to 0.033033.':2, 'frame pts is 0.040000; must be greater than or equal to 0.040000.':1. Sample errors: (SampleNFramesWithFps(tensor([ 0, 0, 0, ..., 49, 48, 48], dtype=torch.uint8)): 'frame pts is 0.033000; must be greater than or equal to 0.033000.',SampleNFramesWithFps(tensor([ 0, 0, 0, ..., 49, 48, 48], dtype=torch.uint8)): 'frame pts is 0.033033; must be greater than or equal to 0.033033.',SampleNFramesWithFps(tensor([ 0, 0, 0, ..., 49, 48, 48], dtype=torch.uint8)): 'frame pts is 0.033000; must be greater than or equal to 0.033000.',SampleNFramesWithFps(tensor([ 0, 0, 0, ..., 49, 48, 48], dtype=torch.uint8)): 'frame pts is 0.033033; must be greater than or equal to 0.033033.',SampleNFramesWithFps(tensor([ 0, 0, 0, ..., 49, 48, 48], dtype=torch.uint8)): 'frame pts is 0.040000; must be greater than or equal to 0.040000.'). - Applied to both getFramesPlayedAt() and getFramesPlayedInRange() methods - Add C++ test cases for precision error scenarios
1 parent e5b2eef commit bc1f739

File tree

2 files changed

+189
-2
lines changed

2 files changed

+189
-2
lines changed

src/torchcodec/_core/SingleStreamDecoder.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -785,8 +785,10 @@ FrameBatchOutput SingleStreamDecoder::getFramesPlayedAt(
785785

786786
for (int64_t i = 0; i < timestamps.numel(); ++i) {
787787
auto frameSeconds = timestampsAccessor[i];
788+
// Use machine epsilon scaled for video processing precision errors
789+
constexpr double eps = std::numeric_limits<double>::epsilon() * 1000;
788790
TORCH_CHECK(
789-
frameSeconds >= minSeconds,
791+
frameSeconds >= (minSeconds - eps),
790792
"frame pts is " + std::to_string(frameSeconds) +
791793
"; must be greater than or equal to " + std::to_string(minSeconds) +
792794
".");
@@ -795,7 +797,7 @@ FrameBatchOutput SingleStreamDecoder::getFramesPlayedAt(
795797
// metadata, then we assume the frame's pts is valid.
796798
if (maxSeconds.has_value()) {
797799
TORCH_CHECK(
798-
frameSeconds < maxSeconds.value(),
800+
frameSeconds < (maxSeconds.value() + eps),
799801
"frame pts is " + std::to_string(frameSeconds) +
800802
"; must be less than " + std::to_string(maxSeconds.value()) +
801803
".");

test/VideoDecoderTest.cpp

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,191 @@ TEST_P(SingleStreamDecoderTest, GetAudioMetadata) {
441441
EXPECT_NEAR(*audioStream.durationSecondsFromHeader, 13.25, 1e-1);
442442
}
443443

444+
TEST_P(SingleStreamDecoderTest, FloatingPointPrecisionExactTimestampsWork) {
445+
std::string path = getResourcePath("nasa_13013.mp4");
446+
447+
std::unique_ptr<SingleStreamDecoder> ourDecoder =
448+
std::make_unique<SingleStreamDecoder>(
449+
path, SingleStreamDecoder::SeekMode::exact);
450+
ourDecoder->scanFileAndUpdateMetadataAndIndex();
451+
std::vector<Transform*> transforms;
452+
ourDecoder->addVideoStream(-1, transforms);
453+
454+
ContainerMetadata metadata = ourDecoder->getContainerMetadata();
455+
const auto& videoStream = metadata.allStreamMetadata[3];
456+
457+
EXPECT_TRUE(videoStream.beginStreamPtsSecondsFromContent.has_value());
458+
double minSeconds = videoStream.beginStreamPtsSecondsFromContent.value();
459+
460+
EXPECT_NO_THROW({
461+
auto timestamps = torch::tensor({minSeconds}, torch::dtype(torch::kDouble));
462+
auto output = ourDecoder->getFramesPlayedAt(timestamps);
463+
EXPECT_EQ(output.data.size(0), 1);
464+
});
465+
}
466+
467+
TEST_P(SingleStreamDecoderTest, FloatingPointPrecisionSmallEpsilonErrorsWork) {
468+
std::string path = getResourcePath("nasa_13013.mp4");
469+
470+
std::unique_ptr<SingleStreamDecoder> ourDecoder =
471+
std::make_unique<SingleStreamDecoder>(
472+
path, SingleStreamDecoder::SeekMode::exact);
473+
ourDecoder->scanFileAndUpdateMetadataAndIndex();
474+
std::vector<Transform*> transforms;
475+
ourDecoder->addVideoStream(-1, transforms);
476+
477+
ContainerMetadata metadata = ourDecoder->getContainerMetadata();
478+
const auto& videoStream = metadata.allStreamMetadata[3];
479+
480+
EXPECT_TRUE(videoStream.beginStreamPtsSecondsFromContent.has_value());
481+
double minSeconds = videoStream.beginStreamPtsSecondsFromContent.value();
482+
483+
// Simulate small floating-point precision errors from video processing
484+
constexpr double eps = std::numeric_limits<double>::epsilon();
485+
double almostMinSeconds = minSeconds - eps * 100;
486+
487+
EXPECT_NO_THROW({
488+
auto timestamps =
489+
torch::tensor({almostMinSeconds}, torch::dtype(torch::kDouble));
490+
auto output = ourDecoder->getFramesPlayedAt(timestamps);
491+
EXPECT_EQ(output.data.size(0), 1);
492+
});
493+
}
494+
495+
TEST_P(SingleStreamDecoderTest, FloatingPointPrecisionLargerEpsilonErrorsWork) {
496+
std::string path = getResourcePath("nasa_13013.mp4");
497+
498+
std::unique_ptr<SingleStreamDecoder> ourDecoder =
499+
std::make_unique<SingleStreamDecoder>(
500+
path, SingleStreamDecoder::SeekMode::exact);
501+
ourDecoder->scanFileAndUpdateMetadataAndIndex();
502+
std::vector<Transform*> transforms;
503+
ourDecoder->addVideoStream(-1, transforms);
504+
505+
ContainerMetadata metadata = ourDecoder->getContainerMetadata();
506+
const auto& videoStream = metadata.allStreamMetadata[3];
507+
508+
EXPECT_TRUE(videoStream.beginStreamPtsSecondsFromContent.has_value());
509+
double minSeconds = videoStream.beginStreamPtsSecondsFromContent.value();
510+
511+
constexpr double eps = std::numeric_limits<double>::epsilon();
512+
double precisionErrorSeconds = minSeconds - eps * 500;
513+
514+
EXPECT_NO_THROW({
515+
auto timestamps =
516+
torch::tensor({precisionErrorSeconds}, torch::dtype(torch::kDouble));
517+
auto output = ourDecoder->getFramesPlayedAt(timestamps);
518+
EXPECT_EQ(output.data.size(0), 1);
519+
});
520+
}
521+
522+
TEST_P(
523+
SingleStreamDecoderTest,
524+
FloatingPointPrecisionInvalidTimestampsStillFail) {
525+
std::string path = getResourcePath("nasa_13013.mp4");
526+
527+
std::unique_ptr<SingleStreamDecoder> ourDecoder =
528+
std::make_unique<SingleStreamDecoder>(
529+
path, SingleStreamDecoder::SeekMode::exact);
530+
ourDecoder->scanFileAndUpdateMetadataAndIndex();
531+
std::vector<Transform*> transforms;
532+
ourDecoder->addVideoStream(-1, transforms);
533+
534+
ContainerMetadata metadata = ourDecoder->getContainerMetadata();
535+
const auto& videoStream = metadata.allStreamMetadata[3];
536+
537+
EXPECT_TRUE(videoStream.beginStreamPtsSecondsFromContent.has_value());
538+
double minSeconds = videoStream.beginStreamPtsSecondsFromContent.value();
539+
540+
// Ensure genuinely invalid timestamps still fail appropriately
541+
double definitelyInvalidTimestamp = minSeconds - 0.1;
542+
543+
EXPECT_THROW(
544+
{
545+
auto timestamps = torch::tensor(
546+
{definitelyInvalidTimestamp}, torch::dtype(torch::kDouble));
547+
ourDecoder->getFramesPlayedAt(timestamps);
548+
},
549+
c10::Error);
550+
}
551+
552+
TEST_P(
553+
SingleStreamDecoderTest,
554+
FloatingPointPrecisionBatchTimestampsWithEpsilonErrorsWork) {
555+
std::string path = getResourcePath("nasa_13013.mp4");
556+
557+
std::unique_ptr<SingleStreamDecoder> ourDecoder =
558+
std::make_unique<SingleStreamDecoder>(
559+
path, SingleStreamDecoder::SeekMode::exact);
560+
ourDecoder->scanFileAndUpdateMetadataAndIndex();
561+
std::vector<Transform*> transforms;
562+
ourDecoder->addVideoStream(-1, transforms);
563+
564+
ContainerMetadata metadata = ourDecoder->getContainerMetadata();
565+
const auto& videoStream = metadata.allStreamMetadata[3];
566+
567+
EXPECT_TRUE(videoStream.beginStreamPtsSecondsFromContent.has_value());
568+
double minSeconds = videoStream.beginStreamPtsSecondsFromContent.value();
569+
570+
constexpr double eps = std::numeric_limits<double>::epsilon();
571+
auto mixedTimestamps = torch::tensor(
572+
{minSeconds, minSeconds - eps * 10, minSeconds + eps * 50},
573+
torch::dtype(torch::kDouble));
574+
575+
EXPECT_NO_THROW({
576+
auto output = ourDecoder->getFramesPlayedAt(mixedTimestamps);
577+
EXPECT_EQ(output.data.size(0), 3);
578+
});
579+
}
580+
581+
TEST_P(SingleStreamDecoderTest, HandleFloatingPointPrecisionInRangeValidation) {
582+
std::string path = getResourcePath("nasa_13013.mp4");
583+
584+
// Set exact seek mode to enable precise timestamp validation
585+
std::unique_ptr<SingleStreamDecoder> ourDecoder =
586+
std::make_unique<SingleStreamDecoder>(
587+
path, SingleStreamDecoder::SeekMode::exact);
588+
ourDecoder->scanFileAndUpdateMetadataAndIndex();
589+
std::vector<Transform*> transforms;
590+
ourDecoder->addVideoStream(-1, transforms);
591+
592+
// Get the metadata to understand the valid timestamp range
593+
ContainerMetadata metadata = ourDecoder->getContainerMetadata();
594+
const auto& videoStream = metadata.allStreamMetadata[3]; // Video stream index
595+
596+
EXPECT_TRUE(videoStream.beginStreamPtsSecondsFromContent.has_value());
597+
double minSeconds = videoStream.beginStreamPtsSecondsFromContent.value();
598+
599+
// Test case 1: Range starting exactly at minSeconds - should work
600+
EXPECT_NO_THROW({
601+
auto output =
602+
ourDecoder->getFramesPlayedInRange(minSeconds, minSeconds + 1.0);
603+
EXPECT_GT(output.data.size(0), 0);
604+
});
605+
606+
// Test case 2: Range with floating-point precision error at start
607+
constexpr double eps = std::numeric_limits<double>::epsilon();
608+
double startWithPrecisionError = minSeconds - eps * 100;
609+
610+
// This should NOT throw an error with our fix
611+
EXPECT_NO_THROW({
612+
auto output = ourDecoder->getFramesPlayedInRange(
613+
startWithPrecisionError, minSeconds + 1.0);
614+
EXPECT_GT(output.data.size(0), 0);
615+
});
616+
617+
// Test case 3: Test that genuinely invalid range still fails appropriately
618+
double definitelyInvalidStart = minSeconds - 0.1;
619+
620+
// This should still throw an error (not a precision issue)
621+
EXPECT_THROW(
622+
{
623+
ourDecoder->getFramesPlayedInRange(
624+
definitelyInvalidStart, minSeconds + 1.0);
625+
},
626+
c10::Error);
627+
}
628+
444629
INSTANTIATE_TEST_SUITE_P(
445630
FromFileAndMemory,
446631
SingleStreamDecoderTest,

0 commit comments

Comments
 (0)