diff --git a/ffmpeg/decoder.cpp b/ffmpeg/decoder.cpp index 0059e0d..2705daf 100644 --- a/ffmpeg/decoder.cpp +++ b/ffmpeg/decoder.cpp @@ -24,6 +24,8 @@ struct FFmpegDecoder::Impl { DecodedFrame pending; int pending_index = 0; AVFrame* last_frame = nullptr; // reference to the last decoded frame + std::vector frame_pts; // per-frame PTS in seconds (containers) + bool frame_pts_tried = false; }; FFmpegDecoder::FFmpegDecoder() : impl_(new Impl()) {} @@ -131,6 +133,8 @@ bool FFmpegDecoder::OpenFile(const std::string& path, CodecType codec) { : ""; impl_->is_raw = (std::strcmp(fmtName, "h264") == 0 || std::strcmp(fmtName, "hevc") == 0); + impl_->frame_pts.clear(); + impl_->frame_pts_tried = false; path_ = path; codec_ = codec; @@ -346,6 +350,35 @@ bool FFmpegDecoder::SeekToFrame(int target_index) { return true; } +bool FFmpegDecoder::BuildFramePts() { + if (!impl_ || impl_->is_raw || impl_->video_stream_idx < 0) { + return false; + } + impl_->frame_pts_tried = true; + AVStream* st = impl_->fmtCtx->streams[impl_->video_stream_idx]; + if (!st) { + return false; + } + + std::vector pts; + AVPacket* pkt = av_packet_alloc(); + while (av_read_frame(impl_->fmtCtx, pkt) >= 0) { + if (pkt->stream_index == impl_->video_stream_idx && + pkt->pts != AV_NOPTS_VALUE) { + pts.push_back(pkt->pts * av_q2d(st->time_base)); + } + av_packet_unref(pkt); + } + av_packet_free(&pkt); + + // The demuxer is now at EOF; rewind for the following seek. + av_seek_frame(impl_->fmtCtx, impl_->video_stream_idx, 0, + AVSEEK_FLAG_BACKWARD); + avcodec_flush_buffers(impl_->codecCtx); + impl_->frame_pts = std::move(pts); + return !impl_->frame_pts.empty(); +} + bool FFmpegDecoder::SeekByContainer(int target_index) { if (!impl_ || !impl_->fmtCtx || impl_->video_stream_idx < 0 || frame_rate_ <= 0.0) { @@ -356,9 +389,18 @@ bool FFmpegDecoder::SeekByContainer(int target_index) { return false; } - const int64_t target_pts = - (int64_t)std::llround((double)target_index * st->time_base.den / - (frame_rate_ * st->time_base.num)); + int64_t target_pts = 0; + if (impl_->frame_pts.empty() && !impl_->frame_pts_tried) { + BuildFramePts(); + } + if ((size_t)target_index < impl_->frame_pts.size()) { + target_pts = (int64_t)std::llround(impl_->frame_pts[(size_t)target_index] / + av_q2d(st->time_base)); + } else { + target_pts = + (int64_t)std::llround((double)target_index * st->time_base.den / + (frame_rate_ * st->time_base.num)); + } const int seek_ret = av_seek_frame(impl_->fmtCtx, impl_->video_stream_idx, target_pts, AVSEEK_FLAG_BACKWARD); diff --git a/ffmpeg/decoder.h b/ffmpeg/decoder.h index 1c31cfb..3565096 100644 --- a/ffmpeg/decoder.h +++ b/ffmpeg/decoder.h @@ -50,6 +50,7 @@ class FFmpegDecoder { private: bool ConvertFrame(const AVFrame* frame, DecodedFrame& out); + bool BuildFramePts(); struct Impl; Impl* impl_ = nullptr; diff --git a/mainwindow.cpp b/mainwindow.cpp index c300b2f..1c60b0c 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -538,6 +538,22 @@ void MainWindow::BuildNalList() { metrics.horizontalAdvance("RSV_IRAP_VCL22") + 24); } +double MainWindow::SpsFrameRate() const { + for (const auto& entry : analyzer_.H264SpsMap()) { + const H264SPS& sps = entry.second; + if (sps.num_units_in_tick > 0 && sps.time_scale > 0) { + return (double)sps.time_scale / (2.0 * sps.num_units_in_tick); + } + } + for (const auto& entry : analyzer_.H265SpsMap()) { + const H265SPS& sps = entry.second; + if (sps.vui_num_units_in_tick > 0 && sps.vui_time_scale > 0) { + return (double)sps.vui_time_scale / sps.vui_num_units_in_tick; + } + } + return 0.0; +} + void MainWindow::UpdateStats() { const auto& s = analyzer_.stats(); const QString codec_str = @@ -644,6 +660,13 @@ void MainWindow::StartPlayer() { // No upfront decode: frames are decoded on demand while playing/seeking. total_frames_ = (int)stats.FrameCount; frame_rate_ = player_decoder_.FrameRate(); + if (!player_decoder_.HasTimestamps()) { + // Raw Annex B has no container timing; prefer the SPS VUI frame rate. + const double vui_fps = SpsFrameRate(); + if (vui_fps > 0.0) { + frame_rate_ = vui_fps; + } + } UpdateStats(); // refresh stats that depend on decoded frames / frame rate if (total_frames_ <= 0) { diff --git a/mainwindow.h b/mainwindow.h index b8bf10d..b337708 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -64,6 +64,7 @@ class MainWindow : public QMainWindow { void SetupUi(); void BuildNalList(); void UpdateStats(); + double SpsFrameRate() const; void DecodeThumbnails(); void LoadRecentFiles(); void SaveRecentFiles(); diff --git a/test/test_h264_params.cpp b/test/test_h264_params.cpp index 02749e4..107065c 100644 --- a/test/test_h264_params.cpp +++ b/test/test_h264_params.cpp @@ -204,3 +204,24 @@ TEST(H264ParamsTest, BuildsSliceDetailsText) { EXPECT_NE(analysis.details.find("NAL payload:"), std::string::npos); EXPECT_NE(analysis.details.find("Entropy coding:"), std::string::npos); } + +TEST(H264ParamsTest, ParsesVuiFrameRate) { + if (!sample_exists()) { + GTEST_SKIP() << "sample stream missing"; + } + + BitstreamParser parser(CodecType::kH264); + ASSERT_TRUE(parser.ParseFile(sample_path())) << parser.LastError(); + + const NALUnit* nal = firstOfType(parser.NalUnits(), 7); + ASSERT_NE(nal, nullptr); + H264SPS sps; + ASSERT_TRUE(ParseH264Sps(*nal, parser.NalData(*nal), sps)); + + if (!sps.timing_info_present_flag || sps.num_units_in_tick == 0) { + GTEST_SKIP() << "sample has no VUI timing"; + } + const double fps = (double)sps.time_scale / (2.0 * sps.num_units_in_tick); + EXPECT_GT(fps, 0.0); + EXPECT_LT(fps, 240.0); +}