Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit7ca9d9c

Browse files
authored
Merge pull request#27878 from 0lekW:ffmpeg-negative-dts-seeking-fix
Fix frame seeking with negative DTS values in FFMPEG backend#27878**Merge with extra**:opencv/opencv_extra#1289Fixes#27819Fixes#23472Accompanied by PR onopencv/opencv_extra#1289The FFmpeg backend fails to correctly seek in H.264 videos that contain negative DTS values in their initial frames. This is a valid encoding practice used by modern video encoders (such as DaVinci Resolve's current export) where B-frame reordering causes the first few frames to have negative DTS values.When picture_pts is unavailable (AV_NOPTS_VALUE), the code falls back to using pkt_dts:`picture_pts = packet_raw.pts != AV_NOPTS_VALUE_ ? packet_raw.pts : packet_raw.dts;`If this DTS value is negative (which is legal per H.264 spec), it propagates through the frame number calculation:`frame_number = dts_to_frame_number(picture_pts) - first_frame_number;`This results in negative frame numbers, messing up seeking operations.Solution implemented in this branch is a timestamp normalization similar to FFmpegs -avoid_negative_ts_make_zero flag:- Calculate a global offset once on the first decoded frame by getting the minimum timestamp in either: - Container start_time - Stream start_time - First observed timestamp (PTS, then DTS).- Apply the offset consistently to all timestamps, shifting negative values to begin at 0 while keeping relative timing.- Simplify timestamp converters to remove `start_time` subtractions since timestamps are pre-normalized.This also includes a new test `videoio_ffmpeg.seek_with_negative_dts`This test verifies that seeking behavior performs as expected on a file which has negative DTS values in the first frames. A PR on opencv_extra accompanies this one with that testing file:opencv/opencv_extra#1279```opencv_extra=ffmpeg-videoio-negative-dts-test-data```<cut/>### Pull Request Readiness ChecklistSee details athttps://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request- [x] I agree to contribute to the project under Apache 2 License.- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV- [x] The PR is proposed to the proper branch- [x] There is a reference to the original bug report and related work- [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name.- [x] The feature is well documented and sample code can be built with the project CMake
1 parent33ebced commit7ca9d9c

File tree

2 files changed

+160
-12
lines changed

2 files changed

+160
-12
lines changed

‎modules/videoio/src/cap_ffmpeg_impl.hpp‎

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,17 @@ inline static std::string _opencv_ffmpeg_get_error_string(int error_code)
524524
returnstd::string("Unknown error");
525525
}
526526

527+
staticinlineint64_tto_avtb(int64_t ts, AVRational tb)
528+
{
529+
returnav_rescale_q(ts, tb, AV_TIME_BASE_Q);
530+
}
531+
532+
staticinlineint64_tfrom_avtb(int64_t ts_avtb, AVRational tb)
533+
{
534+
returnav_rescale_q(ts_avtb, AV_TIME_BASE_Q, tb);
535+
}
536+
537+
527538
structCvCapture_FFMPEG
528539
{
529540
boolopen(constchar* filename,int index,const Ptr<IStreamReader>& stream,const VideoCaptureParameters& params);
@@ -563,6 +574,10 @@ struct CvCapture_FFMPEG
563574
int64_t pts_in_fps_time_base;
564575
int64_t dts_delay_in_fps_time_base;
565576

577+
/// Timestamp offset in AV_TIME_BASE units for normalization
578+
int64_t ts_offset_avtb =0;
579+
bool ts_offset_decided =false;
580+
566581
AVIOContext * avio_context;
567582

568583
AVPacket packet;
@@ -623,6 +638,8 @@ void CvCapture_FFMPEG::init()
623638
picture_pts = AV_NOPTS_VALUE_;
624639
pts_in_fps_time_base =0;
625640
dts_delay_in_fps_time_base =0;
641+
ts_offset_avtb =0;
642+
ts_offset_decided =false;
626643
first_frame_number = -1;
627644
memset( &rgb_picture,0,sizeof(rgb_picture) );
628645
memset( &frame,0,sizeof(frame) );
@@ -1705,15 +1722,74 @@ bool CvCapture_FFMPEG::grabFrame()
17051722
if (picture_pts == AV_NOPTS_VALUE_) {
17061723
int64_t dts =0;
17071724
if (!rawMode) {
1708-
picture_pts = picture->CV_FFMPEG_PTS_FIELD != AV_NOPTS_VALUE_ && picture->CV_FFMPEG_PTS_FIELD !=0 ? picture->CV_FFMPEG_PTS_FIELD : picture->pkt_dts;
1709-
if(frame_number ==0) dts = picture->pkt_dts;
1710-
}
1711-
else {
1712-
const AVPacket& packet_raw = packet.data !=0 ? packet : packet_filtered;
1713-
picture_pts = packet_raw.pts != AV_NOPTS_VALUE_ && packet_raw.pts !=0 ? packet_raw.pts : packet_raw.dts;
1725+
picture_pts = (picture->CV_FFMPEG_PTS_FIELD != AV_NOPTS_VALUE_)
1726+
? picture->CV_FFMPEG_PTS_FIELD
1727+
: picture->pkt_dts;
1728+
if (frame_number ==0) dts = picture->pkt_dts;
1729+
}else {
1730+
const AVPacket& packet_raw = (packet.data !=0) ? packet : packet_filtered;
1731+
picture_pts = (packet_raw.pts != AV_NOPTS_VALUE_)
1732+
? packet_raw.pts
1733+
: packet_raw.dts;
17141734
if (frame_number ==0) dts = packet_raw.dts;
1715-
if (picture_pts <0) picture_pts =0;
17161735
}
1736+
1737+
// Decide timestamp offset once on first frame to normalize all timestamps to start at zero.
1738+
// This handles videos with negative DTS values (e.g., from B-frame reordering) or non-zero
1739+
// start_time. Similar to FFmpeg's -avoid_negative_ts make_zero option.
1740+
if (!ts_offset_decided)
1741+
{
1742+
int64_t min_start_avtb = INT64_MAX;
1743+
1744+
// Check container start_time (already in AV_TIME_BASE units)
1745+
if (ic && ic->start_time != AV_NOPTS_VALUE_)
1746+
{
1747+
min_start_avtb = ic->start_time;
1748+
}
1749+
1750+
// Check stream start_time
1751+
AVStream* st = ic->streams[video_stream];
1752+
if (st->start_time != AV_NOPTS_VALUE_)
1753+
{
1754+
int64_t s =to_avtb(st->start_time, st->time_base);
1755+
if (s < min_start_avtb) min_start_avtb = s;
1756+
}
1757+
1758+
// Check first observed timestamp (PTS preferred, else DTS from frame 0)
1759+
int64_t first_ts_stream = picture_pts;
1760+
if (first_ts_stream == AV_NOPTS_VALUE_ && dts != AV_NOPTS_VALUE_)
1761+
{
1762+
first_ts_stream = dts;
1763+
}
1764+
if (first_ts_stream != AV_NOPTS_VALUE_)
1765+
{
1766+
int64_t t =to_avtb(first_ts_stream, st->time_base);
1767+
if (t < min_start_avtb) min_start_avtb = t;
1768+
}
1769+
1770+
// Compute offset to shift negative timestamps to zero
1771+
ts_offset_avtb = (min_start_avtb != INT64_MAX && min_start_avtb <0) ? -min_start_avtb :0;
1772+
ts_offset_decided =true;
1773+
}
1774+
1775+
// Apply normalization to picture_pts
1776+
if (picture_pts != AV_NOPTS_VALUE_)
1777+
{
1778+
int64_t t =to_avtb(picture_pts, video_st->time_base);
1779+
t += ts_offset_avtb;
1780+
picture_pts =from_avtb(t, video_st->time_base);
1781+
}
1782+
1783+
// Also normalize dts
1784+
if (dts != AV_NOPTS_VALUE_)
1785+
{
1786+
int64_t t =to_avtb(dts, video_st->time_base);
1787+
t += ts_offset_avtb;
1788+
dts =from_avtb(t, video_st->time_base);
1789+
}
1790+
1791+
1792+
17171793
#if LIBAVCODEC_BUILD >= CALC_FFMPEG_VERSION(54, 1, 0) || LIBAVFORMAT_BUILD >= CALC_FFMPEG_VERSION(52, 111, 0)
17181794
AVRational frame_rate = video_st->avg_frame_rate;
17191795
#else
@@ -2120,8 +2196,13 @@ int64_t CvCapture_FFMPEG::dts_to_frame_number(int64_t dts)
21202196

21212197
doubleCvCapture_FFMPEG::dts_to_sec(int64_t dts)const
21222198
{
2123-
return (double)(dts - ic->streams[video_stream]->start_time) *
2124-
r2d(ic->streams[video_stream]->time_base);
2199+
const AVStream* st = ic->streams[video_stream];
2200+
int64_t ts = dts;
2201+
2202+
if (ts_offset_avtb ==0 && st->start_time != AV_NOPTS_VALUE_)
2203+
ts -= st->start_time;
2204+
2205+
return ts *r2d(st->time_base);
21252206
}
21262207

21272208
voidCvCapture_FFMPEG::get_rotation_angle()
@@ -2174,9 +2255,19 @@ void CvCapture_FFMPEG::seek(int64_t _frame_number)
21742255
{
21752256
int64_t _frame_number_temp =std::max(_frame_number-delta, (int64_t)0);
21762257
double sec = (double)_frame_number_temp /get_fps();
2177-
int64_t time_stamp = ic->streams[video_stream]->start_time;
2178-
double time_base =r2d(ic->streams[video_stream]->time_base);
2179-
time_stamp += (int64_t)(sec / time_base +0.5);
2258+
2259+
AVStream* st = ic->streams[video_stream];
2260+
int64_t time_stamp = st->start_time;
2261+
double time_base =r2d(st->time_base);
2262+
int64_t ts_norm = (int64_t)(sec / time_base +0.5);
2263+
2264+
if (ts_offset_avtb !=0) {
2265+
// map normalized target back to original demux timeline
2266+
time_stamp += ts_norm -from_avtb(ts_offset_avtb, st->time_base);
2267+
}else {
2268+
time_stamp += ts_norm;
2269+
}
2270+
21802271
if (get_total_frames() >1)av_seek_frame(ic, video_stream, time_stamp, AVSEEK_FLAG_BACKWARD);
21812272
if(!rawMode)
21822273
avcodec_flush_buffers(context);

‎modules/videoio/test/test_ffmpeg.cpp‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,63 @@ TEST(ffmpeg_cap_properties, set_pos_get_msec)
10551055
EXPECT_EQ(cap.get(CAP_PROP_POS_MSEC),0.0);
10561056
}
10571057

1058+
// Test that seeking twice to the same frame in videos with negative DTS
1059+
// does not result in negative position or timestamp values
1060+
// related issue: https://github.com/opencv/opencv/issues/27819
1061+
TEST(videoio_ffmpeg, seek_with_negative_dts)
1062+
{
1063+
if (!videoio_registry::hasBackend(CAP_FFMPEG))
1064+
throwSkipTestException("FFmpeg backend was not found");
1065+
1066+
const std::string filename =findDataFile("video/negdts_h264.mp4");
1067+
VideoCapturecap(filename, CAP_FFMPEG);
1068+
1069+
if (!cap.isOpened())
1070+
throwSkipTestException("Video stream is not supported");
1071+
1072+
// after open, a single grab() should not yield negative POS_MSEC.
1073+
ASSERT_TRUE(cap.grab());
1074+
EXPECT_GE(cap.get(CAP_PROP_POS_MSEC),0.0) <<"Negative ts immediately after open+grab()";
1075+
1076+
ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES,0));
1077+
(void)cap.get(CAP_PROP_POS_FRAMES);
1078+
1079+
constint framesToProbe[] = {2,3,4,5};
1080+
1081+
for (int f : framesToProbe)
1082+
{
1083+
// Reset to frame 0
1084+
ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES,0));
1085+
cap.get(CAP_PROP_POS_FRAMES);
1086+
1087+
// Seek to target frame
1088+
ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES, f));
1089+
constdouble posAfterFirstSeek = cap.get(CAP_PROP_POS_FRAMES);
1090+
1091+
// Seek to the same frame again
1092+
ASSERT_TRUE(cap.set(CAP_PROP_POS_FRAMES, f));
1093+
constdouble posAfterSecondSeek = cap.get(CAP_PROP_POS_FRAMES);
1094+
constdouble tsAfterSecondSeek = cap.get(CAP_PROP_POS_MSEC);
1095+
1096+
EXPECT_GE(posAfterSecondSeek,0)
1097+
<<"Frame index became negative after second seek to frame" << f
1098+
<<" (first seek gave" << posAfterFirstSeek <<")";
1099+
EXPECT_GE(tsAfterSecondSeek,0.0)
1100+
<<"Timestamp became negative after second seek to frame" << f;
1101+
1102+
// Per-iteration decode check: grab() + ts non-negative
1103+
ASSERT_TRUE(cap.grab());
1104+
EXPECT_GE(cap.get(CAP_PROP_POS_MSEC),0.0) <<"Negative timestamp after grab() at frame" << f;
1105+
1106+
// Verify that reading a frame works and position advances
1107+
Mat frame;
1108+
ASSERT_TRUE(cap.read(frame));
1109+
ASSERT_FALSE(frame.empty());
1110+
EXPECT_GE(cap.get(CAP_PROP_POS_FRAMES), f);
1111+
}
1112+
}
1113+
10581114
#endif// WIN32
10591115

1116+
10601117
}}// namespace

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp