Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions ffmpeg/decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<double> frame_pts; // per-frame PTS in seconds (containers)
bool frame_pts_tried = false;
};

FFmpegDecoder::FFmpegDecoder() : impl_(new Impl()) {}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<double> 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) {
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions ffmpeg/decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class FFmpegDecoder {

private:
bool ConvertFrame(const AVFrame* frame, DecodedFrame& out);
bool BuildFramePts();

struct Impl;
Impl* impl_ = nullptr;
Expand Down
23 changes: 23 additions & 0 deletions mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class MainWindow : public QMainWindow {
void SetupUi();
void BuildNalList();
void UpdateStats();
double SpsFrameRate() const;
void DecodeThumbnails();
void LoadRecentFiles();
void SaveRecentFiles();
Expand Down
21 changes: 21 additions & 0 deletions test/test_h264_params.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading