From a3e6fc18abc5eaa4380f40bb3866246655aa1467 Mon Sep 17 00:00:00 2001 From: think3r Date: Sat, 30 May 2026 17:22:00 +0800 Subject: [PATCH 01/55] Fix Bitrate Plot popup text white on white blending issue setDefaultStyleSheet must be called before setHtml, otherwise the text color style is never applied. The popup had white text (#FFFFFF intended for dark backgrounds) on a semi-transparent white background, making it unreadable. --- YUViewLib/src/ui/views/PlotViewWidget.cpp | 4 ++-- YUViewLib/src/ui/views/SplitViewWidget.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/YUViewLib/src/ui/views/PlotViewWidget.cpp b/YUViewLib/src/ui/views/PlotViewWidget.cpp index 53ab62553..ec9003014 100644 --- a/YUViewLib/src/ui/views/PlotViewWidget.cpp +++ b/YUViewLib/src/ui/views/PlotViewWidget.cpp @@ -808,8 +808,8 @@ void PlotViewWidget::drawInfoBox(QPainter &painter) const // Create a QTextDocument. This object can tell us the size of the rendered text. QTextDocument textDocument; + textDocument.setDefaultStyleSheet("* { color: #000000 }"); textDocument.setHtml(infoString); - textDocument.setDefaultStyleSheet("* { color: #FFFFFF }"); textDocument.setTextWidth(textDocument.size().width()); // Translate to the position where the text box shall be @@ -865,8 +865,8 @@ void PlotViewWidget::drawDebugBox(QPainter &painter) const // Create a QTextDocument. This object can tell us the size of the rendered text. QTextDocument textDocument; + textDocument.setDefaultStyleSheet("* { color: #000000 }"); textDocument.setHtml(infoString); - textDocument.setDefaultStyleSheet("* { color: #FFFFFF }"); textDocument.setTextWidth(textDocument.size().width()); // Translate to the position where the text box shall be diff --git a/YUViewLib/src/ui/views/SplitViewWidget.cpp b/YUViewLib/src/ui/views/SplitViewWidget.cpp index 1e71e5aca..eaee99461 100644 --- a/YUViewLib/src/ui/views/SplitViewWidget.cpp +++ b/YUViewLib/src/ui/views/SplitViewWidget.cpp @@ -732,7 +732,7 @@ void splitViewWidget::paintZoomBox(int view, // Create a QTextDocument. This object can tell us the size of the rendered text. QTextDocument textDocument; - textDocument.setDefaultStyleSheet("* { color: #FFFFFF }"); + textDocument.setDefaultStyleSheet("* { color: #000000 }"); textDocument.setHtml(pixelInfoString); textDocument.setTextWidth(textDocument.size().width()); From 629ee4d8cf3c760a3c25ee095868d26d2b544f1d Mon Sep 17 00:00:00 2001 From: think3r Date: Sat, 30 May 2026 19:47:21 +0800 Subject: [PATCH 02/55] Extend FFmpeg struct version support to cover FFmpeg 6.x/7.x --- YUViewLib/src/ffmpeg/AVCodecWrapper.cpp | 42 +++++++++++++++++-- YUViewLib/src/ffmpeg/AVInputFormatWrapper.cpp | 9 ++-- .../src/ffmpeg/AVMotionVectorWrapper.cpp | 15 ++++--- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/YUViewLib/src/ffmpeg/AVCodecWrapper.cpp b/YUViewLib/src/ffmpeg/AVCodecWrapper.cpp index 2d826e43c..febf9f275 100644 --- a/YUViewLib/src/ffmpeg/AVCodecWrapper.cpp +++ b/YUViewLib/src/ffmpeg/AVCodecWrapper.cpp @@ -57,7 +57,7 @@ typedef struct AVCodec_56_57_58 // Actually, there is more here, but nothing more of the public API } AVCodec_56_57_58; -typedef struct AVCodec_59 +typedef struct AVCodec_59_60 { const char * name; const char * long_name; @@ -73,7 +73,26 @@ typedef struct AVCodec_59 const AVClass * priv_class; // Actually, there is more here, but nothing more of the public API -} AVCodec_59; +} AVCodec_59_60; + +// Same as AVCodec_59_60 but without channel_layouts (removed by FF_API_OLD_CHANNEL_LAYOUT +// in avutil >= 59 for avcodec 61). +typedef struct AVCodec_61 +{ + const char * name; + const char * long_name; + enum AVMediaType type; + enum AVCodecID id; + int capabilities; + uint8_t max_lowres; + const AVRational * supported_framerates; + const enum AVPixelFormat * pix_fmts; + const int * supported_samplerates; + const enum AVSampleFormat *sample_fmts; + const AVClass * priv_class; + + // Actually, there is more here, but nothing more of the public API +} AVCodec_61; template std::vector convertRawListToVec(const T *rawValues, T terminationValue) { @@ -110,9 +129,9 @@ void AVCodecWrapper::update() this->channel_layouts = convertRawListToVec(p->channel_layouts, uint64_t(0)); this->max_lowres = p->max_lowres; } - else if (libVer.avcodec.major == 59) + else if (libVer.avcodec.major == 59 || libVer.avcodec.major == 60) { - auto p = reinterpret_cast(codec); + auto p = reinterpret_cast(codec); this->name = QString(p->name); this->long_name = QString(p->long_name); this->type = p->type; @@ -125,6 +144,21 @@ void AVCodecWrapper::update() this->channel_layouts = convertRawListToVec(p->channel_layouts, uint64_t(0)); this->max_lowres = p->max_lowres; } + else if (libVer.avcodec.major == 61) + { + auto p = reinterpret_cast(codec); + this->name = QString(p->name); + this->long_name = QString(p->long_name); + this->type = p->type; + this->id = p->id; + this->capabilities = p->capabilities; + this->supported_framerates = convertRawListToVec(p->supported_framerates, AVRational({0, 0})); + this->pix_fmts = convertRawListToVec(p->pix_fmts, AVPixelFormat(-1)); + this->supported_samplerates = convertRawListToVec(p->supported_samplerates, 0); + this->sample_fmts = convertRawListToVec(p->sample_fmts, AVSampleFormat(-1)); + this->channel_layouts = {}; + this->max_lowres = p->max_lowres; + } else throw std::runtime_error("Invalid library version"); } diff --git a/YUViewLib/src/ffmpeg/AVInputFormatWrapper.cpp b/YUViewLib/src/ffmpeg/AVInputFormatWrapper.cpp index 0193e9145..19b1f9437 100644 --- a/YUViewLib/src/ffmpeg/AVInputFormatWrapper.cpp +++ b/YUViewLib/src/ffmpeg/AVInputFormatWrapper.cpp @@ -38,7 +38,7 @@ namespace FFmpeg namespace { -typedef struct AVInputFormat_56_57_58_59_60 +typedef struct AVInputFormat_56_57_58_59_60_61 { const char * name; const char * long_name; @@ -49,7 +49,7 @@ typedef struct AVInputFormat_56_57_58_59_60 const char * mime_type; // There is more but it is not part of the public ABI -} AVInputFormat_56_57_58_59_60; +} AVInputFormat_56_57_58_59_60_61; } // namespace @@ -99,9 +99,10 @@ void AVInputFormatWrapper::update() this->libVer.avformat.major == 57 || // this->libVer.avformat.major == 58 || // this->libVer.avformat.major == 59 || // - this->libVer.avformat.major == 60) + this->libVer.avformat.major == 60 || + this->libVer.avformat.major == 61) { - auto p = reinterpret_cast(this->fmt); + auto p = reinterpret_cast(this->fmt); this->name = QString(p->name); this->long_name = QString(p->long_name); this->flags = p->flags; diff --git a/YUViewLib/src/ffmpeg/AVMotionVectorWrapper.cpp b/YUViewLib/src/ffmpeg/AVMotionVectorWrapper.cpp index 32c027616..1bd327b20 100644 --- a/YUViewLib/src/ffmpeg/AVMotionVectorWrapper.cpp +++ b/YUViewLib/src/ffmpeg/AVMotionVectorWrapper.cpp @@ -48,7 +48,7 @@ typedef struct AVMotionVector_54 uint64_t flags; } AVMotionVector_54; -typedef struct AVMotionVector_55_56_57 +typedef struct AVMotionVector_55_56_57_58_59 { int32_t source; uint8_t w, h; @@ -57,7 +57,7 @@ typedef struct AVMotionVector_55_56_57 uint64_t flags; int32_t motion_x, motion_y; uint16_t motion_scale; -} AVMotionVector_55_56_57; +} AVMotionVector_55_56_57_58_59; } // namespace @@ -80,9 +80,11 @@ AVMotionVectorWrapper::AVMotionVectorWrapper(LibraryVersion &libVer, uint8_t *da } else if (libVer.avutil.major == 55 || // libVer.avutil.major == 56 || // - libVer.avutil.major == 57) + libVer.avutil.major == 57 || + libVer.avutil.major == 58 || + libVer.avutil.major == 59) { - auto p = reinterpret_cast(data) + idx; + auto p = reinterpret_cast(data) + idx; this->source = p->source; this->w = p->w; this->h = p->h; @@ -103,8 +105,9 @@ size_t AVMotionVectorWrapper::getNumberOfMotionVectors(LibraryVersion &libVer, s { if (libVer.avutil.major == 54) return dataSize / sizeof(AVMotionVector_54); - else if (libVer.avutil.major == 55 || libVer.avutil.major == 56 || libVer.avutil.major == 57) - return dataSize / sizeof(AVMotionVector_55_56_57); + else if (libVer.avutil.major == 55 || libVer.avutil.major == 56 || libVer.avutil.major == 57 || + libVer.avutil.major == 58 || libVer.avutil.major == 59) + return dataSize / sizeof(AVMotionVector_55_56_57_58_59); else return 0; } From 811363ad920a32644050cc2d53417f9d3a383356 Mon Sep 17 00:00:00 2001 From: think3r Date: Sat, 30 May 2026 22:00:10 +0800 Subject: [PATCH 03/55] Add stream selection dropdown to Bitrate Plot tab - Add bitratePlotStreamComboBox to UI for selecting which streams to display - Add setShowStreamList() method to PlotViewWidget to control visible streams - Add showOnlyBitrateStream tracking variable and slot handler - Connect combo box signal and implement handler to update PlotViewWidget - Update stream combo box in updateStreamInfo() similar to Packet Analysis - Follows same UX pattern as showStreamComboBox in Packet Analysis tab --- CLAUDE.md | 73 +++++++++++++++++++ YUViewLib/src/ui/views/PlotViewWidget.cpp | 6 ++ YUViewLib/src/ui/views/PlotViewWidget.h | 1 + .../ui/widgets/BitstreamAnalysisWidget.cpp | 61 ++++++++++++++-- .../src/ui/widgets/BitstreamAnalysisWidget.h | 3 + YUViewLib/ui/bitstreamAnalysisWidget.ui | 35 +++++---- ff_libs.md | 34 +++++++++ 7 files changed, 194 insertions(+), 19 deletions(-) create mode 100644 CLAUDE.md create mode 100644 ff_libs.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..b2548d338 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 构建项目 + +```bash +# 创建构建目录 +mkdir build && cd build + +# 配置项目(启用单元测试) +qmake6 CONFIG+=UNITTESTS .. + +# 编译 +make -j$(nproc) +``` + +## 运行测试 + +```bash +# 运行单元测试(需要配置 UNITTESTS) +./build/YUViewUnitTest/YUViewUnitTest + +# CI 环境使用 offscreen 模式 +QT_QPA_PLATFORM=offscreen ./build/YUViewUnitTest/YUViewUnitTest +``` + +## 项目结构 + +- **YUViewLib/** - 核心业务逻辑库(静态库) + - `src/parser/` - 比特流解析器(HEVC/AVC/VVC/AV1/MPEG2) + - `src/decoder/` - 解码器抽象层和实现(FFmpeg/Dav1d/libde265/HM/VTM) + - `src/video/` - 视频帧处理(RGB/YUV 格式转换) + - `src/playlistitem/` - 播放列表项(Raw 文件/压缩视频/图像/统计叠加) + - `src/ui/` - Qt UI 组件(主窗口、播放控制、设置对话框) + - `src/statistics/` - 统计信息叠加系统 +- **YUViewApp/** - Qt 应用程序入口点 +- **YUViewUnitTest/** - Google Test 单元测试 +- **submodules/** - googletest 和 googletest-qmake + +## 架构模式 + +1. **解码器插件架构**:`decoderBase` 定义抽象接口,`decoderFFmpeg`、`decoderDav1d`、`decoderLibde265` 等实现具体解码器 +2. **播放列表项层次**:`playlistItem` 基类,各类媒体(Raw YUV/压缩视频/图像序列/统计文件)作为子类 +3. **视频处理器层次**:`videoHandler` 处理帧操作,子类包括 `videoHandlerDifference`、`videoHandlerResample` +4. **解析器层次**:按编码器在 `parser/` 下组织(HEVC/AVC/VVC/AV1 等) + +## 代码规范 + +- C++20 标准 +- 核心代码优先 Qt-free(增强可移植性) +- 格式化工具:`.clang-format`(2 空格缩进、Allman 大括号风格) +- 编码规范详见 `HACKING.md`: + - UTF8 编码、LF 行尾 + - 成员变量使用 CamelCase,无前缀 + - 参数传递:按值传递基本类型,按引用传递复杂类型 + - const 正确性 + - 优先使用 `std` 而非 Qt 类型 + +## 依赖 + +- Qt6 (qt6-base-dev) +- libde265(HEVC 解码,可选) +- FFmpeg(视频解码) +- Google Test(通过 git 子模块管理) + +## 子模块初始化 + +```bash +git clone --recurse-submodules +# 或初始化 +git submodule update --init --recursive +``` \ No newline at end of file diff --git a/YUViewLib/src/ui/views/PlotViewWidget.cpp b/YUViewLib/src/ui/views/PlotViewWidget.cpp index ec9003014..b8cf551db 100644 --- a/YUViewLib/src/ui/views/PlotViewWidget.cpp +++ b/YUViewLib/src/ui/views/PlotViewWidget.cpp @@ -91,6 +91,12 @@ void PlotViewWidget::modelDataChanged() } } +void PlotViewWidget::setShowStreamList(const QList &list) +{ + this->showStreamList = list; + this->update(); +} + void PlotViewWidget::modelNrStreamsChanged() { this->showStreamList.clear(); diff --git a/YUViewLib/src/ui/views/PlotViewWidget.h b/YUViewLib/src/ui/views/PlotViewWidget.h index 3ec73d855..fdff26abc 100644 --- a/YUViewLib/src/ui/views/PlotViewWidget.h +++ b/YUViewLib/src/ui/views/PlotViewWidget.h @@ -42,6 +42,7 @@ class PlotViewWidget : public MoveAndZoomableView public: PlotViewWidget(QWidget *parent = 0); void setModel(PlotModel *model); + void setShowStreamList(const QList &list); private slots: void modelDataChanged(); diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp index baf37762a..a8868e733 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp @@ -64,10 +64,14 @@ BitstreamAnalysisWidget::BitstreamAnalysisWidget(QWidget *parent) : QWidget(pare &QCheckBox::toggled, this, &BitstreamAnalysisWidget::parseEntireBitstreamCheckBoxToggled); - this->connect(this->ui.bitratePlotOrderComboBox, - QOverload::of(&QComboBox::currentIndexChanged), - this, - &BitstreamAnalysisWidget::bitratePlotOrderComboBoxIndexChanged); +this->connect(this->ui.bitratePlotOrderComboBox, + QOverload::of(&QComboBox::currentIndexChanged), + this, + &BitstreamAnalysisWidget::bitratePlotOrderComboBoxIndexChanged); + this->connect(this->ui.bitratePlotStreamComboBox, + QOverload::of(&QComboBox::currentIndexChanged), + this, + &BitstreamAnalysisWidget::bitratePlotStreamComboBoxIndexChanged); this->currentSelectedItemsChanged(nullptr, nullptr, false); } @@ -119,8 +123,33 @@ void BitstreamAnalysisWidget::updateStreamInfo() for (unsigned i = 0; i < this->parser->getNrStreams(); i++) { const auto info = this->parser->getShortStreamDescription(i); - this->ui.showStreamComboBox->addItem(QString("Stream %1 - ").arg(i) + +this->ui.showStreamComboBox->addItem(QString("Stream %1 - ").arg(i) + QString::fromStdString(info)); + } + } + } + + auto nrStreams = this->parser->getNrStreams(); + auto nrBitrateSelections = nrStreams; + if (nrStreams > 1) + nrBitrateSelections += 1; + if (this->ui.bitratePlotStreamComboBox->count() != int(nrBitrateSelections)) + { + this->ui.bitratePlotStreamComboBox->clear(); + if (nrBitrateSelections == 1) + { + this->ui.bitratePlotStreamComboBox->addItem("Show stream 0"); + this->ui.bitratePlotStreamComboBox->setEnabled(false); + } + else + { + this->ui.bitratePlotStreamComboBox->setEnabled(true); + this->ui.bitratePlotStreamComboBox->addItem("Show all streams"); + for (unsigned i = 0; i < nrStreams; i++) + { + const auto info = this->parser->getShortStreamDescription(i); + this->ui.bitratePlotStreamComboBox->addItem(QString("Stream %1 - ").arg(i) + + QString::fromStdString(info)); } } } @@ -150,6 +179,28 @@ void BitstreamAnalysisWidget::bitratePlotOrderComboBoxIndexChanged(int index) this->parser->setBitrateSortingIndex(index); } +void BitstreamAnalysisWidget::bitratePlotStreamComboBoxIndexChanged(int index) +{ + const int selectedStream = index - 1; + if (this->showOnlyBitrateStream == selectedStream) + return; + + this->showOnlyBitrateStream = selectedStream; + + QList showList; + if (selectedStream == -1) + { + for (unsigned i = 0; i < this->parser->getNrStreams(); i++) + showList.append(i); + } + else + { + showList.append(unsigned(selectedStream)); + } + + this->ui.plotViewWidget->setShowStreamList(showList); +} + void BitstreamAnalysisWidget::updateParsingStatusText(int progressValue) { if (progressValue <= -1) diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h index dfb1a04a0..8f98e6f33 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h @@ -64,6 +64,7 @@ private slots: void colorCodeStreamsCheckBoxToggled(bool state) { this->parser->setStreamColorCoding(state); } void parseEntireBitstreamCheckBoxToggled(bool) { this->restartParsingOfCurrentItem(); } void bitratePlotOrderComboBoxIndexChanged(int index); + void bitratePlotStreamComboBoxIndexChanged(int index); protected: void hideEvent(QHideEvent *event) override; @@ -88,4 +89,6 @@ private slots: // -1: Show all streams. Otherwise only show the given stream index. int showOnlyStream{-1}; + // -1: Show all streams. Otherwise only show the given stream index. + int showOnlyBitrateStream{-1}; }; diff --git a/YUViewLib/ui/bitstreamAnalysisWidget.ui b/YUViewLib/ui/bitstreamAnalysisWidget.ui index 4dc12b23d..3099f7580 100644 --- a/YUViewLib/ui/bitstreamAnalysisWidget.ui +++ b/YUViewLib/ui/bitstreamAnalysisWidget.ui @@ -118,21 +118,28 @@ - - - - - - Decoding Order - - - - - Display Order + + + + + Select which streams to show in the bitrate plot. - - - + + + + + + + Decoding Order + + + + + Display Order + + + + diff --git a/ff_libs.md b/ff_libs.md new file mode 100644 index 000000000..56e146c8f --- /dev/null +++ b/ff_libs.md @@ -0,0 +1,34 @@ +# FFmpeg 版本支持矩阵 + +## 加载器支持的版本组合 + +来自 `FFmpegVersionHandler.cpp:104-110`。按优先级从新到旧尝试加载。 + +| avutil | avcodec | avformat | swresample | FFmpeg 分支 | +|--------|---------|----------|------------|-------------| +| 59 | 61 | 61 | 5 | FFmpeg 7.x | +| 58 | 60 | 60 | 4 | FFmpeg 6.x | +| 57 | 59 | 59 | 4 | FFmpeg 5.x | +| 56 | 58 | 58 | 3 | FFmpeg 4.x | +| 55 | 57 | 57 | 2 | FFmpeg 3.x | +| 54 | 56 | 56 | 1 | FFmpeg 2.x | + +## 各 wrapper 实际覆盖范围 + +| 文件 | 检查库 | 覆盖版本 | 对应 FFmpeg 分支 | 未知版本行为 | +|------|--------|---------|-----------------|-------------| +| AVMotionVectorWrapper | avutil | 54, 55-59 | 2.x ~ 7.x | 构造 throw / count return 0 | +| AVFrameSideDataWrapper | avutil | 54-59 | 2.x ~ 7.x | throw | +| AVFrameWrapper | avutil | 54-59 | 2.x ~ 7.x | throw(getMetadata 断言 >=57) | +| AVPixFmtDescriptorWrapper | avutil | 54-59 | 2.x ~ 7.x | 静默跳过(无 throw) | +| AVCodecContextWrapper | avcodec | 56-61 | 2.x ~ 7.x | throw | +| AVCodecWrapper | avcodec | 56-61 | 2.x ~ 7.x | throw | +| AVPacketWrapper | avcodec | 56-61 | 2.x ~ 7.x | throw | +| AVFormatContextWrapper | avformat | 56-61 | 2.x ~ 7.x | throw | +| AVStreamWrapper | avformat | 56-61 | 2.x ~ 7.x | throw | +| AVCodecParametersWrapper | avformat | 56-61 | 2.x ~ 7.x | throw(56 设 nullptr) | +| AVInputFormatWrapper | avformat | 56-61 | 2.x ~ 7.x | 静默跳过(无 throw) | + +## 关键风险点 + +1. **AVPixFmtDescriptorWrapper.cpp** 和 **AVInputFormatWrapper.cpp** 是仅有的两个对未知版本静默失败的文件,可能导致后续空指针访问。 From 9626cb6ceeaebc7feb71129eaf1300b5437ec9ee Mon Sep 17 00:00:00 2001 From: think3r Date: Sat, 30 May 2026 22:00:54 +0800 Subject: [PATCH 04/55] Move Type field from Average to Bar info box in Bitrate Plot - Remove misleading Type row from Stream Average tooltip (plotIndex == 1) - Add Type row to Stream tooltip for video frames (plotIndex == 0) - Type (I/P/B frame) is only relevant to individual frames, not averages --- YUViewLib/src/parser/common/BitratePlotModel.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/YUViewLib/src/parser/common/BitratePlotModel.cpp b/YUViewLib/src/parser/common/BitratePlotModel.cpp index a99dbd798..fe86d2273 100644 --- a/YUViewLib/src/parser/common/BitratePlotModel.cpp +++ b/YUViewLib/src/parser/common/BitratePlotModel.cpp @@ -119,13 +119,11 @@ BitratePlotModel::getPointInfo(unsigned streamIndex, unsigned plotIndex, unsigne "PTS:%2" "DTS:%3" "Average:%4" - "Type:%5" "") .arg(streamIndex) .arg(entry.pts) .arg(entry.dts) - .arg(this->calculateAverageValue(streamIndex, pointIndex)) - .arg(entry.frameType); + .arg(this->calculateAverageValue(streamIndex, pointIndex)); else return QString("

Stream %1

" "" @@ -133,12 +131,14 @@ BitratePlotModel::getPointInfo(unsigned streamIndex, unsigned plotIndex, unsigne "" "" "" + "" "
DTS:%3
Duration:%4
Bitrate:%5
Type:%6
") .arg(streamIndex) .arg(entry.pts) .arg(entry.dts) .arg(entry.duration) - .arg(entry.bitrate); + .arg(entry.bitrate) + .arg(entry.frameType); } std::optional BitratePlotModel::getReasonabelRangeToShowOnXAxisPer100Pixels() const From 5ec6a645bf2ebae1dc02491a1a7d7eb0c8643264 Mon Sep 17 00:00:00 2001 From: think3r Date: Sat, 30 May 2026 22:59:31 +0800 Subject: [PATCH 05/55] Fix empty frame types in AVC/AV1/MPEG2 parsers: add missing 'meaning' field to CodingEnum entries --- .../src/parser/AV1/interpolation_filter.cpp | 10 +++--- YUViewLib/src/parser/AV1/obu_header.h | 32 +++++++++---------- YUViewLib/src/parser/AVC/slice_header.cpp | 20 ++++++------ YUViewLib/src/parser/Mpeg2/nal_extension.cpp | 20 ++++++------ YUViewLib/src/parser/Mpeg2/nal_unit_header.h | 22 ++++++------- 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/YUViewLib/src/parser/AV1/interpolation_filter.cpp b/YUViewLib/src/parser/AV1/interpolation_filter.cpp index b98a1c493..2ce616495 100644 --- a/YUViewLib/src/parser/AV1/interpolation_filter.cpp +++ b/YUViewLib/src/parser/AV1/interpolation_filter.cpp @@ -42,11 +42,11 @@ namespace { CodingEnum - interpolationFilterCoding({{0, InterpolationFilter::EIGHTTAP, "EIGHTTAP"}, - {1, InterpolationFilter::EIGHTTAP_SMOOTH, "EIGHTTAP_SMOOTH"}, - {2, InterpolationFilter::EIGHTTAP_SHARP, "EIGHTTAP_SHARP"}, - {3, InterpolationFilter::BILINEAR, "BILINEAR"}, - {4, InterpolationFilter::SWITCHABLE, "SWITCHABLE"}}, + interpolationFilterCoding({{0, InterpolationFilter::EIGHTTAP, "EIGHTTAP", "8-tap"}, + {1, InterpolationFilter::EIGHTTAP_SMOOTH, "EIGHTTAP_SMOOTH", "8-tap smooth"}, + {2, InterpolationFilter::EIGHTTAP_SHARP, "EIGHTTAP_SHARP", "8-tap sharp"}, + {3, InterpolationFilter::BILINEAR, "BILINEAR", "Bilinear"}, + {4, InterpolationFilter::SWITCHABLE, "SWITCHABLE", "Switchable"}}, InterpolationFilter::EIGHTTAP); } diff --git a/YUViewLib/src/parser/AV1/obu_header.h b/YUViewLib/src/parser/AV1/obu_header.h index 7de474106..917aa0bc7 100644 --- a/YUViewLib/src/parser/AV1/obu_header.h +++ b/YUViewLib/src/parser/AV1/obu_header.h @@ -55,22 +55,22 @@ enum class ObuType }; static CodingEnum - obuTypeCoding({{0, ObuType::RESERVED, "RESERVED"}, - {1, ObuType::OBU_SEQUENCE_HEADER, "OBU_SEQUENCE_HEADER"}, - {2, ObuType::OBU_TEMPORAL_DELIMITER, "OBU_TEMPORAL_DELIMITER"}, - {3, ObuType::OBU_FRAME_HEADER, "OBU_FRAME_HEADER"}, - {4, ObuType::OBU_TILE_GROUP, "OBU_TILE_GROUP"}, - {5, ObuType::OBU_METADATA, "OBU_METADATA"}, - {6, ObuType::OBU_FRAME, "OBU_FRAME"}, - {7, ObuType::OBU_REDUNDANT_FRAME_HEADER, "OBU_REDUNDANT_FRAME_HEADER"}, - {8, ObuType::OBU_TILE_LIST, "OBU_TILE_LIST"}, - {9, ObuType::RESERVED, "RESERVED"}, - {10, ObuType::RESERVED, "RESERVED"}, - {11, ObuType::RESERVED, "RESERVED"}, - {12, ObuType::RESERVED, "RESERVED"}, - {13, ObuType::RESERVED, "RESERVED"}, - {14, ObuType::RESERVED, "RESERVED"}, - {15, ObuType::OBU_PADDING, "OBU_PADDING"}}, + obuTypeCoding({{0, ObuType::RESERVED, "RESERVED", "Reserved"}, + {1, ObuType::OBU_SEQUENCE_HEADER, "OBU_SEQUENCE_HEADER", "Sequence Header"}, + {2, ObuType::OBU_TEMPORAL_DELIMITER, "OBU_TEMPORAL_DELIMITER", "Temporal Delimiter"}, + {3, ObuType::OBU_FRAME_HEADER, "OBU_FRAME_HEADER", "Frame Header"}, + {4, ObuType::OBU_TILE_GROUP, "OBU_TILE_GROUP", "Tile Group"}, + {5, ObuType::OBU_METADATA, "OBU_METADATA", "Metadata"}, + {6, ObuType::OBU_FRAME, "OBU_FRAME", "Frame"}, + {7, ObuType::OBU_REDUNDANT_FRAME_HEADER, "OBU_REDUNDANT_FRAME_HEADER", "Redundant Frame Header"}, + {8, ObuType::OBU_TILE_LIST, "OBU_TILE_LIST", "Tile List"}, + {9, ObuType::RESERVED, "RESERVED", "Reserved"}, + {10, ObuType::RESERVED, "RESERVED", "Reserved"}, + {11, ObuType::RESERVED, "RESERVED", "Reserved"}, + {12, ObuType::RESERVED, "RESERVED", "Reserved"}, + {13, ObuType::RESERVED, "RESERVED", "Reserved"}, + {14, ObuType::RESERVED, "RESERVED", "Reserved"}, + {15, ObuType::OBU_PADDING, "OBU_PADDING", "Padding"}}, ObuType::RESERVED); class obu_header diff --git a/YUViewLib/src/parser/AVC/slice_header.cpp b/YUViewLib/src/parser/AVC/slice_header.cpp index b5b64f29a..43eb59382 100644 --- a/YUViewLib/src/parser/AVC/slice_header.cpp +++ b/YUViewLib/src/parser/AVC/slice_header.cpp @@ -59,16 +59,16 @@ namespace { parser::CodingEnum - sliceTypeCoding({{0, SliceType::SLICE_P, "P (P slice)"}, - {1, SliceType::SLICE_B, "B (B slice)"}, - {2, SliceType::SLICE_I, "I (I slice)"}, - {3, SliceType::SLICE_SP, "SP (SP slice)"}, - {4, SliceType::SLICE_SI, "SI (SI slice)"}, - {5, SliceType::SLICE_P, "P (P slice) all slices"}, - {6, SliceType::SLICE_B, "B (B slice) all slices"}, - {7, SliceType::SLICE_I, "I (I slice) all slices"}, - {8, SliceType::SLICE_SP, "SP (SP slice) all slices"}, - {9, SliceType::SLICE_SI, "SI (SI slice) all slices"}}, + sliceTypeCoding({{0, SliceType::SLICE_P, "P (P slice)", "P"}, + {1, SliceType::SLICE_B, "B (B slice)", "B"}, + {2, SliceType::SLICE_I, "I (I slice)", "I"}, + {3, SliceType::SLICE_SP, "SP (SP slice)", "SP"}, + {4, SliceType::SLICE_SI, "SI (SI slice)", "SI"}, + {5, SliceType::SLICE_P, "P (P slice) all slices", "P"}, + {6, SliceType::SLICE_B, "B (B slice) all slices", "B"}, + {7, SliceType::SLICE_I, "I (I slice) all slices", "I"}, + {8, SliceType::SLICE_SP, "SP (SP slice) all slices", "SP"}, + {9, SliceType::SLICE_SI, "SI (SI slice) all slices", "SI"}}, SliceType::SLICE_P); } diff --git a/YUViewLib/src/parser/Mpeg2/nal_extension.cpp b/YUViewLib/src/parser/Mpeg2/nal_extension.cpp index ba8079181..32d6df093 100644 --- a/YUViewLib/src/parser/Mpeg2/nal_extension.cpp +++ b/YUViewLib/src/parser/Mpeg2/nal_extension.cpp @@ -44,16 +44,16 @@ namespace using namespace parser::mpeg2; parser::CodingEnum extensionTypeCoding( - {{0, ExtensionType::EXT_SEQUENCE, "sequence_extension()"}, - {1, ExtensionType::EXT_SEQUENCE_DISPLAY, "sequence_display_extension()"}, - {2, ExtensionType::EXT_QUANT_MATRIX, "quant_matrix_extension()"}, - {3, ExtensionType::EXT_COPYRIGHT, "copyright_extension()"}, - {4, ExtensionType::EXT_SEQUENCE_SCALABLE, "sequence_scalable_extension()"}, - {5, ExtensionType::EXT_PICTURE_DISPLAY, "picture_display_extension()"}, - {6, ExtensionType::EXT_PICTURE_CODING, "picture_coding_extension()"}, - {7, ExtensionType::EXT_PICTURE_SPATICAL_SCALABLE, "picture_spatial_scalable_extension()"}, - {8, ExtensionType::EXT_PICTURE_TEMPORAL_SCALABLE, "picture_temporal_scalable_extension()"}, - {9, ExtensionType::EXT_RESERVED, "Reserved"}}, + {{0, ExtensionType::EXT_SEQUENCE, "sequence_extension()", "Sequence"}, + {1, ExtensionType::EXT_SEQUENCE_DISPLAY, "sequence_display_extension()", "Sequence Display"}, + {2, ExtensionType::EXT_QUANT_MATRIX, "quant_matrix_extension()", "Quant Matrix"}, + {3, ExtensionType::EXT_COPYRIGHT, "copyright_extension()", "Copyright"}, + {4, ExtensionType::EXT_SEQUENCE_SCALABLE, "sequence_scalable_extension()", "Sequence Scalable"}, + {5, ExtensionType::EXT_PICTURE_DISPLAY, "picture_display_extension()", "Picture Display"}, + {6, ExtensionType::EXT_PICTURE_CODING, "picture_coding_extension()", "Picture Coding"}, + {7, ExtensionType::EXT_PICTURE_SPATICAL_SCALABLE, "picture_spatial_scalable_extension()", "Picture Spatial Scalable"}, + {8, ExtensionType::EXT_PICTURE_TEMPORAL_SCALABLE, "picture_temporal_scalable_extension()", "Picture Temporal Scalable"}, + {9, ExtensionType::EXT_RESERVED, "Reserved", "Reserved"}}, ExtensionType::EXT_RESERVED); } // namespace diff --git a/YUViewLib/src/parser/Mpeg2/nal_unit_header.h b/YUViewLib/src/parser/Mpeg2/nal_unit_header.h index 998a77a53..8f52e532a 100644 --- a/YUViewLib/src/parser/Mpeg2/nal_unit_header.h +++ b/YUViewLib/src/parser/Mpeg2/nal_unit_header.h @@ -56,17 +56,17 @@ enum class NalType }; static parser::CodingEnum - nalTypeCoding({{0, NalType::UNSPECIFIED, "UNSPECIFIED"}, - {0, NalType::PICTURE, "PICTURE"}, - {1, NalType::SLICE, "SLICE"}, - {2, NalType::USER_DATA, "USER_DATA"}, - {3, NalType::SEQUENCE_HEADER, "SEQUENCE_HEADER"}, - {4, NalType::SEQUENCE_ERROR, "SEQUENCE_ERROR"}, - {5, NalType::EXTENSION_START, "EXTENSION_START"}, - {6, NalType::SEQUENCE_END, "SEQUENCE_END"}, - {7, NalType::GROUP_START, "GROUP_START"}, - {8, NalType::SYSTEM_START_CODE, "SYSTEM_START_CODE"}, - {9, NalType::RESERVED, "RESERVED"}}, + nalTypeCoding({{0, NalType::UNSPECIFIED, "UNSPECIFIED", "Unspecified"}, + {0, NalType::PICTURE, "PICTURE", "Picture"}, + {1, NalType::SLICE, "SLICE", "Slice"}, + {2, NalType::USER_DATA, "USER_DATA", "User Data"}, + {3, NalType::SEQUENCE_HEADER, "SEQUENCE_HEADER", "Sequence Header"}, + {4, NalType::SEQUENCE_ERROR, "SEQUENCE_ERROR", "Sequence Error"}, + {5, NalType::EXTENSION_START, "EXTENSION_START", "Extension Start"}, + {6, NalType::SEQUENCE_END, "SEQUENCE_END", "Sequence End"}, + {7, NalType::GROUP_START, "GROUP_START", "Group Start"}, + {8, NalType::SYSTEM_START_CODE, "SYSTEM_START_CODE", "System Start Code"}, + {9, NalType::RESERVED, "RESERVED", "Reserved"}}, NalType::UNSPECIFIED); /* The basic Mpeg2 NAL unit. Technically, there is no concept of NAL units in mpeg2 (h262) but there From ff880997d50bd6640dc413b5bc3f060b612613fe Mon Sep 17 00:00:00 2001 From: think3r Date: Sat, 30 May 2026 23:05:17 +0800 Subject: [PATCH 06/55] Fix Y-axis range not updating when switching streams in Bitrate Plot When switching from video to audio stream in the Bitrate Plot, the Y-axis range stayed at the video scale (global max across all streams), making audio bitrate data invisible. Use getVisibleRange(Axis::Y) instead of model->getYRange() in both coordinate conversion functions so the Y-axis dynamically scales to the current visible stream's range. --- YUViewLib/src/ui/views/PlotViewWidget.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/YUViewLib/src/ui/views/PlotViewWidget.cpp b/YUViewLib/src/ui/views/PlotViewWidget.cpp index b8cf551db..884ff2d16 100644 --- a/YUViewLib/src/ui/views/PlotViewWidget.cpp +++ b/YUViewLib/src/ui/views/PlotViewWidget.cpp @@ -964,7 +964,10 @@ QPointF PlotViewWidget::convertPlotPosToPixelPos(const QPointF & plotPos, Range yRange = {0, 100}; if (this->model) - yRange = this->model->getYRange(); + { + auto visibleRange = getVisibleRange(Axis::Y); + yRange = visibleRange ? *visibleRange : this->model->getYRange(); + } const auto rangeY = double(yRange.max - yRange.min); const auto pixelPosX = this->propertiesAxis[0].line.p1().x() + @@ -1000,7 +1003,10 @@ QPointF PlotViewWidget::convertPixelPosToPlotPos(const QPointF & pixelPos, Range yRange = {0, 100}; if (this->model) - yRange = this->model->getYRange(); + { + auto visibleRange = getVisibleRange(Axis::Y); + yRange = visibleRange ? *visibleRange : this->model->getYRange(); + } const auto rangeY = double(yRange.max - yRange.min); const auto valueX = From 73214317a3aa819e07bbfe32b90ba1a21775834e Mon Sep 17 00:00:00 2001 From: think3r Date: Sat, 30 May 2026 23:09:42 +0800 Subject: [PATCH 07/55] Fix drag-and-drop regression in playlist Revert dragMoveEvent changes from commit 44d1d86 that broke two cases: 1. External file drops (from file manager) were rejected 2. Internal playlist reordering between items was broken Fix: Add hasUrls check early to accept external file drops before playlist validation, and delegate null-dropTarget events to QTreeWidget::dragMoveEvent for proper internal handling. --- YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp b/YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp index 447a9e6da..b9891c4a0 100644 --- a/YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp +++ b/YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp @@ -186,6 +186,12 @@ playlistItem *PlaylistTreeWidget::getDropTarget(const QPoint &pos) const void PlaylistTreeWidget::dragMoveEvent(QDragMoveEvent *event) { + if (event->mimeData()->hasUrls()) + { + event->acceptProposedAction(); + return; + } + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) const auto dropTarget = this->getDropTarget(event->position().toPoint()); #else @@ -195,6 +201,7 @@ void PlaylistTreeWidget::dragMoveEvent(QDragMoveEvent *event) if (!dropTarget) { event->ignore(); + QTreeWidget::dragMoveEvent(event); return; } From 7968a615a351f4c3a8a76f8fae70d01f2c805971 Mon Sep 17 00:00:00 2001 From: think3r Date: Sun, 31 May 2026 01:35:54 +0800 Subject: [PATCH 08/55] =?UTF-8?q?docs:=20=E6=89=A9=E5=B1=95=20ff=5Flibs.md?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E8=A7=A3=E7=A0=81=E5=99=A8=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=B8=8E=20Packet=20Analysis=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加解码器与外部库章节:decoderBase 继承层次、各解码器支持格式 - 添加 CI 预编译库支持:各平台 Qt/libde265/openSSL/Flatpak 分发情况 - 添加 Packet Analysis 解析架构:YUView 自有解析器 vs FFmpeg demux 角色划分 --- ff_libs.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/ff_libs.md b/ff_libs.md index 56e146c8f..63a06b725 100644 --- a/ff_libs.md +++ b/ff_libs.md @@ -32,3 +32,113 @@ ## 关键风险点 1. **AVPixFmtDescriptorWrapper.cpp** 和 **AVInputFormatWrapper.cpp** 是仅有的两个对未知版本静默失败的文件,可能导致后续空指针访问。 + +--- + +# 解码器与外部库 + +## 解码器架构 + +### 基类层次 + +``` +decoderBase (抽象基类) + │ + └── decoderFFmpeg ← 直接继承,使用 FFmpeg 多格式解码 + │ + └── decoderBaseSingleLib ← 通过 QLibrary 加载外部库 + │ + ├── decoderLibde265 (libde265) + ├── decoderHM (HM 参考软件) + ├── decoderVTM (VTM 参考软件) + ├── decoderVVDec (VVDec) + └── decoderDav1d (dav1d) +``` + +### 各解码器支持情况 + +| 解码器 | 底层库 | 支持格式 | 预编译库提供 | +|--------|--------|----------|-------------| +| `decoderFFmpeg` | FFmpeg | HEVC/AVC/VVC/AV1/MPEG-2/VP9 等 | **所有平台** | +| `decoderLibde265` | libde265 | HEVC | **Windows/macOS/Linux** (CI 下载) | +| `decoderDav1d` | dav1d | AV1 | 仅源码构建 | +| `decoderHM` | HM 参考软件 | HEVC | 仅源码构建 | +| `decoderVTM` | VTM 参考软件 | VVC | 仅源码构建 | +| `decoderVVDec` | VVDec | VVC | 仅源码构建 | + +--- + +# CI 预编译库支持 + +来自 `.github/workflows/Build.yml` 和 `flatpak.yml`。 + +## 按平台分发 + +| 平台 | Qt | libde265 | openSSL | Flatpak | +|------|-----|----------|---------|---------| +| **Ubuntu 22.04/24.04 (x64)** | apt / 预编译 | 预编译 .so | - | - | +| **Ubuntu 22.04/24.04 (ARM)** | apt | - | - | - | +| **macOS 15 (Apple Silicon/Intel)** | Homebrew / 预编译 | 预编译 .dylib | - | - | +| **Windows 2022** | 预编译 | 预编译 .dll | 预编译 | - | +| **Linux Flatpak** | - | - | - | ✅ | + +## CI Jobs + +| Job | 平台 | 构建目标 | +|-----|------|---------| +| `build-unix-native` | ubuntu-22.04/24.04 (x64 + ARM) | 本地测试 | +| `build-mac-native` | macos-15 (Arm64 + Intel) | 本地测试 | +| `build-linux-mac` | ubuntu-22.04 / macos-15 | .AppImage / .app.zip | +| `build-windows` | windows-2022 | .zip / .msi | +| `flatpak-builder` | ubuntu-latest (GNOME 50) | .flatpak | + +--- + +# Packet Analysis 解析架构 + +Packet Analysis 的数据**不是**由 FFmpeg 直接提供,而是由 YUView 自己的解析器完成。FFmpeg 仅作为容器格式的解复用器(demuxer)。 + +## 解析器层次 + +``` +Parser (基类) + │ + ├── ParserAnnexBAVC ──► 纯 YUView 解析 AVC/H.264 语法 + ├── ParserAnnexBHEVC ──► 纯 YUView 解析 HEVC/H.265 语法 + ├── ParserAnnexBVVC ──► 纯 YUView 解析 VVC/H.266 语法 + ├── ParserAV1OBU ──► 纯 YUView 解析 AV1 OBU 语法 + ├── ParserSubtitle ──► 纯 YUView 解析字幕 + │ + └── ParserAVFormat ──► FFmpeg 解复用 + YUView 深解析 +``` + +## ParserAVFormat 的工作流程 + +`ParserAVFormat`(`YUViewLib/src/parser/AVFormat/ParserAVFormat.cpp`)用于容器格式: + +1. **FFmpeg 解复用**:使用 `FileSourceFFmpegFile` 调用 `libavformat` 提取原始 packet +2. **YUView 深解析**:根据视频格式选择对应解析器 + - AVC/HEVC/MPEG2 → `ParserAnnexB*` 解析 NAL unit 内部语法 + - AV1 → `ParserAV1OBU` 解析 OBU + - 字幕 → YUView 自己的字幕解析器 + +## 输入格式与解析器映射 + +| InputFormat | 解析器 | 数据来源 | +|-------------|--------|---------| +| `AnnexBHEVC` | `ParserAnnexBHEVC` | YUView 纯解析 | +| `AnnexBVVC` | `ParserAnnexBVVC` | YUView 纯解析 | +| `AnnexBAVC` | `ParserAnnexBAVC` | YUView 纯解析 | +| `AnnexBMPEG2` | `ParserAnnexBMPEG2` | YUView 纯解析 | +| `Libav` | `ParserAVFormat` | FFmpeg demux + YUView 深解析 | +| `AV1` | `ParserAV1OBU` | YUView 纯解析 | + +## 关键文件 + +- UI: `YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp` +- 解析器基类: `YUViewLib/src/parser/Parser.h` +- FFmpeg 容器解析: `YUViewLib/src/parser/AVFormat/ParserAVFormat.cpp` +- AVC 解析: `YUViewLib/src/parser/AVC/` +- HEVC 解析: `YUViewLib/src/parser/HEVC/` +- VVC 解析: `YUViewLib/src/parser/VVC/` +- AV1 解析: `YUViewLib/src/parser/AV1/` \ No newline at end of file From 61547b49ccafdf7c794403233ba7d08de96cc4f8 Mon Sep 17 00:00:00 2001 From: think3r Date: Sun, 31 May 2026 02:05:36 +0800 Subject: [PATCH 09/55] Fix: Use distinct blue-grey color for raw bitstream in Packet Analysis Raw bitstream NAL units had streamIndex=-1 but no color assigned, causing black background. Instead of setting streamIndex=0 (which would incorrectly color raw streams the same as container stream 0), use a dedicated blue-grey color (#b0bec5) for idx==-1 case. This keeps raw bitstreams visually distinguishable from container streams. --- YUViewLib/src/parser/common/PacketItemModel.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/YUViewLib/src/parser/common/PacketItemModel.cpp b/YUViewLib/src/parser/common/PacketItemModel.cpp index 5b5e9b4d1..b31924ab0 100644 --- a/YUViewLib/src/parser/common/PacketItemModel.cpp +++ b/YUViewLib/src/parser/common/PacketItemModel.cpp @@ -62,7 +62,8 @@ auto streamIndexColors = std::vector({Color("#90caf9"), // blue (200) Color("#8e24aa"), // purple (600) Color("#00897b"), // teal (600) Color("#6d4c41"), // brown (600) - Color("#7cb342")}); // light green (600) + Color("#7cb342")}); // light green (600) +auto rawStreamColor = Color("#b0bec5"); // blue-grey (200) for raw bitstream (idx=-1) PacketItemModel::PacketItemModel(QObject *parent) : QAbstractItemModel(parent) { @@ -103,6 +104,8 @@ QVariant PacketItemModel::data(const QModelIndex &index, int role) const if (idx >= 0) return QVariant( QBrush(functionsGui::toQColor(streamIndexColors.at(idx % streamIndexColors.size())))); + else if (idx == -1) + return QVariant(QBrush(functionsGui::toQColor(rawStreamColor))); return QVariant(QBrush()); } else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) From 754fe5ec57e3685aa710eba9e17dfea740834bd0 Mon Sep 17 00:00:00 2001 From: think3r Date: Sun, 31 May 2026 02:12:19 +0800 Subject: [PATCH 10/55] Fix: VVC Bitrate Plot Type field was always empty VVC parser's createBitrateEntryForAU never set entry.frameType, leaving it as empty QString. The 'Type:' column in Bitrate Plot view showed nothing. - Add sliceTypes tracking to ParsingState::CurrentAU - Accumulate slice type counts when parsing slice NALs - Set entry.frameType using convertSliceCountsToString - Clear sliceTypes when new AU starts --- YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp | 12 ++++++++++-- YUViewLib/src/parser/VVC/ParserAnnexBVVC.h | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp index 1ca5163c8..e79a629b2 100644 --- a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp +++ b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp @@ -36,6 +36,8 @@ #include #include +#include + #include "SEI/buffering_period.h" #include "SEI/sei_message.h" #include "access_unit_delimiter_rbsp.h" @@ -85,8 +87,11 @@ createBitrateEntryForAU(ParsingState &parsingSta entry.dts = int(parsingState.currentAU.counter); entry.duration = 1; } - entry.bitrate = unsigned(parsingState.currentAU.sizeBytes); - entry.keyframe = parsingState.currentAU.isKeyframe; + entry.bitrate = unsigned(parsingState.currentAU.sizeBytes); + entry.keyframe = parsingState.currentAU.isKeyframe; + auto sliceTypes = convertSliceCountsToString(parsingState.currentAU.sliceTypes); + if (!sliceTypes.empty()) + entry.frameType = QString::fromStdString(sliceTypes); return entry; } @@ -480,6 +485,8 @@ ParserAnnexBVVC::parseAndAddNALUnit(int updatedParsingState.currentAU.isKeyframe = (nalType == NalType::IDR_W_RADL || nalType == NalType::IDR_N_LP || nalType == NalType::CRA_NUT); + updatedParsingState.currentAU.sliceTypes[std::string(SliceTypeMapper.getName( + newSliceLayer->slice_header_instance.sh_slice_type))]++; if (updatedParsingState.currentAU.isKeyframe) { nalVVC->rawData = data; @@ -562,6 +569,7 @@ ParserAnnexBVVC::parseAndAddNALUnit(int updatedParsingState.currentAU.fileStartEndPos = nalStartEndPosFile; updatedParsingState.currentAU.sizeBytes = 0; updatedParsingState.currentAU.counter++; + updatedParsingState.currentAU.sliceTypes.clear(); } else if (nalStartEndPosFile) { diff --git a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.h b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.h index 43edf03ef..8e22f98eb 100644 --- a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.h +++ b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.h @@ -68,6 +68,7 @@ struct ParsingState unsigned layerID{0}; bool isKeyframe{}; std::optional fileStartEndPos; + std::map sliceTypes; }; CurrentAU currentAU{}; From a94a7c95774f198c8dcf0b9574d972d964a860ce Mon Sep 17 00:00:00 2001 From: think3r Date: Sun, 31 May 2026 02:25:19 +0800 Subject: [PATCH 11/55] Fix: add mutex to FFmpeg log callback to prevent race condition crash Multiple threads (video loading and bitstream analysis) concurrently call avLogCallback() which appended to static logListFFmpeg without synchronization, causing heap corruption (double-free in realloc) and SIGABRT on macOS ARM64. --- YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp | 4 +++- YUViewLib/src/ffmpeg/FFmpegVersionHandler.h | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp b/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp index 714cf8bfe..0f60bf9f2 100644 --- a/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp +++ b/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp @@ -307,6 +307,7 @@ void FFmpegVersionHandler::flush_buffers(AVCodecContextWrapper &decCtx) } QStringList FFmpegVersionHandler::logListFFmpeg; +QMutex FFmpegVersionHandler::logListMutex; FFmpegVersionHandler::FFmpegVersionHandler() { @@ -323,7 +324,8 @@ void FFmpegVersionHandler::avLogCallback(void *, int level, const char *fmt, va_ { QString msg; msg.vasprintf(fmt, vargs); - auto now = QDateTime::currentDateTime(); + auto now = QDateTime::currentDateTime(); + QMutexLocker locker(&FFmpegVersionHandler::logListMutex); FFmpegVersionHandler::logListFFmpeg.append(now.toString("hh:mm:ss.zzz") + QString(" - L%1 - ").arg(level) + msg); } diff --git a/YUViewLib/src/ffmpeg/FFmpegVersionHandler.h b/YUViewLib/src/ffmpeg/FFmpegVersionHandler.h index b916a246f..dd8577d65 100644 --- a/YUViewLib/src/ffmpeg/FFmpegVersionHandler.h +++ b/YUViewLib/src/ffmpeg/FFmpegVersionHandler.h @@ -43,6 +43,7 @@ #include "AVPixFmtDescriptorWrapper.h" #include "FFMpegLibrariesTypes.h" #include "FFmpegLibraryFunctions.h" +#include #include namespace FFmpeg @@ -143,6 +144,7 @@ class FFmpegVersionHandler // FFmpeg has a callback where it loggs stuff. This log goes here. static QStringList logListFFmpeg; + static QMutex logListMutex; static void avLogCallback(void *ptr, int level, const char *fmt, va_list vargs); }; From d490e52527ed23547d1e2ba7227aa2c899703c1e Mon Sep 17 00:00:00 2001 From: think3r Date: Sun, 31 May 2026 15:55:19 +0800 Subject: [PATCH 12/55] fix: remove Up/Down global shortcuts to restore tree view keyboard navigation Qt::Key_Up and Qt::Key_Down were registered as window-level menu shortcuts for playlist navigation, which intercepted these keys application-wide and prevented the dataTreeView in Packet Analysis from handling them for node navigation. Remove the shortcuts from the menu actions. The existing keyPressEvent handler in MainWindow still handles these keys when the main window has direct focus. --- YUViewLib/src/ui/Mainwindow.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/YUViewLib/src/ui/Mainwindow.cpp b/YUViewLib/src/ui/Mainwindow.cpp index e0c6852b0..38da13fd5 100644 --- a/YUViewLib/src/ui/Mainwindow.cpp +++ b/YUViewLib/src/ui/Mainwindow.cpp @@ -402,13 +402,11 @@ void MainWindow::createMenusAndActions() addActionToMenu(playbackMenu, "Next Playlist Item", ui.playlistTreeWidget, - &PlaylistTreeWidget::onSelectNextItem, - Qt::Key_Down); + &PlaylistTreeWidget::onSelectNextItem); addActionToMenu(playbackMenu, "Previous Playlist Item", ui.playlistTreeWidget, - &PlaylistTreeWidget::selectPreviousItem, - Qt::Key_Up); + &PlaylistTreeWidget::selectPreviousItem); addActionToMenu(playbackMenu, "Next Frame", ui.playbackController, From 55b7e3deca270e04d9f3ee9cd5260ffe015a423e Mon Sep 17 00:00:00 2001 From: think3r Date: Sun, 31 May 2026 16:43:12 +0800 Subject: [PATCH 13/55] fix: reduce tree view Name column width from 600px to 400px --- YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp index a8868e733..028d511fd 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp @@ -294,7 +294,7 @@ void BitstreamAnalysisWidget::restartParsingOfCurrentItem() this->createAndConnectNewParser(this->currentCompressedVideo->getInputFormat()); this->ui.dataTreeView->setModel(this->parser->getPacketItemModel()); - this->ui.dataTreeView->setColumnWidth(0, 600); + this->ui.dataTreeView->setColumnWidth(0, 400); this->ui.dataTreeView->setColumnWidth(1, 100); this->ui.dataTreeView->setColumnWidth(2, 120); this->ui.plotViewWidget->setModel(this->parser->getBitratePlotModel()); From 31b59fd05be77315165589b9e2a7d6a24ec928a2 Mon Sep 17 00:00:00 2001 From: think3r Date: Sun, 31 May 2026 15:02:21 +0800 Subject: [PATCH 14/55] feat: add hex view to bitstream analysis with crash fix Add a hex dump viewer to the Packet Analysis tab that displays raw NAL bytes and highlights the byte range corresponding to the selected syntax element. Key changes: - New HexViewWidget (hex + ASCII display with highlight support) - TreeItem stores bitOffset per syntax element and raw NAL data on root - SubByteReaderLogging tracks bit position for each parsed field - BitstreamAnalysisWidget selection handler traverses via QModelIndex (not raw internalPointer) to avoid use-after-free crash - Proper proxy-to-source index mapping for FilterByStreamIndexProxyModel - Thread-safe childItems access with mutex in TreeItem --- .../parser/common/SubByteReaderLogging.cpp | 53 ++++-- YUViewLib/src/parser/common/TreeItem.h | 31 +++- .../ui/widgets/BitstreamAnalysisWidget.cpp | 166 ++++++++++++++++- .../src/ui/widgets/BitstreamAnalysisWidget.h | 6 + YUViewLib/src/ui/widgets/HexViewWidget.cpp | 169 ++++++++++++++++++ YUViewLib/src/ui/widgets/HexViewWidget.h | 27 +++ YUViewLib/ui/bitstreamAnalysisWidget.ui | 84 ++++++--- 7 files changed, 483 insertions(+), 53 deletions(-) create mode 100644 YUViewLib/src/ui/widgets/HexViewWidget.cpp create mode 100644 YUViewLib/src/ui/widgets/HexViewWidget.h diff --git a/YUViewLib/src/parser/common/SubByteReaderLogging.cpp b/YUViewLib/src/parser/common/SubByteReaderLogging.cpp index 41b348fec..2f5f1ac12 100644 --- a/YUViewLib/src/parser/common/SubByteReaderLogging.cpp +++ b/YUViewLib/src/parser/common/SubByteReaderLogging.cpp @@ -55,7 +55,8 @@ void checkAndLog(std::shared_ptr item, const std::string & symbolName, const Options & options, int64_t value, - const std::string & code) + const std::string & code, + size_t bitOffset) { CheckResult checkResult; for (auto &check : options.checkList) @@ -74,8 +75,9 @@ void checkAndLog(std::shared_ptr item, const bool isError = !checkResult; if (isError) meaning += " " + checkResult.errorMessage; - item->createChildItem( + auto child = item->createChildItem( symbolName, value, formatCoding(formatName, code.size()), code, meaning, isError); + child->setBitOffset(bitOffset); } if (!checkResult && checkResult.checkLevel == CheckLevel::Error) throw std::logic_error(checkResult.errorMessage); @@ -85,7 +87,8 @@ void checkAndLog(std::shared_ptr item, const std::string & byteName, const Options & options, ByteVector value, - const std::string & code) + const std::string & code, + size_t bitOffset) { // There are no range checks for ByteVectors. Also the meaningMap does nothing. if (item && !options.loggingDisabled) @@ -100,11 +103,13 @@ void checkAndLog(std::shared_ptr item, valueStream << "0x" << std::setfill('0') << std::setw(2) << std::hex << unsigned(c) << " (" << c << ")"; auto byteCode = code.substr(i * 8, 8); - item->createChildItem(byteName + (value.size() > 1 ? "[" + std::to_string(i) + "]" : ""), - valueStream.str(), - formatCoding("u(8)", code.size()), - byteCode, - options.meaningString); + auto child = item->createChildItem( + byteName + (value.size() > 1 ? "[" + std::to_string(i) + "]" : ""), + valueStream.str(), + formatCoding("u(8)", code.size()), + byteCode, + options.meaningString); + child->setBitOffset(bitOffset + i * 8); } } } @@ -153,6 +158,7 @@ SubByteReaderLogging::SubByteReaderLogging(const ByteVector & inArr, { if (item) { + item->setRawData(inArr); if (new_sub_item_name.empty()) this->currentTreeLevel = item; else @@ -191,8 +197,9 @@ uint64_t SubByteReaderLogging::readBits(const std::string &symbolName, { try { + auto startBit = this->nrBitsRead(); auto [value, code] = SubByteReader::readBits(numBits); - checkAndLog(this->currentTreeLevel, "u(v)", symbolName, options, value, code); + checkAndLog(this->currentTreeLevel, "u(v)", symbolName, options, value, code, startBit); return value; } catch (const std::exception &ex) @@ -205,8 +212,9 @@ bool SubByteReaderLogging::readFlag(const std::string &symbolName, const Options { try { + auto startBit = this->nrBitsRead(); auto [value, code] = SubByteReader::readBits(1); - checkAndLog(this->currentTreeLevel, "u(1)", symbolName, options, value, code); + checkAndLog(this->currentTreeLevel, "u(1)", symbolName, options, value, code, startBit); return (value != 0); } catch (const std::exception &ex) @@ -219,8 +227,9 @@ uint64_t SubByteReaderLogging::readUEV(const std::string &symbolName, const Opti { try { + auto startBit = this->nrBitsRead(); auto [value, code] = SubByteReader::readUE_V(); - checkAndLog(this->currentTreeLevel, "ue(v)", symbolName, options, value, code); + checkAndLog(this->currentTreeLevel, "ue(v)", symbolName, options, value, code, startBit); return value; } catch (const std::exception &ex) @@ -233,8 +242,9 @@ int64_t SubByteReaderLogging::readSEV(const std::string &symbolName, const Optio { try { + auto startBit = this->nrBitsRead(); auto [value, code] = SubByteReader::readSE_V(); - checkAndLog(this->currentTreeLevel, "se(v)", symbolName, options, value, code); + checkAndLog(this->currentTreeLevel, "se(v)", symbolName, options, value, code, startBit); return value; } catch (const std::exception &ex) @@ -247,8 +257,9 @@ uint64_t SubByteReaderLogging::readLEB128(const std::string &symbolName, const O { try { + auto startBit = this->nrBitsRead(); auto [value, code] = SubByteReader::readLEB128(); - checkAndLog(this->currentTreeLevel, "leb128(v)", symbolName, options, value, code); + checkAndLog(this->currentTreeLevel, "leb128(v)", symbolName, options, value, code, startBit); return value; } catch (const std::exception &ex) @@ -262,8 +273,9 @@ SubByteReaderLogging::readNS(const std::string &symbolName, uint64_t maxVal, con { try { + auto startBit = this->nrBitsRead(); auto [value, code] = SubByteReader::readNS(maxVal); - checkAndLog(this->currentTreeLevel, "ns(n)", symbolName, options, value, code); + checkAndLog(this->currentTreeLevel, "ns(n)", symbolName, options, value, code, startBit); return value; } catch (const std::exception &ex) @@ -277,8 +289,9 @@ SubByteReaderLogging::readSU(const std::string &symbolName, unsigned nrBits, con { try { + auto startBit = this->nrBitsRead(); auto [value, code] = SubByteReader::readSU(nrBits); - checkAndLog(this->currentTreeLevel, "su(n)", symbolName, options, value, code); + checkAndLog(this->currentTreeLevel, "su(n)", symbolName, options, value, code, startBit); return value; } catch (const std::exception &ex) @@ -296,8 +309,9 @@ ByteVector SubByteReaderLogging::readBytes(const std::string &symbolName, if (!this->byte_aligned()) throw std::logic_error("Trying to read bytes while not byte aligned."); + auto startBit = this->nrBitsRead(); auto [value, code] = SubByteReader::readBytes(nrBytes); - checkAndLog(this->currentTreeLevel, symbolName, options, value, code); + checkAndLog(this->currentTreeLevel, symbolName, options, value, code, startBit); return value; } catch (const std::exception &ex) @@ -310,7 +324,7 @@ void SubByteReaderLogging::logCalculatedValue(const std::string &symbolName, int64_t value, const Options & options) { - checkAndLog(this->currentTreeLevel, "Calc", symbolName, options, value, ""); + checkAndLog(this->currentTreeLevel, "Calc", symbolName, options, value, "", this->nrBitsRead()); } void SubByteReaderLogging::logArbitrary(const std::string &symbolName, @@ -320,7 +334,10 @@ void SubByteReaderLogging::logArbitrary(const std::string &symbolName, const std::string &meaning) { if (this->currentTreeLevel) - this->currentTreeLevel->createChildItem(symbolName, value, coding, code, meaning); + { + auto child = this->currentTreeLevel->createChildItem(symbolName, value, coding, code, meaning); + child->setBitOffset(this->nrBitsRead()); + } } void SubByteReaderLogging::stashAndReplaceCurrentTreeItem(std::shared_ptr newItem) diff --git a/YUViewLib/src/parser/common/TreeItem.h b/YUViewLib/src/parser/common/TreeItem.h index 3e0f47ffd..8e3f71be3 100644 --- a/YUViewLib/src/parser/common/TreeItem.h +++ b/YUViewLib/src/parser/common/TreeItem.h @@ -32,7 +32,10 @@ #pragma once +#include + #include +#include #include #include #include @@ -61,6 +64,11 @@ class TreeItem : public std::enable_shared_from_this void setError(bool isError = true) { this->error = isError; } bool isError() const { return this->error; } + void setBitOffset(size_t offset) { this->bitOffset = offset; } + [[nodiscard]] size_t getBitOffset() const { return this->bitOffset; } + void setRawData(const ByteVector &data) { this->rawData = data; } + [[nodiscard]] const std::optional &getRawData() const { return this->rawData; } + std::string getName(bool showStreamIndex) const { std::stringstream ss; @@ -94,7 +102,10 @@ class TreeItem : public std::enable_shared_from_this newItem->parent = this->weak_from_this(); newItem->setProperties(name, std::to_string(value), coding, code, meaning); newItem->error = isError; - this->childItems.push_back(newItem); + { + std::lock_guard lock(this->childItemsMutex); + this->childItems.push_back(newItem); + } return newItem; } @@ -109,11 +120,18 @@ class TreeItem : public std::enable_shared_from_this newItem->parent = this->weak_from_this(); newItem->setProperties(name, value, coding, code, meaning); newItem->error = isError; - this->childItems.push_back(newItem); + { + std::lock_guard lock(this->childItemsMutex); + this->childItems.push_back(newItem); + } return newItem; } - size_t getNrChildItems() const { return this->childItems.size(); } + size_t getNrChildItems() const + { + std::lock_guard lock(this->childItemsMutex); + return this->childItems.size(); + } std::string getData(unsigned idx) const { @@ -136,6 +154,7 @@ class TreeItem : public std::enable_shared_from_this const std::shared_ptr getChild(unsigned idx) const { + std::lock_guard lock(this->childItemsMutex); if (idx < this->childItems.size()) return this->childItems[idx]; return {}; @@ -145,6 +164,7 @@ class TreeItem : public std::enable_shared_from_this std::optional getIndexOfChildItem(std::shared_ptr child) const { + std::lock_guard lock(this->childItemsMutex); for (size_t i = 0; i < this->childItems.size(); i++) if (this->childItems[i] == child) return i; @@ -155,6 +175,7 @@ class TreeItem : public std::enable_shared_from_this private: std::vector> childItems; + mutable std::mutex childItemsMutex; std::weak_ptr parent{}; std::string name; @@ -164,6 +185,10 @@ class TreeItem : public std::enable_shared_from_this std::string meaning; bool error{}; + // The bit position within the parent NAL where this syntax element starts + size_t bitOffset{0}; + // Raw NAL bytes (set only for NAL root TreeItems) + std::optional rawData; // This is set for the first layer items in case of AVPackets int streamIndex{-1}; }; diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp index 028d511fd..aba0b9918 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp @@ -37,6 +37,9 @@ #include "parser/HEVC/ParserAnnexBHEVC.h" #include "parser/Mpeg2/ParserAnnexBMpeg2.h" #include "parser/VVC/ParserAnnexBVVC.h" +#include "parser/common/Functions.h" + +#include #define BITSTREAM_ANALYSIS_WIDGET_DEBUG_OUTPUT 0 #if BITSTREAM_ANALYSIS_WIDGET_DEBUG_OUTPUT @@ -69,9 +72,14 @@ this->connect(this->ui.bitratePlotOrderComboBox, this, &BitstreamAnalysisWidget::bitratePlotOrderComboBoxIndexChanged); this->connect(this->ui.bitratePlotStreamComboBox, - QOverload::of(&QComboBox::currentIndexChanged), - this, - &BitstreamAnalysisWidget::bitratePlotStreamComboBoxIndexChanged); + QOverload::of(&QComboBox::currentIndexChanged), + this, + &BitstreamAnalysisWidget::bitratePlotStreamComboBoxIndexChanged); + + this->connect(this->ui.showHexViewCheckBox, + &QCheckBox::toggled, + this, + &BitstreamAnalysisWidget::onShowHexViewToggled); this->currentSelectedItemsChanged(nullptr, nullptr, false); } @@ -234,13 +242,31 @@ void BitstreamAnalysisWidget::stopAndDeleteParserBlocking() this, &BitstreamAnalysisWidget::backgroundParsingDone); + // Explicitly disconnect the selection model's currentChanged signal. + // setModel(nullptr) uses deleteLater() on the old selection model, so the old + // app-level connection may still fire if the selection model emits currentChanged + // during teardown — use-after-free on the TreeItem* stored as internalPointer. + if (this->ui.dataTreeView->selectionModel()) + { + QObject::disconnect(this->ui.dataTreeView->selectionModel(), + &QItemSelectionModel::currentChanged, + this, + &BitstreamAnalysisWidget::onDataTreeViewSelectionChanged); + } + if (this->backgroundParserFuture.isRunning()) { DEBUG_ANALYSIS("BitstreamAnalysisWidget::stopAndDeleteParser stopping parser"); this->parser->setAbortParsing(); this->backgroundParserFuture.waitForFinished(); } + + this->ui.dataTreeView->setModel(nullptr); + this->ui.plotViewWidget->setModel(nullptr); + this->ui.hrdPlotWidget->setModel(nullptr); + this->parser.reset(); + this->currentHighlightNalRoot.reset(); DEBUG_ANALYSIS("BitstreamAnalysisWidget::stopAndDeleteParser parser stopped and deleted"); } @@ -300,6 +326,18 @@ void BitstreamAnalysisWidget::restartParsingOfCurrentItem() this->ui.plotViewWidget->setModel(this->parser->getBitratePlotModel()); this->ui.hrdPlotWidget->setModel(this->parser->getHRDPlotModel()); + this->currentHighlightNalRoot.reset(); + this->ui.hexViewWidget->clear(); + this->ui.hexViewWidget->setVisible(this->ui.showHexViewCheckBox->isChecked()); + + if (this->parser) + { + this->connect(this->ui.dataTreeView->selectionModel(), + &QItemSelectionModel::currentChanged, + this, + &BitstreamAnalysisWidget::onDataTreeViewSelectionChanged); + } + this->updateStreamInfo(); this->updateParsingStatusText(0); @@ -352,3 +390,125 @@ void BitstreamAnalysisWidget::showEvent(QShowEvent *event) this->restartParsingOfCurrentItem(); QWidget::showEvent(event); } + +void BitstreamAnalysisWidget::onShowHexViewToggled(bool checked) +{ + this->ui.hexViewWidget->setVisible(checked); +} + +struct NalRootResult +{ + TreeItem *nalRoot{}; + TreeItem *selectedItem{}; +}; + +static NalRootResult findNalRootItemByModelIndex(QModelIndex idx, + const QAbstractItemModel *model) +{ + NalRootResult result; + auto *firstItem = static_cast(idx.internalPointer()); + if (!firstItem) + return result; + result.selectedItem = firstItem; + + constexpr int maxDepth = 100; + for (int depth = 0; depth < maxDepth && idx.isValid(); depth++) + { + auto *item = static_cast(idx.internalPointer()); + if (item && item->getRawData().has_value()) + { + result.nalRoot = item; + return result; + } + idx = model->parent(idx); + } + return result; +} + +static size_t computeBitLength(TreeItem *item, size_t totalBits) +{ + auto parent = item->getParentItem().lock(); + if (!parent) + return totalBits; + + auto self = item->shared_from_this(); + auto idx = parent->getIndexOfChildItem(self); + if (!idx) + return totalBits; + + auto nextSibling = parent->getChild(unsigned(*idx + 1)); + if (nextSibling) + return nextSibling->getBitOffset() - item->getBitOffset(); + + auto code = item->getData(3); + if (!code.empty()) + return code.size(); + + return totalBits - item->getBitOffset(); +} + +void BitstreamAnalysisWidget::onDataTreeViewSelectionChanged(const QModelIndex ¤t, + const QModelIndex &) +{ + if (!current.isValid() || !this->parser) + { + this->currentHighlightNalRoot.reset(); + this->ui.hexViewWidget->clear(); + return; + } + + auto *proxyModel = qobject_cast(this->ui.dataTreeView->model()); + QModelIndex sourceIdx = proxyModel ? proxyModel->mapToSource(current) : current; + if (!sourceIdx.isValid()) + { + this->currentHighlightNalRoot.reset(); + this->ui.hexViewWidget->clear(); + return; + } + + auto *sourceModel = proxyModel ? proxyModel->sourceModel() : this->ui.dataTreeView->model(); + auto result = findNalRootItemByModelIndex(sourceIdx, sourceModel); + if (!result.nalRoot) + { + this->currentHighlightNalRoot.reset(); + this->ui.hexViewWidget->clear(); + return; + } + + std::shared_ptr nalRoot; + try + { + nalRoot = result.nalRoot->shared_from_this(); + } + catch (const std::bad_weak_ptr &) + { + this->currentHighlightNalRoot.reset(); + this->ui.hexViewWidget->clear(); + return; + } + + if (nalRoot.get() != this->currentHighlightNalRoot.get()) + this->currentHighlightNalRoot = nalRoot; + + const auto &rawData = nalRoot->getRawData().value(); + QByteArray byteData(reinterpret_cast(rawData.data()), int(rawData.size())); + this->ui.hexViewWidget->setData(byteData); + + if (result.selectedItem) + { + const auto coding = result.selectedItem->getData(2); + const auto code = result.selectedItem->getData(3); + if (!code.empty() && coding != "Calc") + { + const size_t totalBits = rawData.size() * 8; + const size_t bitStart = result.selectedItem->getBitOffset(); + if (bitStart < totalBits) + { + const size_t bitLen = computeBitLength(result.selectedItem, totalBits); + const int byteOff = int(bitStart / 8); + const int byteLen = std::max(1, int((bitLen + 7) / 8)); + this->ui.hexViewWidget->setHighlight(byteOff, byteLen); + } + } + } +} diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h index 8f98e6f33..00e8fc4b2 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h @@ -40,6 +40,7 @@ #include #include +#include "ui/widgets/HexViewWidget.h" #include "ui_bitstreamAnalysisWidget.h" class BitstreamAnalysisWidget : public QWidget { @@ -66,6 +67,9 @@ private slots: void bitratePlotOrderComboBoxIndexChanged(int index); void bitratePlotStreamComboBoxIndexChanged(int index); + void onDataTreeViewSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous); + void onShowHexViewToggled(bool checked); + protected: void hideEvent(QHideEvent *event) override; void showEvent(QShowEvent *event) override; @@ -87,6 +91,8 @@ private slots: QPointer currentCompressedVideo; + std::shared_ptr currentHighlightNalRoot; + // -1: Show all streams. Otherwise only show the given stream index. int showOnlyStream{-1}; // -1: Show all streams. Otherwise only show the given stream index. diff --git a/YUViewLib/src/ui/widgets/HexViewWidget.cpp b/YUViewLib/src/ui/widgets/HexViewWidget.cpp new file mode 100644 index 000000000..d7b7c5966 --- /dev/null +++ b/YUViewLib/src/ui/widgets/HexViewWidget.cpp @@ -0,0 +1,169 @@ +#include "HexViewWidget.h" + +#include +#include +#include + +static const int BYTES_PER_LINE = 16; + +HexViewWidget::HexViewWidget(QWidget *parent) + : QWidget(parent) +{ + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + this->view = new QPlainTextEdit(this); + this->view->setReadOnly(true); + this->view->setFocusPolicy(Qt::NoFocus); + this->view->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + this->view->setLineWrapMode(QPlainTextEdit::NoWrap); + this->view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + this->view->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + this->view->setStyleSheet("QPlainTextEdit { background: #1e1e1e; color: #d4d4d4; }"); + + layout->addWidget(this->view); +} + +void HexViewWidget::setData(const QByteArray &data) +{ + this->currentData = data; + this->highlightOffset = -1; + this->highlightLength = -1; + this->rebuildDisplay(); +} + +void HexViewWidget::setHighlight(int byteOffset, int byteLength) +{ + this->highlightOffset = byteOffset; + this->highlightLength = byteLength; + this->rebuildDisplay(); +} + +void HexViewWidget::clearHighlight() +{ + this->highlightOffset = -1; + this->highlightLength = -1; + this->rebuildDisplay(); +} + +void HexViewWidget::clear() +{ + this->currentData.clear(); + this->highlightOffset = -1; + this->highlightLength = -1; + if (!this->view) + return; + this->view->setExtraSelections(QList()); + this->view->clear(); +} + +void HexViewWidget::rebuildDisplay() +{ + if (!this->view) + return; + this->view->setExtraSelections(QList()); + this->view->clear(); + if (this->currentData.isEmpty()) + return; + + const auto *bytes = reinterpret_cast(this->currentData.constData()); + int size = this->currentData.size(); + + QString text; + text.reserve(size * 4 + size / BYTES_PER_LINE * 10); + + int lineStart = 0; + while (lineStart < size) + { + int lineEnd = qMin(lineStart + BYTES_PER_LINE, size); + int lineLen = lineEnd - lineStart; + + // Offset + text += QString("%1 ").arg(lineStart, 8, 16, QLatin1Char('0')); + + // Hex bytes + QString hexPart; + QString asciiPart; + + for (int i = 0; i < BYTES_PER_LINE; i++) + { + if (i < lineLen) + { + int byteIdx = lineStart + i; + hexPart += QString("%1").arg(bytes[byteIdx], 2, 16, QLatin1Char('0')).toUpper(); + unsigned char c = bytes[byteIdx]; + asciiPart += (c >= 32 && c <= 126) ? QChar(c) : QChar('.'); + } + else + { + hexPart += " "; + asciiPart += ' '; + } + if (i == BYTES_PER_LINE / 2 - 1) + hexPart += ' '; + hexPart += ' '; + } + + text += hexPart; + text += " |"; + text += asciiPart; + text += "|\n"; + + lineStart += BYTES_PER_LINE; + } + + this->view->setPlainText(text); + + if (this->highlightOffset >= 0 && this->highlightLength > 0) + { + int hiStart = this->highlightOffset; + int hiEnd = qMin(hiStart + this->highlightLength, size); + + QList selections; + + for (int i = hiStart; i < hiEnd; i++) + { + int lineIdx = i / BYTES_PER_LINE; + int colInLine = i % BYTES_PER_LINE; + + int linePos = lineIdx; + int colStart = colInLine; + + // Calculate position in plain text + QTextBlock block = this->view->document()->findBlockByNumber(linePos); + if (!block.isValid()) + continue; + + // Hex column position + // Format: "XXXXXXXX HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH |AAAA...|\n" + // where X = offset (8), then 2 spaces, then 16*3 hex chars + 1 separator space + int hexCol = 10 + colStart * 3; + if (colStart >= 8) + hexCol += 1; // extra space between groups + + QTextCursor cursor(block); + cursor.setPosition(block.position() + hexCol); + + auto sel = QTextEdit::ExtraSelection(); + sel.format.setBackground(QColor(0, 120, 215)); + sel.format.setForeground(QColor(255, 255, 255)); + sel.cursor = cursor; + sel.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 2); + selections.append(sel); + + // Also highlight ASCII (starts at pos 61: 10 prefix + 49 hex + 2 " |") + int asciiCol = 61 + colStart; + + QTextCursor asciiCursor(block); + asciiCursor.setPosition(block.position() + asciiCol); + + auto asciiSel = QTextEdit::ExtraSelection(); + asciiSel.format = sel.format; + asciiSel.cursor = asciiCursor; + asciiSel.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1); + selections.append(asciiSel); + } + + this->view->setExtraSelections(selections); + } +} diff --git a/YUViewLib/src/ui/widgets/HexViewWidget.h b/YUViewLib/src/ui/widgets/HexViewWidget.h new file mode 100644 index 000000000..f71e6fabe --- /dev/null +++ b/YUViewLib/src/ui/widgets/HexViewWidget.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +class HexViewWidget : public QWidget +{ + Q_OBJECT + +public: + HexViewWidget(QWidget *parent = nullptr); + + void setData(const QByteArray &data); + void setHighlight(int byteOffset, int byteLength); + void clearHighlight(); + void clear(); + +private: + void rebuildDisplay(); + + QPlainTextEdit *view; + QByteArray currentData; + + int highlightOffset{-1}; + int highlightLength{-1}; +}; diff --git a/YUViewLib/ui/bitstreamAnalysisWidget.ui b/YUViewLib/ui/bitstreamAnalysisWidget.ui index 3099f7580..705f470e1 100644 --- a/YUViewLib/ui/bitstreamAnalysisWidget.ui +++ b/YUViewLib/ui/bitstreamAnalysisWidget.ui @@ -76,22 +76,32 @@
- - - - By default, only a limite amount of data from the bitstream will be parsed to keep memory consumption low - - - - - - By default, only a limite amount of data from the bitstream will be parsed to keep memory consumption low - - - Parse Entire Bitstream - - - + + + + By default, only a limite amount of data from the bitstream will be parsed to keep memory consumption low + + + + + + By default, only a limite amount of data from the bitstream will be parsed to keep memory consumption low + + + Parse Entire Bitstream + + + + + + + Show Hex View + + + true + + + @@ -107,11 +117,21 @@
- - - - - + + + + Qt::Vertical + + + + + 80 + + + + + + Bitrate Plot @@ -185,14 +205,20 @@ - - - PlotViewWidget - QWidget -
ui/views/PlotViewWidget.h
- 1 -
-
+ + + PlotViewWidget + QWidget +
ui/views/PlotViewWidget.h
+ 1 +
+ + HexViewWidget + QWidget +
ui/widgets/HexViewWidget.h
+ 1 +
+
From e15a648a465af6e30e7105c37146bd5f30f179cf Mon Sep 17 00:00:00 2001 From: think3r Date: Sun, 31 May 2026 17:56:34 +0800 Subject: [PATCH 15/55] feat: add HexView with TreeView keyboard navigation improvements - Add hex view to bitstream analysis with crash fix - Add Enter key to expand/collapse tree view items - Center selected item on keyboard navigation (not mouse clicks) - Set TreeView/HexView splitter ratio to 3:2 - Fix HexView not showing for video stream top-level items - Fix continuous hex highlight for cross-byte fields --- .../ui/widgets/BitstreamAnalysisWidget.cpp | 62 +++++++++++++++ .../src/ui/widgets/BitstreamAnalysisWidget.h | 1 + YUViewLib/src/ui/widgets/HexViewWidget.cpp | 76 ++++++++++--------- 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp index aba0b9918..6436f6762 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp @@ -40,6 +40,7 @@ #include "parser/common/Functions.h" #include +#include #define BITSTREAM_ANALYSIS_WIDGET_DEBUG_OUTPUT 0 #if BITSTREAM_ANALYSIS_WIDGET_DEBUG_OUTPUT @@ -81,6 +82,11 @@ this->connect(this->ui.bitratePlotOrderComboBox, this, &BitstreamAnalysisWidget::onShowHexViewToggled); + this->ui.dataTreeView->installEventFilter(this); + + this->ui.packetAnalysisSplitter->setStretchFactor(0, 3); // TreeView + this->ui.packetAnalysisSplitter->setStretchFactor(1, 2); // HexView + this->currentSelectedItemsChanged(nullptr, nullptr, false); } @@ -396,12 +402,59 @@ void BitstreamAnalysisWidget::onShowHexViewToggled(bool checked) this->ui.hexViewWidget->setVisible(checked); } +bool BitstreamAnalysisWidget::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == this->ui.dataTreeView && event->type() == QEvent::KeyPress) + { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) + { + auto idx = this->ui.dataTreeView->currentIndex(); + if (idx.isValid()) + this->ui.dataTreeView->setExpanded(idx, !this->ui.dataTreeView->isExpanded(idx)); + this->ui.dataTreeView->scrollTo(this->ui.dataTreeView->currentIndex(), + QAbstractItemView::PositionAtCenter); + return true; + } + if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down || + keyEvent->key() == Qt::Key_Left || keyEvent->key() == Qt::Key_Right || + keyEvent->key() == Qt::Key_PageUp || keyEvent->key() == Qt::Key_PageDown || + keyEvent->key() == Qt::Key_Home || keyEvent->key() == Qt::Key_End) + { + event->accept(); + auto result = QWidget::eventFilter(watched, event); + QTimer::singleShot(0, this, [this]() { + auto idx = this->ui.dataTreeView->currentIndex(); + if (idx.isValid()) + this->ui.dataTreeView->scrollTo(idx, QAbstractItemView::PositionAtCenter); + }); + return result; + } + } + return QWidget::eventFilter(watched, event); +} + struct NalRootResult { TreeItem *nalRoot{}; TreeItem *selectedItem{}; }; +static TreeItem *findFirstDescendantWithRawData(TreeItem *item, int maxDepth) +{ + if (!item || maxDepth <= 0) + return {}; + for (unsigned i = 0; i < item->getNrChildItems(); i++) + { + auto child = item->getChild(i); + if (child && child->getRawData().has_value()) + return child.get(); + if (auto *found = findFirstDescendantWithRawData(child.get(), maxDepth - 1)) + return found; + } + return {}; +} + static NalRootResult findNalRootItemByModelIndex(QModelIndex idx, const QAbstractItemModel *model) { @@ -422,6 +475,14 @@ static NalRootResult findNalRootItemByModelIndex(QModelIndex idx, } idx = model->parent(idx); } + + if (auto *child = findFirstDescendantWithRawData(firstItem, maxDepth)) + { + result.nalRoot = child; + if (!result.selectedItem->getRawData().has_value()) + result.selectedItem = child; + } + return result; } @@ -511,4 +572,5 @@ void BitstreamAnalysisWidget::onDataTreeViewSelectionChanged(const QModelIndex & } } } + } diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h index 00e8fc4b2..0b6bca8b9 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.h @@ -73,6 +73,7 @@ private slots: protected: void hideEvent(QHideEvent *event) override; void showEvent(QShowEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; private: Ui::bitstreamAnalysisWidget ui; diff --git a/YUViewLib/src/ui/widgets/HexViewWidget.cpp b/YUViewLib/src/ui/widgets/HexViewWidget.cpp index d7b7c5966..8c2306236 100644 --- a/YUViewLib/src/ui/widgets/HexViewWidget.cpp +++ b/YUViewLib/src/ui/widgets/HexViewWidget.cpp @@ -116,51 +116,55 @@ void HexViewWidget::rebuildDisplay() if (this->highlightOffset >= 0 && this->highlightLength > 0) { - int hiStart = this->highlightOffset; - int hiEnd = qMin(hiStart + this->highlightLength, size); + int hiStart = this->highlightOffset; + int hiEnd = qMin(hiStart + this->highlightLength, size); + int lastByte = hiEnd - 1; + + int startLine = hiStart / BYTES_PER_LINE; + int startCol = hiStart % BYTES_PER_LINE; + int endLine = lastByte / BYTES_PER_LINE; + int endCol = lastByte % BYTES_PER_LINE; + + auto hexCol = [](int col) { + int c = 10 + col * 3; + if (col >= 8) + c += 1; + return c; + }; + + // Format: "XXXXXXXX HH HH ... HH HH ... HH |AAAA...|\n" + // offset(8) + " "(2) + hexPart(49) + " |"(2) = 61 + const int ASCII_START = 61; QList selections; - for (int i = hiStart; i < hiEnd; i++) - { - int lineIdx = i / BYTES_PER_LINE; - int colInLine = i % BYTES_PER_LINE; - - int linePos = lineIdx; - int colStart = colInLine; - - // Calculate position in plain text - QTextBlock block = this->view->document()->findBlockByNumber(linePos); - if (!block.isValid()) - continue; + QTextBlock startBlock = this->view->document()->findBlockByNumber(startLine); + QTextBlock endBlock = this->view->document()->findBlockByNumber(endLine); - // Hex column position - // Format: "XXXXXXXX HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH |AAAA...|\n" - // where X = offset (8), then 2 spaces, then 16*3 hex chars + 1 separator space - int hexCol = 10 + colStart * 3; - if (colStart >= 8) - hexCol += 1; // extra space between groups - - QTextCursor cursor(block); - cursor.setPosition(block.position() + hexCol); + if (startBlock.isValid() && endBlock.isValid()) + { + auto format = QTextCharFormat(); + format.setBackground(QColor(0, 120, 215)); + format.setForeground(QColor(255, 255, 255)); - auto sel = QTextEdit::ExtraSelection(); - sel.format.setBackground(QColor(0, 120, 215)); - sel.format.setForeground(QColor(255, 255, 255)); - sel.cursor = cursor; - sel.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 2); - selections.append(sel); + int startHexPos = startBlock.position() + hexCol(startCol); + int endHexPos = endBlock.position() + hexCol(endCol) + 2; - // Also highlight ASCII (starts at pos 61: 10 prefix + 49 hex + 2 " |") - int asciiCol = 61 + colStart; + auto hexSel = QTextEdit::ExtraSelection(); + hexSel.format = format; + hexSel.cursor = QTextCursor(this->view->document()); + hexSel.cursor.setPosition(startHexPos); + hexSel.cursor.setPosition(endHexPos, QTextCursor::KeepAnchor); + selections.append(hexSel); - QTextCursor asciiCursor(block); - asciiCursor.setPosition(block.position() + asciiCol); + int startAsciiPos = startBlock.position() + ASCII_START + startCol; + int endAsciiPos = endBlock.position() + ASCII_START + endCol + 1; auto asciiSel = QTextEdit::ExtraSelection(); - asciiSel.format = sel.format; - asciiSel.cursor = asciiCursor; - asciiSel.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1); + asciiSel.format = format; + asciiSel.cursor = QTextCursor(this->view->document()); + asciiSel.cursor.setPosition(startAsciiPos); + asciiSel.cursor.setPosition(endAsciiPos, QTextCursor::KeepAnchor); selections.append(asciiSel); } From c42dbec687caaa68834f829b0b65ee82fb5dd7ad Mon Sep 17 00:00:00 2001 From: think3r Date: Mon, 1 Jun 2026 00:12:23 +0800 Subject: [PATCH 16/55] fix: hex view highlight accuracy and code cleanup - Fix byteLen calculation for fields crossing byte boundaries (e.g. nuh_layer_id spanning bytes 0-1 now correctly highlights 2 bytes) - Fix computeBitLength TOCTOU: sanity-check nextSibling-derived bitLen against code.size() to guard against stale sibling lookup - Fix hex view cross-line highlight: split into per-line selections to prevent QTextCursor from merging hex and ASCII regions across lines - Remove unused Functions.h include from BitstreamAnalysisWidget - Remove dead SubByteReaderLogging(SubByteReader&,...) constructor - Fix UI indentation for parseEntireFileCheckBox and showHexViewCheckBox --- .../parser/common/SubByteReaderLogging.cpp | 14 ----- .../src/parser/common/SubByteReaderLogging.h | 3 -- .../ui/widgets/BitstreamAnalysisWidget.cpp | 38 ++++++++------ YUViewLib/src/ui/widgets/HexViewWidget.cpp | 30 +++++------ YUViewLib/ui/bitstreamAnalysisWidget.ui | 52 +++++++++---------- 5 files changed, 62 insertions(+), 75 deletions(-) diff --git a/YUViewLib/src/parser/common/SubByteReaderLogging.cpp b/YUViewLib/src/parser/common/SubByteReaderLogging.cpp index 2f5f1ac12..3b4325250 100644 --- a/YUViewLib/src/parser/common/SubByteReaderLogging.cpp +++ b/YUViewLib/src/parser/common/SubByteReaderLogging.cpp @@ -136,20 +136,6 @@ QByteArray SubByteReaderLogging::convertToQByteArray(ByteVector data) return ret; } -SubByteReaderLogging::SubByteReaderLogging(SubByteReader & reader, - std::shared_ptr item, - std::string new_sub_item_name) - : SubByteReader(reader) -{ - if (item) - { - if (new_sub_item_name.empty()) - this->currentTreeLevel = item; - else - this->currentTreeLevel = item->createChildItem(new_sub_item_name); - } -} - SubByteReaderLogging::SubByteReaderLogging(const ByteVector & inArr, std::shared_ptr item, std::string new_sub_item_name, diff --git a/YUViewLib/src/parser/common/SubByteReaderLogging.h b/YUViewLib/src/parser/common/SubByteReaderLogging.h index 535cc9e51..a6b96fa62 100644 --- a/YUViewLib/src/parser/common/SubByteReaderLogging.h +++ b/YUViewLib/src/parser/common/SubByteReaderLogging.h @@ -55,9 +55,6 @@ class SubByteReaderLogging : public SubByteReader { public: SubByteReaderLogging() = default; - SubByteReaderLogging(SubByteReader & reader, - std::shared_ptr item, - std::string new_sub_item_name = ""); SubByteReaderLogging(const ByteVector & inArr, std::shared_ptr item, std::string new_sub_item_name = "", diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp index 6436f6762..6f28a918a 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp @@ -37,8 +37,6 @@ #include "parser/HEVC/ParserAnnexBHEVC.h" #include "parser/Mpeg2/ParserAnnexBMpeg2.h" #include "parser/VVC/ParserAnnexBVVC.h" -#include "parser/common/Functions.h" - #include #include @@ -488,22 +486,28 @@ static NalRootResult findNalRootItemByModelIndex(QModelIndex idx, static size_t computeBitLength(TreeItem *item, size_t totalBits) { - auto parent = item->getParentItem().lock(); - if (!parent) - return totalBits; - - auto self = item->shared_from_this(); - auto idx = parent->getIndexOfChildItem(self); - if (!idx) - return totalBits; + auto code = item->getData(3); + const size_t codeLen = code.empty() ? 0 : code.size(); - auto nextSibling = parent->getChild(unsigned(*idx + 1)); - if (nextSibling) - return nextSibling->getBitOffset() - item->getBitOffset(); + auto parent = item->getParentItem().lock(); + if (parent) + { + auto self = item->shared_from_this(); + auto idx = parent->getIndexOfChildItem(self); + if (idx) + { + auto nextSibling = parent->getChild(unsigned(*idx + 1)); + if (nextSibling) + { + size_t bitLen = nextSibling->getBitOffset() - item->getBitOffset(); + if (bitLen > 0 && (codeLen == 0 || bitLen <= codeLen)) + return bitLen; + } + } + } - auto code = item->getData(3); - if (!code.empty()) - return code.size(); + if (codeLen > 0) + return codeLen; return totalBits - item->getBitOffset(); } @@ -567,7 +571,7 @@ void BitstreamAnalysisWidget::onDataTreeViewSelectionChanged(const QModelIndex & { const size_t bitLen = computeBitLength(result.selectedItem, totalBits); const int byteOff = int(bitStart / 8); - const int byteLen = std::max(1, int((bitLen + 7) / 8)); + const int byteLen = int((bitStart + bitLen + 7) / 8) - byteOff; this->ui.hexViewWidget->setHighlight(byteOff, byteLen); } } diff --git a/YUViewLib/src/ui/widgets/HexViewWidget.cpp b/YUViewLib/src/ui/widgets/HexViewWidget.cpp index 8c2306236..da08ad801 100644 --- a/YUViewLib/src/ui/widgets/HexViewWidget.cpp +++ b/YUViewLib/src/ui/widgets/HexViewWidget.cpp @@ -138,33 +138,33 @@ void HexViewWidget::rebuildDisplay() QList selections; - QTextBlock startBlock = this->view->document()->findBlockByNumber(startLine); - QTextBlock endBlock = this->view->document()->findBlockByNumber(endLine); + auto format = QTextCharFormat(); + format.setBackground(QColor(0, 120, 215)); + format.setForeground(QColor(255, 255, 255)); - if (startBlock.isValid() && endBlock.isValid()) + for (int line = startLine; line <= endLine; line++) { - auto format = QTextCharFormat(); - format.setBackground(QColor(0, 120, 215)); - format.setForeground(QColor(255, 255, 255)); + QTextBlock block = this->view->document()->findBlockByNumber(line); + if (!block.isValid()) + continue; - int startHexPos = startBlock.position() + hexCol(startCol); - int endHexPos = endBlock.position() + hexCol(endCol) + 2; + int colStart = (line == startLine) ? startCol : 0; + int colEnd = (line == endLine) ? endCol : (BYTES_PER_LINE - 1); auto hexSel = QTextEdit::ExtraSelection(); hexSel.format = format; hexSel.cursor = QTextCursor(this->view->document()); - hexSel.cursor.setPosition(startHexPos); - hexSel.cursor.setPosition(endHexPos, QTextCursor::KeepAnchor); + hexSel.cursor.setPosition(block.position() + hexCol(colStart)); + hexSel.cursor.setPosition(block.position() + hexCol(colEnd) + 2, + QTextCursor::KeepAnchor); selections.append(hexSel); - int startAsciiPos = startBlock.position() + ASCII_START + startCol; - int endAsciiPos = endBlock.position() + ASCII_START + endCol + 1; - auto asciiSel = QTextEdit::ExtraSelection(); asciiSel.format = format; asciiSel.cursor = QTextCursor(this->view->document()); - asciiSel.cursor.setPosition(startAsciiPos); - asciiSel.cursor.setPosition(endAsciiPos, QTextCursor::KeepAnchor); + asciiSel.cursor.setPosition(block.position() + ASCII_START + colStart); + asciiSel.cursor.setPosition(block.position() + ASCII_START + colEnd + 1, + QTextCursor::KeepAnchor); selections.append(asciiSel); } diff --git a/YUViewLib/ui/bitstreamAnalysisWidget.ui b/YUViewLib/ui/bitstreamAnalysisWidget.ui index 705f470e1..db5b40878 100644 --- a/YUViewLib/ui/bitstreamAnalysisWidget.ui +++ b/YUViewLib/ui/bitstreamAnalysisWidget.ui @@ -76,32 +76,32 @@ - - - - By default, only a limite amount of data from the bitstream will be parsed to keep memory consumption low - - - - - - By default, only a limite amount of data from the bitstream will be parsed to keep memory consumption low - - - Parse Entire Bitstream - - - - - - - Show Hex View - - - true - - - + + + + By default, only a limite amount of data from the bitstream will be parsed to keep memory consumption low + + + + + + By default, only a limite amount of data from the bitstream will be parsed to keep memory consumption low + + + Parse Entire Bitstream + + + + + + + Show Hex View + + + true + + + From 4b48231127bcb82ea02b32a15937185830ba1650 Mon Sep 17 00:00:00 2001 From: think3r Date: Mon, 1 Jun 2026 15:42:21 +0800 Subject: [PATCH 17/55] fix: clear HexView data when deleting playlist items --- YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp index 6f28a918a..dd1c8d7ff 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp @@ -318,6 +318,7 @@ void BitstreamAnalysisWidget::restartParsingOfCurrentItem() this->ui.dataTreeView->setModel(nullptr); this->ui.plotViewWidget->setModel(nullptr); this->ui.hrdPlotWidget->setModel(nullptr); + this->ui.hexViewWidget->clear(); return; } From 5297b28fc4ea6b2b6bce3f9d8a1d0a6010a34d59 Mon Sep 17 00:00:00 2001 From: think3r Date: Mon, 1 Jun 2026 16:53:32 +0800 Subject: [PATCH 18/55] feat(): Replace hardcoded colors with theme-aware palette colors across UI - Add #borderColor placeholder to QSS theme system and Simple Light/Blue theme variant - Preserve alpha channel in icon tinting; use system palette for default theme - Add Material Design 200 color palette for dark themes in PacketItemModel - Add theme-aware helper methods (getTextColor, getBackgroundColor, getGridColor, getHighlightColor) to PlotViewWidget and SplitViewWidget - Use palette colors in PlaybackController, SettingsDialog, ColorMapEditor, HexViewWidget, and PlaylistTreeWidget - Add PacketItemDelegate for blended selection colors in BitstreamAnalysis tree view - Update MainWindow to handle 5-color theme scheme --- YUViewLib/images/YUViewSimple.qss | 37 ++++++--- YUViewLib/src/common/Functions.cpp | 16 +++- YUViewLib/src/common/Functions.h | 2 +- YUViewLib/src/common/FunctionsGui.cpp | 32 +++++--- .../src/parser/common/PacketItemModel.cpp | 43 +++++++++- YUViewLib/src/ui/Mainwindow.cpp | 3 +- YUViewLib/src/ui/PlaybackController.cpp | 3 +- YUViewLib/src/ui/SettingsDialog.cpp | 5 +- .../StatisticsStyleControl_ColorMapEditor.cpp | 2 +- YUViewLib/src/ui/views/PlotViewWidget.cpp | 78 +++++++++++++------ YUViewLib/src/ui/views/PlotViewWidget.h | 6 ++ YUViewLib/src/ui/views/SplitViewWidget.cpp | 54 ++++++++----- YUViewLib/src/ui/views/SplitViewWidget.h | 4 + .../ui/widgets/BitstreamAnalysisWidget.cpp | 23 ++++++ YUViewLib/src/ui/widgets/HexViewWidget.cpp | 6 +- .../src/ui/widgets/PlaylistTreeWidget.cpp | 4 +- 16 files changed, 234 insertions(+), 84 deletions(-) diff --git a/YUViewLib/images/YUViewSimple.qss b/YUViewLib/images/YUViewSimple.qss index 7b22d3eb3..919913c10 100644 --- a/YUViewLib/images/YUViewSimple.qss +++ b/YUViewLib/images/YUViewSimple.qss @@ -4,6 +4,7 @@ The colors we use here: background: #backgroundColor text and borders: #activeColor (active) #inactiveColor (inavtive) active indication color: #highlightColor (used for hovering, active indication) + border color: #borderColor (used for widget borders) */ QToolTip { @@ -19,8 +20,8 @@ QWidget /* The default values for all widgets */ color: #activeColor; background-color: #backgroundColor; - /*selection-background-color:#DBA800;*/ - /*selection-color: #BC9000; */ + selection-background-color: #highlightColor; + selection-color: white; } QWidget:disabled @@ -28,6 +29,20 @@ QWidget:disabled color: #inactiveColor; } +QTreeView::item:selected, +QTreeWidget::item:selected +{ + background-color: #highlightColor; + color: white; +} + +QTreeView::item:selected:!active, +QTreeWidget::item:selected:!active +{ + background-color: #inactiveColor; + color: white; +} + QCheckBox, QRadioButton { @@ -76,7 +91,7 @@ QRadioButton::indicator:checked:disabled QPushButton { border-width: 1px; - border-color: #C6C6C6; + border-color: #borderColor; border-style: solid; padding: 2px; padding-left: 8px; @@ -90,7 +105,7 @@ QTreeWidget, QTextEdit { border-width: 1px; - border-color: #C6C6C6; + border-color: #borderColor; border-style: solid; padding: 2px; } @@ -148,7 +163,7 @@ QSpinBox QProgressBar { border-width: 1px; - border-color: #C6C6C6; + border-color: #borderColor; border-style: solid; } QProgressBar::chunk @@ -160,7 +175,7 @@ QProgressBar::chunk QDockWidget { background: #highlightColor; - color: black; + color: #activeColor; border: 1px solid orange; } @@ -210,12 +225,12 @@ QMenuBar::item:selected QMenuBar::item:pressed { background-color: #highlightColor; - color: black; + color: #activeColor; } /* The menu if opened */ QMenu { - border: 1px solid #C6C6C6; + border: 1px solid #borderColor; color: #activeColor; } QMenu::item:selected @@ -233,13 +248,13 @@ QHeaderView::section QFrame[frameShape="4"], QFrame[frameShape="5"] { - border: 1px solid #C6C6C6; + border: 1px solid #borderColor; background-color: #backgroundColor; } /* The slider */ QSlider::handle:horizontal { - border: 1px solid #C6C6C6; + border: 1px solid #borderColor; background-color: #backgroundColor; width: 0.5em; margin: -0.4em 0; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */ @@ -254,7 +269,7 @@ QSlider::handle:horizontal:hover } QSlider::groove:horizontal { - background-color: #C6C6C6; + background-color: #borderColor; height: 0.4em; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */ } QSlider::groove:horizontal:disabled diff --git a/YUViewLib/src/common/Functions.cpp b/YUViewLib/src/common/Functions.cpp index 534802bbb..9226a97b4 100644 --- a/YUViewLib/src/common/Functions.cpp +++ b/YUViewLib/src/common/Functions.cpp @@ -97,12 +97,14 @@ QStringList getThemeNameList() ret.append("Default"); ret.append("Simple Dark/Blue"); ret.append("Simple Dark/Orange"); + ret.append("Simple Light/Blue"); return ret; } QString getThemeFileName(QString themeName) { - if (themeName == "Simple Dark/Blue" || themeName == "Simple Dark/Orange") + if (themeName == "Simple Dark/Blue" || themeName == "Simple Dark/Orange" + || themeName == "Simple Light/Blue") return ":YUViewSimple.qss"; return ""; } @@ -113,12 +115,20 @@ QStringList getThemeColors(QString themeName) return QStringList() << "#262626" << "#E0E0E0" << "#808080" - << "#3daee9"; + << "#3daee9" + << "#C6C6C6"; if (themeName == "Simple Dark/Orange") return QStringList() << "#262626" << "#E0E0E0" << "#808080" - << "#FFC300 "; + << "#FFC300" + << "#C6C6C6"; + if (themeName == "Simple Light/Blue") + return QStringList() << "#F5F5F5" + << "#202020" + << "#A0A0A0" + << "#0078D7" + << "#C0C0C0"; return QStringList(); } diff --git a/YUViewLib/src/common/Functions.h b/YUViewLib/src/common/Functions.h index 39aad04af..9a26f89cf 100644 --- a/YUViewLib/src/common/Functions.h +++ b/YUViewLib/src/common/Functions.h @@ -56,7 +56,7 @@ QStringList getThemeNameList(); QString getThemeFileName(QString themeName); // For the given theme, return the primary colors to replace. // In the qss file, we can use tags, which will be replaced by these colors. The tags are: -// #backgroundColor, #activeColor, #inactiveColor, #highlightColor +// #backgroundColor, #activeColor, #inactiveColor, #highlightColor, #borderColor // The values to replace them by are returned in this order. QStringList getThemeColors(QString themeName); diff --git a/YUViewLib/src/common/FunctionsGui.cpp b/YUViewLib/src/common/FunctionsGui.cpp index e2fea6dc0..f8ca46be7 100644 --- a/YUViewLib/src/common/FunctionsGui.cpp +++ b/YUViewLib/src/common/FunctionsGui.cpp @@ -34,6 +34,8 @@ #include +#include +#include #include QColor functionsGui::toQColor(const Color &color) @@ -65,7 +67,7 @@ QIcon functionsGui::convertIcon(QString iconPath) // Get the active and inactive colors QStringList colors = functions::getThemeColors(themeName); QRgb activeColor, inActiveColor; - if (colors.size() == 4) + if (colors.size() >= 4) { QColor active(colors[1]); QColor inactive(colors[2]); @@ -74,29 +76,35 @@ QIcon functionsGui::convertIcon(QString iconPath) } else { - activeColor = qRgb(0, 0, 0); - inActiveColor = qRgb(128, 128, 128); + // Default theme: detect system palette colors + QPalette pal = QApplication::palette(); + QColor active = pal.color(QPalette::ButtonText); + QColor inactive = pal.color(QPalette::PlaceholderText); + if (!inactive.isValid()) + inactive = active.lighter(150); + activeColor = active.rgb(); + inActiveColor = inactive.rgb(); } - // Color the icon in the active/inactive colors + // Color the icon in the active/inactive colors, preserving alpha channel QImage input(iconPath); QImage active(input.size(), input.format()); QImage inActive(input.size(), input.format()); + active.fill(Qt::transparent); + inActive.fill(Qt::transparent); + for (int y = 0; y < input.height(); y++) { for (int x = 0; x < input.width(); x++) { QRgb in = input.pixel(x, y); - if (qAlpha(in) != 0) - { - active.setPixel(x, y, activeColor); - inActive.setPixel(x, y, inActiveColor); - } - else + int alpha = qAlpha(in); + if (alpha > 0) { - active.setPixel(x, y, in); - inActive.setPixel(x, y, in); + // Preserve alpha channel for smooth anti-aliasing + active.setPixel(x, y, qRgba(qRed(activeColor), qGreen(activeColor), qBlue(activeColor), alpha)); + inActive.setPixel(x, y, qRgba(qRed(inActiveColor), qGreen(inActiveColor), qBlue(inActiveColor), alpha)); } } } diff --git a/YUViewLib/src/parser/common/PacketItemModel.cpp b/YUViewLib/src/parser/common/PacketItemModel.cpp index b31924ab0..befc1a08f 100644 --- a/YUViewLib/src/parser/common/PacketItemModel.cpp +++ b/YUViewLib/src/parser/common/PacketItemModel.cpp @@ -36,7 +36,9 @@ #include #include +#include #include +#include #if PARSERCOMMON_DEBUG_FILTER_OUTPUT && !NDEBUG #include @@ -65,6 +67,34 @@ auto streamIndexColors = std::vector({Color("#90caf9"), // blue (200) Color("#7cb342")}); // light green (600) auto rawStreamColor = Color("#b0bec5"); // blue-grey (200) for raw bitstream (idx=-1) +// Softer variants for dark themes (Material Design 200 series) +auto streamIndexColorsDark = std::vector({Color("#90caf9"), // blue (200) + Color("#a5d6a7"), // green (200) + Color("#ffe082"), // amber (200) + Color("#ef9a9a"), // red (200) + Color("#fff59d"), // yellow (200) + Color("#ce93d8"), // purple (200) + Color("#80cbc4"), // teal (200) + Color("#bcaaa4"), // brown (200) + Color("#c5e1a5"), // light green (200) + Color("#9fa8da"), // indigo (200) + Color("#b39ddb"), // deep purple (200) + Color("#ffab91"), // deep orange (200) + Color("#ffe0b2"), // orange (200) + Color("#f48fb1"), // pink (200) + Color("#81d4fa"), // light blue (200) + Color("#b0bec5"), // blue grey (200) + Color("#80deea")}); // cyan (200) +auto rawStreamColorDark = Color("#b0bec5"); // blue-grey (200) for raw bitstream (idx=-1) + +static bool isDarkTheme() +{ + if (!qApp) + return false; + const auto windowColor = qApp->palette().color(QPalette::Window); + return windowColor.lightness() < 128; +} + PacketItemModel::PacketItemModel(QObject *parent) : QAbstractItemModel(parent) { } @@ -94,6 +124,8 @@ QVariant PacketItemModel::data(const QModelIndex &index, int role) const { if (item->isError()) return QVariant(QBrush(QColor(255, 0, 0))); + if (isDarkTheme()) + return QVariant(QBrush(QColor(40, 40, 40))); return QVariant(QBrush()); } if (role == Qt::BackgroundRole) @@ -102,10 +134,15 @@ QVariant PacketItemModel::data(const QModelIndex &index, int role) const return QVariant(QBrush()); const int idx = item->getStreamIndex(); if (idx >= 0) - return QVariant( - QBrush(functionsGui::toQColor(streamIndexColors.at(idx % streamIndexColors.size())))); + { + const auto &colors = isDarkTheme() ? streamIndexColorsDark : streamIndexColors; + return QVariant(QBrush(functionsGui::toQColor(colors.at(idx % colors.size())))); + } else if (idx == -1) - return QVariant(QBrush(functionsGui::toQColor(rawStreamColor))); + { + const auto &rawColor = isDarkTheme() ? rawStreamColorDark : rawStreamColor; + return QVariant(QBrush(functionsGui::toQColor(rawColor))); + } return QVariant(QBrush()); } else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) diff --git a/YUViewLib/src/ui/Mainwindow.cpp b/YUViewLib/src/ui/Mainwindow.cpp index 38da13fd5..c82c80b87 100644 --- a/YUViewLib/src/ui/Mainwindow.cpp +++ b/YUViewLib/src/ui/Mainwindow.cpp @@ -811,12 +811,13 @@ void MainWindow::updateSettings() // Now replace the placeholder color values with the real values QStringList colors = functions::getThemeColors(themeName); - if (colors.count() == 4) + if (colors.count() == 5) { styleSheet.replace("#backgroundColor", colors[0]); styleSheet.replace("#activeColor", colors[1]); styleSheet.replace("#inactiveColor", colors[2]); styleSheet.replace("#highlightColor", colors[3]); + styleSheet.replace("#borderColor", colors[4]); } else styleSheet.clear(); diff --git a/YUViewLib/src/ui/PlaybackController.cpp b/YUViewLib/src/ui/PlaybackController.cpp index a60b8b419..6c1ba29c3 100644 --- a/YUViewLib/src/ui/PlaybackController.cpp +++ b/YUViewLib/src/ui/PlaybackController.cpp @@ -559,7 +559,8 @@ void PlaybackController::goToNextFrame(const int nextFrameIndex) if (actualFramesPerSec > 0) this->ui.fpsLabel->setText(QString::number(actualFramesPerSec, 'f', 1)); if (this->playbackWasStalled) - this->ui.fpsLabel->setStyleSheet("QLabel { background-color: yellow }"); + this->ui.fpsLabel->setStyleSheet(QString("QLabel { background-color: %1 }") + .arg(palette().color(QPalette::Highlight).lighter(150).name())); else this->ui.fpsLabel->setStyleSheet(""); this->playbackWasStalled = false; diff --git a/YUViewLib/src/ui/SettingsDialog.cpp b/YUViewLib/src/ui/SettingsDialog.cpp index 339603eae..966f30781 100644 --- a/YUViewLib/src/ui/SettingsDialog.cpp +++ b/YUViewLib/src/ui/SettingsDialog.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include #include @@ -172,9 +173,9 @@ void SettingsDialog::initializeDefaults() if (!settings.contains("View/BackgroundColor")) settings.setValue("View/BackgroundColor", QColor(128, 128, 128)); if (!settings.contains("View/GridColor")) - settings.setValue("View/GridColor", QColor(0, 0, 0)); + settings.setValue("View/GridColor", QApplication::palette().color(QPalette::WindowText)); if (!settings.contains("Plot/BackgroundColor")) - settings.setValue("Plot/BackgroundColor", QColor(255, 255, 255)); + settings.setValue("Plot/BackgroundColor", QApplication::palette().color(QPalette::Base)); } unsigned int SettingsDialog::getCacheSizeInMB() const diff --git a/YUViewLib/src/ui/StatisticsStyleControl_ColorMapEditor.cpp b/YUViewLib/src/ui/StatisticsStyleControl_ColorMapEditor.cpp index 1cdf6e5d0..e68d4d40e 100644 --- a/YUViewLib/src/ui/StatisticsStyleControl_ColorMapEditor.cpp +++ b/YUViewLib/src/ui/StatisticsStyleControl_ColorMapEditor.cpp @@ -143,7 +143,7 @@ void StatisticsStyleControl_ColorMapEditor::on_pushButtonAdd_clicked() newItem->setData(Qt::EditRole, newValue); table->setItem(rowCount - 1, 0, newItem); newItem = new QTableWidgetItem(); - newItem->setBackground(QBrush(Qt::black)); + newItem->setBackground(QBrush(table->palette().color(QPalette::Base))); table->setItem(rowCount - 1, 1, newItem); // Add the "other" item at the last position again with the same color as it was before. diff --git a/YUViewLib/src/ui/views/PlotViewWidget.cpp b/YUViewLib/src/ui/views/PlotViewWidget.cpp index 884ff2d16..7d03f6b9b 100644 --- a/YUViewLib/src/ui/views/PlotViewWidget.cpp +++ b/YUViewLib/src/ui/views/PlotViewWidget.cpp @@ -55,8 +55,7 @@ const int tickLength = 3; const int tickToTextSpace = 2; const int fadeBoxThickness = 10; -const QColor gridLineMajor(180, 180, 180); -const QColor gridLineMinor(230, 230, 230); +// Grid line colors are determined dynamically based on theme PlotViewWidget::PlotViewWidget(QWidget *parent) : MoveAndZoomableView(parent) { @@ -165,7 +164,7 @@ void PlotViewWidget::zoomToFitInternal() this->update(); } -void drawTextInCenterOfArea(QPainter &painter, QRect area, QString text) +void drawTextInCenterOfArea(QPainter &painter, QRect area, QString text, const QColor &textColor, const QColor &bgColor) { // Set the QRect where to show the text QFont displayFont = painter.font(); @@ -178,8 +177,8 @@ void drawTextInCenterOfArea(QPainter &painter, QRect area, QString text) // Draw a rectangle around the text in white with a black border QRect boxRect = textRect + QMargins(5, 5, 5, 5); - painter.setPen(QPen(Qt::black, 1)); - painter.fillRect(boxRect, Qt::white); + painter.setPen(QPen(textColor, 1)); + painter.fillRect(boxRect, bgColor); painter.drawRect(boxRect); painter.drawText(textRect, Qt::AlignCenter, text); @@ -227,7 +226,7 @@ void PlotViewWidget::paintEvent(QPaintEvent *) if (this->model == nullptr) { - drawTextInCenterOfArea(painter, this->rect(), "Please select an item"); + drawTextInCenterOfArea(painter, this->rect(), "Please select an item", getTextColor(), getBackgroundColor()); } } @@ -307,7 +306,7 @@ void PlotViewWidget::setZoomFactor(double zoom) void PlotViewWidget::drawWhiteBoarders(QPainter &painter, const QRectF &widgetRect) const { - painter.setBrush(Qt::white); + painter.setBrush(getBackgroundColor()); painter.setPen(Qt::NoPen); painter.drawRect(QRectF(QPointF(0, 0), QPointF(this->plotRect.left(), widgetRect.bottom()))); painter.drawRect(QRectF(QPointF(0, 0), QPointF(widgetRect.right(), this->plotRect.top()))); @@ -385,7 +384,7 @@ PlotViewWidget::getAxisTicksToShow(const Axis axis, Range visibleRange) void PlotViewWidget::drawAxis(QPainter &painter) const { - painter.setPen(QPen(Qt::black, 1)); + painter.setPen(QPen(getTextColor(), 1)); painter.drawLine(this->plotRect.bottomLeft(), this->plotRect.topLeft()); painter.drawLine(this->plotRect.bottomLeft(), this->plotRect.bottomRight()); } @@ -400,7 +399,7 @@ void PlotViewWidget::drawAxisTicks(QPainter & paint const auto tickLine = (properties.axis == Axis::X) ? QPointF(0, tickLength) : QPointF(-tickLength, 0); - painter.setPen(QPen(Qt::black, 1)); + painter.setPen(QPen(getTextColor(), 1)); for (auto tick : ticks) { QPointF p = properties.line.p1(); @@ -420,7 +419,7 @@ void PlotViewWidget::drawAxisTickLabels(QPainter & if (ticks.isEmpty() || this->model == nullptr) return; - painter.setPen(QPen(Qt::black, 1)); + painter.setPen(QPen(getTextColor(), 1)); QFont displayFont = painter.font(); QFontMetricsF metrics(displayFont); @@ -520,7 +519,7 @@ void PlotViewWidget::drawGridLines(QPainter & painter drawEnd.setY(tick.pixelPosInWidget); } - painter.setPen(tick.minorTick ? gridLineMinor : gridLineMajor); + painter.setPen(tick.minorTick ? getGridColor(false) : getGridColor(true)); painter.drawLine(drawStart, drawEnd); } } @@ -537,8 +536,9 @@ void PlotViewWidget::drawFadeBoxes(QPainter &painter, const QRectF &widgetRect) painter.setPen(Qt::NoPen); - auto setGradientBrush = [&gradient, &painter](bool inverse) { - gradient.setColorAt(inverse ? 1 : 0, Qt::white); + const auto bgColor = getBackgroundColor(); + auto setGradientBrush = [&gradient, &painter, &bgColor](bool inverse) { + gradient.setColorAt(inverse ? 1 : 0, bgColor); gradient.setColorAt(inverse ? 0 : 1, Qt::transparent); painter.setBrush(gradient); }; @@ -560,7 +560,7 @@ void PlotViewWidget::drawFadeBoxes(QPainter &painter, const QRectF &widgetRect) void PlotViewWidget::drawWhiteBoxesInLabelArea(QPainter &painter, const QRectF &widgetRect) const { - painter.setBrush(Qt::white); + painter.setBrush(getBackgroundColor()); painter.setPen(Qt::NoPen); painter.drawRect(QRectF(this->plotRect.bottomRight(), widgetRect.bottomRight())); @@ -606,11 +606,11 @@ void PlotViewWidget::drawLimits(QPainter &painter) const textRect.setSize(textSize); textRect.moveTop(dummyPointForY.y()); textRect.moveRight(this->plotRect.right() - fadeBoxThickness); - painter.setPen(Qt::black); + painter.setPen(getTextColor()); painter.drawText(textRect, Qt::AlignCenter, limit.name); } - QPen limitPen(Qt::black); + QPen limitPen(getTextColor()); limitPen.setWidth(2); painter.setPen(limitPen); painter.drawLine(line); @@ -814,7 +814,8 @@ void PlotViewWidget::drawInfoBox(QPainter &painter) const // Create a QTextDocument. This object can tell us the size of the rendered text. QTextDocument textDocument; - textDocument.setDefaultStyleSheet("* { color: #000000 }"); + const auto textColor = getTextColor(); + textDocument.setDefaultStyleSheet(QString("* { color: %1 }").arg(textColor.name())); textDocument.setHtml(infoString); textDocument.setTextWidth(textDocument.size().width()); @@ -841,8 +842,9 @@ void PlotViewWidget::drawInfoBox(QPainter &painter) const // Draw a black rectangle and then the text on top of that QRect rect(QPoint(0, 0), textDocument.size().toSize() + QSize(2 * padding, 2 * padding)); QBrush originalBrush; - painter.setBrush(QColor(255, 255, 255, 150)); - painter.setPen(Qt::black); + const auto bgColor = getBackgroundColor(); + painter.setBrush(QColor(bgColor.red(), bgColor.green(), bgColor.blue(), 150)); + painter.setPen(getTextColor()); painter.drawRect(rect); painter.translate(padding, padding); textDocument.drawContents(&painter); @@ -871,7 +873,8 @@ void PlotViewWidget::drawDebugBox(QPainter &painter) const // Create a QTextDocument. This object can tell us the size of the rendered text. QTextDocument textDocument; - textDocument.setDefaultStyleSheet("* { color: #000000 }"); + const auto textColor = getTextColor(); + textDocument.setDefaultStyleSheet(QString("* { color: %1 }").arg(textColor.name())); textDocument.setHtml(infoString); textDocument.setTextWidth(textDocument.size().width()); @@ -882,8 +885,9 @@ void PlotViewWidget::drawDebugBox(QPainter &painter) const // Draw a black rectangle and then the text on top of that QRect rect(QPoint(0, 0), textDocument.size().toSize() + QSize(2 * padding, 2 * padding)); QBrush originalBrush; - painter.setBrush(QColor(255, 255, 255, 150)); - painter.setPen(Qt::black); + const auto bgColor = getBackgroundColor(); + painter.setBrush(QColor(bgColor.red(), bgColor.green(), bgColor.blue(), 150)); + painter.setPen(getTextColor()); painter.drawRect(rect); painter.translate(padding, padding); textDocument.drawContents(&painter); @@ -1095,7 +1099,7 @@ QRectF PlotViewWidget::getMaxLabelDrawSize(QPainter & painter, if (!this->model) return {}; - painter.setPen(QPen(Qt::black, 1)); + painter.setPen(QPen(getTextColor(), 1)); painter.setBrush(Qt::NoBrush); QFont displayFont = painter.font(); QFontMetricsF metrics(displayFont); @@ -1124,3 +1128,31 @@ void PlotViewWidget::initViewFromModel() this->update(); } } + +QColor PlotViewWidget::getTextColor() const +{ + const auto bg = palette().color(QPalette::Window); + return bg.lightness() < 128 ? Qt::white : Qt::black; +} + +QColor PlotViewWidget::getBackgroundColor() const +{ + return palette().color(QPalette::Window); +} + +QColor PlotViewWidget::getGridColor(bool major) const +{ + const auto bg = getBackgroundColor(); + if (bg.lightness() < 128) { + // Dark theme + return major ? QColor(80, 80, 80) : QColor(60, 60, 60); + } else { + // Light theme + return major ? QColor(180, 180, 180) : QColor(220, 220, 220); + } +} + +QColor PlotViewWidget::getHighlightColor() const +{ + return palette().color(QPalette::Highlight); +} diff --git a/YUViewLib/src/ui/views/PlotViewWidget.h b/YUViewLib/src/ui/views/PlotViewWidget.h index fdff26abc..ac22c76ee 100644 --- a/YUViewLib/src/ui/views/PlotViewWidget.h +++ b/YUViewLib/src/ui/views/PlotViewWidget.h @@ -127,4 +127,10 @@ private slots: bool viewInitializedForModel {false}; bool fixYAxis {true}; + + // Helper methods for theme-aware colors + QColor getTextColor() const; + QColor getBackgroundColor() const; + QColor getGridColor(bool major) const; + QColor getHighlightColor() const; }; diff --git a/YUViewLib/src/ui/views/SplitViewWidget.cpp b/YUViewLib/src/ui/views/SplitViewWidget.cpp index eaee99461..a18e2b111 100644 --- a/YUViewLib/src/ui/views/SplitViewWidget.cpp +++ b/YUViewLib/src/ui/views/SplitViewWidget.cpp @@ -180,8 +180,8 @@ void splitViewWidget::paintEvent(QPaintEvent *) // Draw a rectangle around the text in white with a black border QRect boxRect = textRect + QMargins(5, 5, 5, 5); - painter.setPen(QPen(Qt::black, 1)); - painter.fillRect(boxRect, Qt::white); + painter.setPen(QPen(getTextColor(), 1)); + painter.fillRect(boxRect, getBackgroundColor()); painter.drawRect(boxRect); painter.drawText(textRect, Qt::AlignCenter, text); @@ -467,14 +467,14 @@ void splitViewWidget::paintEvent(QPaintEvent *) triangle.lineTo(xSplit + 10, drawArea_botR.y()); triangle.closeSubpath(); - painter.fillPath(triangle, Qt::white); + painter.fillPath(triangle, getTextColor()); } else { // Draw the splitting line at position xSplit. All pixels left of the line // belong to the left view, and all pixels on the right belong to the right one. QLine line(xSplit, 0, xSplit, drawArea_botR.y()); - painter.setPen(Qt::white); + painter.setPen(getTextColor()); painter.drawLine(line); } } @@ -500,7 +500,7 @@ void splitViewWidget::paintEvent(QPaintEvent *) // Draw the zoom factor QString zoomString = QString("x") + QString::number(zoom, 'g', (zoom < 0.5) ? 4 : 2); painter.setRenderHint(QPainter::TextAntialiasing); - painter.setPen(QColor(Qt::black)); + painter.setPen(getTextColor()); painter.setFont(zoomFactorFont); painter.drawText(zoomFactorFontPos, zoomString); } @@ -662,7 +662,7 @@ void splitViewWidget::paintZoomBox(int view, zoomViewRect.moveBottomRight(drawArea_botR - QPoint(margin, margin)); // Fill the viewRect with the background color - painter.setPen(Qt::black); + painter.setPen(getTextColor()); painter.fillRect(zoomViewRect, painter.background()); // Restrict drawing to the zoom view rectangle. Save the old clipping region (if any) so we can @@ -732,7 +732,8 @@ void splitViewWidget::paintZoomBox(int view, // Create a QTextDocument. This object can tell us the size of the rendered text. QTextDocument textDocument; - textDocument.setDefaultStyleSheet("* { color: #000000 }"); + const auto textColor = getTextColor(); + textDocument.setDefaultStyleSheet(QString("* { color: %1 }").arg(textColor.name())); textDocument.setHtml(pixelInfoString); textDocument.setTextWidth(textDocument.size().width()); @@ -749,8 +750,9 @@ void splitViewWidget::paintZoomBox(int view, // Draw a black rectangle and then the text on top of that QRect rect(QPoint(0, 0), textDocument.size().toSize() + QSize(2 * padding, 2 * padding)); QBrush originalBrush; - painter.setBrush(QColor(0, 0, 0, 70)); - painter.setPen(Qt::black); + const auto bgColor = getBackgroundColor(); + painter.setBrush(QColor(bgColor.red(), bgColor.green(), bgColor.blue(), 200)); + painter.setPen(getTextColor()); painter.drawRect(rect); painter.translate(padding, padding); textDocument.drawContents(&painter); @@ -831,9 +833,9 @@ void splitViewWidget::paintPixelRulersX(QPainter &painter, { // Where is the x position of the pixel in the item on screen? int xPosOnScreen = x * zoom - videoRect.width() / 2 + worldTransform.x(); - painter.setPen(QPen(Qt::white)); + painter.setPen(QPen(getBackgroundColor())); painter.drawLine(xPosOnScreen, 0, xPosOnScreen, 5); - painter.setPen(QPen(Qt::black)); + painter.setPen(QPen(getTextColor())); painter.drawLine(xPosOnScreen + 1, 0, xPosOnScreen + 1, 5); // Draw the values (every fifth value, all values for zoom >= 128) @@ -848,9 +850,9 @@ void splitViewWidget::paintPixelRulersX(QPainter &painter, QRect textRect(rectPosTopLeft, rectSize); // Draw a white rect ... - painter.fillRect(textRect, Qt::white); + painter.fillRect(textRect, getBackgroundColor()); // ... and the text - painter.setPen(QPen(Qt::black)); + painter.setPen(QPen(getTextColor())); painter.drawText(textRect, Qt::AlignCenter, numberText); } } @@ -885,9 +887,9 @@ void splitViewWidget::paintPixelRulersY(QPainter &painter, for (int y = yMin; y < yMax + 1; y++) { int yPosOnScreen = y * zoom - videoRect.height() / 2 + worldTransform.y(); - painter.setPen(QPen(Qt::white)); + painter.setPen(QPen(getBackgroundColor())); painter.drawLine(xPos, yPosOnScreen, xPos + 5, yPosOnScreen); - painter.setPen(QPen(Qt::black)); + painter.setPen(QPen(getTextColor())); painter.drawLine(xPos, yPosOnScreen + 1, xPos + 5, yPosOnScreen + 1); // Draw the values (every fifth value, all values for zoom >= 128) @@ -902,9 +904,9 @@ void splitViewWidget::paintPixelRulersY(QPainter &painter, QRect textRect(rectPosTopLeft, rectSize); // Draw a white rect ... - painter.fillRect(textRect, Qt::white); + painter.fillRect(textRect, getBackgroundColor()); // ... and the text - painter.setPen(QPen(Qt::black)); + painter.setPen(QPen(getTextColor())); painter.drawText(textRect, Qt::AlignCenter, numberText); } } @@ -927,8 +929,8 @@ void splitViewWidget::drawLoadingMessage(QPainter *painter, const QPoint &pos) // Draw a rectangle around the text in white with a black border QRect boxRect = textRect + QMargins(5, 5, 5, 5); - painter->setPen(QPen(Qt::black, 1)); - painter->fillRect(boxRect, Qt::white); + painter->setPen(QPen(getTextColor(), 1)); + painter->fillRect(boxRect, getBackgroundColor()); painter->drawRect(boxRect); // Draw the text @@ -1939,8 +1941,8 @@ void splitViewWidget::drawItemPathAndName(QPainter *painter, int posX, int width // Draw a rectangle around the text in white with a black border QRect boxRect = textRect + QMargins(5, 5, 5, 5); - painter->setPen(QPen(Qt::black, 1)); - painter->fillRect(boxRect, Qt::white); + painter->setPen(QPen(getTextColor(), 1)); + painter->fillRect(boxRect, getBackgroundColor()); painter->drawRect(boxRect); // Draw the text @@ -2075,3 +2077,13 @@ void splitViewWidget::getStateFromMaster() MoveAndZoomableView::getStateFromMaster(); } + +QColor splitViewWidget::getTextColor() const +{ + return palette().color(QPalette::WindowText); +} + +QColor splitViewWidget::getBackgroundColor() const +{ + return palette().color(QPalette::Window); +} diff --git a/YUViewLib/src/ui/views/SplitViewWidget.h b/YUViewLib/src/ui/views/SplitViewWidget.h index c130194fe..1b9e97474 100644 --- a/YUViewLib/src/ui/views/SplitViewWidget.h +++ b/YUViewLib/src/ui/views/SplitViewWidget.h @@ -333,6 +333,10 @@ private slots: QPointer getOtherWidget() const; void getStateFromMaster() override; + + // Helper methods for theme-aware colors + QColor getTextColor() const; + QColor getBackgroundColor() const; }; #endif // SPLITVIEWWIDGET_H diff --git a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp index dd1c8d7ff..964e83132 100644 --- a/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp +++ b/YUViewLib/src/ui/widgets/BitstreamAnalysisWidget.cpp @@ -38,8 +38,30 @@ #include "parser/Mpeg2/ParserAnnexBMpeg2.h" #include "parser/VVC/ParserAnnexBVVC.h" #include +#include #include +class PacketItemDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + + void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override + { + QStyledItemDelegate::initStyleOption(option, index); + const auto bgData = index.data(Qt::BackgroundRole); + if (bgData.canConvert() && (option->state & QStyle::State_Selected)) + { + const auto streamColor = bgData.value().color(); + const auto highlight = option->palette.color(QPalette::Highlight); + option->backgroundBrush = QBrush(QColor( + (streamColor.red() + highlight.red()) / 2, + (streamColor.green() + highlight.green()) / 2, + (streamColor.blue() + highlight.blue()) / 2)); + } + } +}; + #define BITSTREAM_ANALYSIS_WIDGET_DEBUG_OUTPUT 0 #if BITSTREAM_ANALYSIS_WIDGET_DEBUG_OUTPUT #include @@ -325,6 +347,7 @@ void BitstreamAnalysisWidget::restartParsingOfCurrentItem() this->createAndConnectNewParser(this->currentCompressedVideo->getInputFormat()); this->ui.dataTreeView->setModel(this->parser->getPacketItemModel()); + this->ui.dataTreeView->setItemDelegate(new PacketItemDelegate(this->ui.dataTreeView)); this->ui.dataTreeView->setColumnWidth(0, 400); this->ui.dataTreeView->setColumnWidth(1, 100); this->ui.dataTreeView->setColumnWidth(2, 120); diff --git a/YUViewLib/src/ui/widgets/HexViewWidget.cpp b/YUViewLib/src/ui/widgets/HexViewWidget.cpp index da08ad801..522d9f14d 100644 --- a/YUViewLib/src/ui/widgets/HexViewWidget.cpp +++ b/YUViewLib/src/ui/widgets/HexViewWidget.cpp @@ -19,7 +19,7 @@ HexViewWidget::HexViewWidget(QWidget *parent) this->view->setLineWrapMode(QPlainTextEdit::NoWrap); this->view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); this->view->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); - this->view->setStyleSheet("QPlainTextEdit { background: #1e1e1e; color: #d4d4d4; }"); + // Let the global stylesheet handle colors (follows theme) layout->addWidget(this->view); } @@ -139,8 +139,8 @@ void HexViewWidget::rebuildDisplay() QList selections; auto format = QTextCharFormat(); - format.setBackground(QColor(0, 120, 215)); - format.setForeground(QColor(255, 255, 255)); + format.setBackground(palette().color(QPalette::Highlight)); + format.setForeground(palette().color(QPalette::HighlightedText)); for (int line = startLine; line <= endLine; line++) { diff --git a/YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp b/YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp index b9891c4a0..3fd940662 100644 --- a/YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp +++ b/YUViewLib/src/ui/widgets/PlaylistTreeWidget.cpp @@ -102,7 +102,7 @@ class bufferStatusWidget : public QWidget // This is the end of a block. Draw it int xStart = (int)((float)lastPos / range.second * s.width()); int xEnd = (int)((float)pos / range.second * s.width()); - painter.fillRect(xStart, 0, xEnd - xStart, s.height(), QColor(33, 150, 243)); + painter.fillRect(xStart, 0, xEnd - xStart, s.height(), this->palette().color(QPalette::Highlight)); // A new rectangle starts here lastPos = pos; @@ -111,7 +111,7 @@ class bufferStatusWidget : public QWidget // Draw the last rectangle that goes to the end of the list int xStart = (int)((float)lastPos / range.second * s.width()); int xEnd = (int)((float)frameList.last() / range.second * s.width()); - painter.fillRect(xStart, 0, xEnd - xStart, s.height(), QColor(33, 150, 243)); + painter.fillRect(xStart, 0, xEnd - xStart, s.height(), this->palette().color(QPalette::Highlight)); } // Draw the percentage as text From 020273f69f7778fbfc2abc87a28f0ad81edad941 Mon Sep 17 00:00:00 2001 From: think3r Date: Mon, 1 Jun 2026 17:57:17 +0800 Subject: [PATCH 19/55] chore(): add windows build.bat --- build.bat | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 build.bat diff --git a/build.bat b/build.bat new file mode 100644 index 000000000..ed8f11e38 --- /dev/null +++ b/build.bat @@ -0,0 +1,7 @@ +@echo off +call "C:\Program Files\Microsoft Visual Studio\18\Insiders\VC\Auxiliary\Build\vcvars64.bat" +if not exist build mkdir build +cd build +if not exist Makefile qmake CONFIG+=UNITTESTS .. +jom +pause From 8c6e100de267ae03e6e75bb963d6d4be00530700 Mon Sep 17 00:00:00 2001 From: think3r Date: Tue, 2 Jun 2026 16:40:15 +0800 Subject: [PATCH 20/55] chroe(): remove test form build.bat --- build.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.bat b/build.bat index ed8f11e38..8f020b75e 100644 --- a/build.bat +++ b/build.bat @@ -2,6 +2,6 @@ call "C:\Program Files\Microsoft Visual Studio\18\Insiders\VC\Auxiliary\Build\vcvars64.bat" if not exist build mkdir build cd build -if not exist Makefile qmake CONFIG+=UNITTESTS .. +if not exist Makefile qmake .. jom pause From 90d90a4a8ce4d4d54d75aae1ce9c050e73523e7c Mon Sep 17 00:00:00 2001 From: think3r Date: Tue, 2 Jun 2026 15:45:27 +0800 Subject: [PATCH 21/55] fix: restore Libde265 statistics overlay controls (Slice Index, Opacity, Motion Vector) Two issues fixed: 1. StatisticUIHandler.cpp - Revert QCheckBox signal to stateChanged: Commit 3e5e5f92 changed &QCheckBox::stateChanged to QCheckBoxStateChanged (which maps to &QCheckBox::checkStateChanged on Qt 6.7+). This broke the signal-slot connection to onStatisticsControlChanged(), causing all statistics checkboxes (Slice Index, Motion Vector, etc.) and Opacity sliders to become unresponsive. Reverting to &QCheckBox::stateChanged restores compatibility across all Qt versions. 2. playlistItemCompressedVideo.cpp - Fix loadStatistics seek logic: Setting currentFrameIdx[0] = -1 caused loadRawData() to potentially decode the entire bitstream instead of stopping at the target frame. Changed to frameToLoad - 1 to match the correct pattern used in seekToPosition(). --- .../src/playlistitem/playlistItemCompressedVideo.cpp | 2 +- YUViewLib/src/statistics/StatisticUIHandler.cpp | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/YUViewLib/src/playlistitem/playlistItemCompressedVideo.cpp b/YUViewLib/src/playlistitem/playlistItemCompressedVideo.cpp index 30dd3dfc5..8d06d4862 100644 --- a/YUViewLib/src/playlistitem/playlistItemCompressedVideo.cpp +++ b/YUViewLib/src/playlistitem/playlistItemCompressedVideo.cpp @@ -1116,7 +1116,7 @@ void playlistItemCompressedVideo::loadStatistics(int frameIdx) // Reload the current frame (force a seek and decode operation) int frameToLoad = this->currentFrameIdx[0]; - this->currentFrameIdx[0] = -1; + this->currentFrameIdx[0] = frameToLoad - 1; this->loadRawData(frameToLoad, false); // The statistics should now be loaded diff --git a/YUViewLib/src/statistics/StatisticUIHandler.cpp b/YUViewLib/src/statistics/StatisticUIHandler.cpp index 25567f537..a76dadb87 100644 --- a/YUViewLib/src/statistics/StatisticUIHandler.cpp +++ b/YUViewLib/src/statistics/StatisticUIHandler.cpp @@ -41,7 +41,6 @@ #include #include -#include #include #include @@ -97,8 +96,10 @@ QLayout *StatisticUIHandler::createStatisticsHandlerControls(bool recreateContro itemNameCheck->setChecked(statType.render); itemNameCheck->setToolTip(statType.description); ui.gridLayout->addWidget(itemNameCheck, int(row + 2), 0); - connect( - itemNameCheck, QCheckBoxStateChanged, this, &StatisticUIHandler::onStatisticsControlChanged); + connect(itemNameCheck, + &QCheckBox::stateChanged, + this, + &StatisticUIHandler::onStatisticsControlChanged); itemNameCheckBoxes[0].push_back(itemNameCheck); // Append the opacity slider @@ -160,7 +161,7 @@ QWidget *StatisticUIHandler::getSecondaryStatisticsHandlerControls(bool recreate itemNameCheck->setChecked(statType.render); ui2.gridLayout->addWidget(itemNameCheck, int(row + 2), 0); connect(itemNameCheck, - QCheckBoxStateChanged, + &QCheckBox::stateChanged, this, &StatisticUIHandler::onSecondaryStatisticsControlChanged); itemNameCheckBoxes[1].push_back(itemNameCheck); From e386bf542b2bd08f34a4a94a6c036eaf48338b3d Mon Sep 17 00:00:00 2001 From: think3r Date: Mon, 1 Jun 2026 15:31:27 +0800 Subject: [PATCH 22/55] feat(): add log support --- YUViewApp/YUViewApp.pro | 2 + YUViewLib/src/crash/CrashHandler.cpp | 235 ++++++++++ YUViewLib/src/crash/CrashHandler.h | 100 ++++ YUViewLib/src/crash/CrashHandlerMac.cpp | 241 ++++++++++ YUViewLib/src/crash/CrashHandlerWindows.cpp | 438 ++++++++++++++++++ YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp | 28 +- YUViewLib/src/logging/LogPanel.cpp | 172 +++++++ YUViewLib/src/logging/LogPanel.h | 80 ++++ YUViewLib/src/logging/Logger.cpp | 317 +++++++++++++ YUViewLib/src/logging/Logger.h | 130 ++++++ YUViewLib/src/ui/Mainwindow.cpp | 61 +++ YUViewLib/src/ui/Mainwindow.h | 3 + YUViewLib/src/ui/YUViewApplication.cpp | 34 ++ YUViewLib/src/ui/YUViewApplication.h | 5 + 14 files changed, 1842 insertions(+), 4 deletions(-) create mode 100644 YUViewLib/src/crash/CrashHandler.cpp create mode 100644 YUViewLib/src/crash/CrashHandler.h create mode 100644 YUViewLib/src/crash/CrashHandlerMac.cpp create mode 100644 YUViewLib/src/crash/CrashHandlerWindows.cpp create mode 100644 YUViewLib/src/logging/LogPanel.cpp create mode 100644 YUViewLib/src/logging/LogPanel.h create mode 100644 YUViewLib/src/logging/Logger.cpp create mode 100644 YUViewLib/src/logging/Logger.h diff --git a/YUViewApp/YUViewApp.pro b/YUViewApp/YUViewApp.pro index 561012031..0451981be 100644 --- a/YUViewApp/YUViewApp.pro +++ b/YUViewApp/YUViewApp.pro @@ -78,6 +78,8 @@ win32 { RC_FILE += images/WindowsAppIcon.rc SVNN = $$system("git describe --tags") DEFINES += NOMINMAX + # dbghelp: required for MiniDumpWriteDump and StackWalk64 in CrashHandlerWindows + LIBS += -ldbghelp } LASTHASH = $$system("git rev-parse HEAD") diff --git a/YUViewLib/src/crash/CrashHandler.cpp b/YUViewLib/src/crash/CrashHandler.cpp new file mode 100644 index 000000000..27b4b955c --- /dev/null +++ b/YUViewLib/src/crash/CrashHandler.cpp @@ -0,0 +1,235 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset + * + * Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + * + * You must obey the GNU General Public License in all respects for all + * of the code used other than OpenSSL. If you modify file(s) with this + * exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do + * so, delete this exception statement from your version. If you delete + * this exception statement from all source files in the program, then + * also delete it here. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "CrashHandler.h" + +#include +#include +#include +#include +#include + +#include +#include + +#if defined(Q_OS_WIN) || defined(_WIN32) +// clang-format off +# include +# include // _get_osfhandle +// clang-format on +#else +# include +# include +#endif + +// --------------------------------------------------------------------------- +// Static storage +// --------------------------------------------------------------------------- + +QString CrashHandler::s_logDir; +QString CrashHandler::s_appVersion; + +// --------------------------------------------------------------------------- +// Singleton +// --------------------------------------------------------------------------- + +CrashHandler &CrashHandler::instance() +{ + static CrashHandler inst; + return inst; +} + +// --------------------------------------------------------------------------- +// init +// --------------------------------------------------------------------------- + +void CrashHandler::init(const QString &logDir) +{ + s_logDir = logDir; + s_appVersion = qApp ? qApp->applicationVersion() : QString("unknown"); + + installPlatformHandlers(); + + // std::set_terminate – catches uncaught C++ exceptions on all platforms. + std::set_terminate([]() + { + // Try to get the exception type name. + const char *reason = "Uncaught C++ exception (unknown type)"; + std::string typeMsg; + try + { + std::rethrow_exception(std::current_exception()); + } + catch (const std::exception &e) + { + typeMsg = std::string("Uncaught C++ exception: ") + typeid(e).name() + ": " + e.what(); + reason = typeMsg.c_str(); + } + catch (...) + { + // Unidentified exception – keep default reason. + } + +#if defined(Q_OS_WIN) || defined(_WIN32) + // On Windows we open the file with CreateFileA to stay async-safe. + const QString path = s_logDir + "/yuview_crash_terminate_" + + QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss") + ".log"; + const QByteArray pathBytes = path.toLocal8Bit(); + HANDLE hFile = CreateFileA(pathBytes.constData(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + if (hFile != INVALID_HANDLE_VALUE) + { + DWORD written = 0; + // Use wf (not write) to avoid collision with the CRT ::write symbol. + auto wf = [&](const char *msg) + { + WriteFile(hFile, msg, static_cast(strlen(msg)), &written, nullptr); + }; + wf("=== YUView Crash Report (terminate) ===\n"); + wf("Reason: "); + wf(reason); + wf("\n"); + wf("======================================\n"); + CloseHandle(hFile); + } +#else + const QString path = s_logDir + "/yuview_crash_terminate_" + + QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss") + ".log"; + const QByteArray pathBytes = path.toUtf8(); + int fd = ::open(pathBytes.constData(), + O_WRONLY | O_CREAT | O_TRUNC, + 0644); + if (fd >= 0) + { + writeCrashLogHeader(fd, reason); + writeCrashLogTail(fd); + ::close(fd); + } +#endif + + // Re-raise default behaviour (produces core / Windows crash dialog). + std::abort(); + }); +} + +// --------------------------------------------------------------------------- +// Pending crash report helpers +// --------------------------------------------------------------------------- + +QString CrashHandler::crashLogDirectory() +{ + return s_logDir; +} + +QString CrashHandler::appVersion() +{ + return s_appVersion; +} + +bool CrashHandler::hasPendingCrashReport() +{ + if (s_logDir.isEmpty()) + return false; + QDir dir(s_logDir); + return !dir.entryList({"yuview_crash_*.log"}, QDir::Files).isEmpty(); +} + +QString CrashHandler::lastCrashReportPath() +{ + if (s_logDir.isEmpty()) + return {}; + QDir dir(s_logDir); + const QFileInfoList files = + dir.entryInfoList({"yuview_crash_*.log"}, QDir::Files, QDir::Time); + if (files.isEmpty()) + return {}; + return files.first().absoluteFilePath(); +} + +void CrashHandler::archiveCrashReport() +{ + const QString src = lastCrashReportPath(); + if (src.isEmpty()) + return; + // Rename: yuview_crash_XXX.log → yuview_crash_XXX.log.seen + QFile::rename(src, src + ".seen"); +} + +// --------------------------------------------------------------------------- +// writeCrashLogHeader / writeCrashLogTail +// These are called from signal handlers so they MUST use only +// async-signal-safe functions (write, not printf/fwrite). +// --------------------------------------------------------------------------- + +static void safeWrite(int fd, const char *s) +{ + if (fd < 0 || !s) + return; +#if defined(Q_OS_WIN) || defined(_WIN32) + DWORD written = 0; + HANDLE hFile = reinterpret_cast(_get_osfhandle(fd)); + WriteFile(hFile, s, static_cast(strlen(s)), &written, nullptr); +#else + size_t remaining = strlen(s); + while (remaining > 0) + { + ssize_t n = ::write(fd, s, remaining); + if (n <= 0) + break; + s += n; + remaining -= static_cast(n); + } +#endif +} + +void CrashHandler::writeCrashLogHeader(int fd, const char *reason) +{ + safeWrite(fd, "=== YUView Crash Report ===\n"); + safeWrite(fd, "Reason: "); + safeWrite(fd, reason ? reason : "(unknown)"); + safeWrite(fd, "\n"); + safeWrite(fd, "Version: "); + safeWrite(fd, s_appVersion.toUtf8().constData()); + safeWrite(fd, "\n"); + safeWrite(fd, "===========================\n"); + safeWrite(fd, "Stack Trace:\n"); +} + +void CrashHandler::writeCrashLogTail(int fd) +{ + safeWrite(fd, "===========================\n"); +} diff --git a/YUViewLib/src/crash/CrashHandler.h b/YUViewLib/src/crash/CrashHandler.h new file mode 100644 index 000000000..5bcca917d --- /dev/null +++ b/YUViewLib/src/crash/CrashHandler.h @@ -0,0 +1,100 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset + * + * Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + * + * You must obey the GNU General Public License in all respects for all + * of the code used other than OpenSSL. If you modify file(s) with this + * exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do + * so, delete this exception statement from your version. If you delete + * this exception statement from all source files in the program, then + * also delete it here. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +// --------------------------------------------------------------------------- +// CrashHandler +// +// Cross-platform crash / unhandled-exception handler. +// +// • Windows: SetUnhandledExceptionFilter + MiniDumpWriteDump (dbghelp.dll) +// + StackWalk64 for a text stack trace in the crash log. +// • macOS / Linux: sigaction for SIGSEGV / SIGABRT / SIGBUS / SIGFPE / SIGILL +// + backtrace() + backtrace_symbols_fd() for the stack trace. +// • All platforms: std::set_terminate() to catch uncaught C++ exceptions. +// +// The handler writes two artefacts to the log directory: +// yuview_crash_.log – human-readable summary + stack trace +// yuview_crash_.dmp – Windows minidump (Windows only) +// +// On the next launch, CrashHandler::hasPendingCrashReport() returns true and +// lastCrashReportPath() gives the path so the UI can inform the user. +// +// Usage: +// CrashHandler::instance().init(logDirectory); +// --------------------------------------------------------------------------- + +class CrashHandler +{ +public: + static CrashHandler &instance(); + + // Install all handlers. logDir must already exist. + void init(const QString &logDir); + + // Returns true if a crash log from the previous run is present. + static bool hasPendingCrashReport(); + + // Path to the most recent crash log (newest yuview_crash_*.log). + static QString lastCrashReportPath(); + + // Archive (rename) the pending crash log so it is not shown again. + static void archiveCrashReport(); + + // The directory where crash logs are written (set during init). + static QString crashLogDirectory(); + + // App version string set during init – used by platform crash handlers. + static QString appVersion(); + +private: + CrashHandler() = default; + ~CrashHandler() = default; + + CrashHandler(const CrashHandler &) = delete; + CrashHandler &operator=(const CrashHandler &) = delete; + + static void installPlatformHandlers(); + + // Shared helper: write the crash log header and tail. + // Called from platform-specific handlers after collecting the trace. + static void writeCrashLogHeader(int fd, const char *reason); + static void writeCrashLogTail(int fd); + + // Directory set during init – stored in a static so signal handlers + // (which cannot use member variables safely) can access it. + static QString s_logDir; + static QString s_appVersion; +}; diff --git a/YUViewLib/src/crash/CrashHandlerMac.cpp b/YUViewLib/src/crash/CrashHandlerMac.cpp new file mode 100644 index 000000000..cbad195a7 --- /dev/null +++ b/YUViewLib/src/crash/CrashHandlerMac.cpp @@ -0,0 +1,241 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset + * + * Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + * + * You must obey the GNU General Public License in all respects for all + * of the code used other than OpenSSL. If you modify file(s) with this + * exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do + * so, delete this exception statement from your version. If you delete + * this exception statement from all source files in the program, then + * also delete it here. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// This file is compiled on macOS and Linux only. +#if !defined(_WIN32) && !defined(Q_OS_WIN) + +# include "CrashHandler.h" + +# include +# include +# include +# include +# include +# include + +# include +# include + +// --------------------------------------------------------------------------- +// Internal helpers (all async-signal-safe unless noted) +// --------------------------------------------------------------------------- + +namespace +{ + +// Previous signal handlers so we can chain to the OS default. +static struct sigaction g_oldSIGSEGV{}; +static struct sigaction g_oldSIGABRT{}; +static struct sigaction g_oldSIGBUS{}; +static struct sigaction g_oldSIGFPE{}; +static struct sigaction g_oldSIGILL{}; + +// Guard against recursive signals (e.g. SIGSEGV inside the handler). +static volatile sig_atomic_t g_handlerActive = 0; + +// async-signal-safe: write a NUL-terminated string to fd. +static void safeWrite(int fd, const char *s) +{ + if (fd < 0 || !s) + return; + size_t remaining = strlen(s); + while (remaining > 0) + { + ssize_t n = ::write(fd, s, remaining); + if (n <= 0) + break; + s += n; + remaining -= static_cast(n); + } +} + +// async-signal-safe: write an integer as decimal. +static void safeWriteInt(int fd, long v) +{ + if (v < 0) + { + safeWrite(fd, "-"); + v = -v; + } + char buf[24]; + int idx = 0; + if (v == 0) + { + buf[idx++] = '0'; + } + else + { + while (v > 0) + { + buf[idx++] = '0' + static_cast(v % 10); + v /= 10; + } + // reverse + for (int i = 0, j = idx - 1; i < j; ++i, --j) + { + char tmp = buf[i]; + buf[i] = buf[j]; + buf[j] = tmp; + } + } + buf[idx] = '\0'; + safeWrite(fd, buf); +} + +// Build a timestamped crash-log path into `out` (async-signal-safe). +// `out` must be at least 512 bytes. +static void buildCrashLogPath(char *out, int outLen, const char *logDir) +{ + // Use clock_gettime – async-signal-safe. + struct timespec ts{}; + clock_gettime(CLOCK_REALTIME, &ts); + + // Convert seconds to broken-down time using gmtime_r (async-signal-safe). + struct tm tm_info{}; + time_t sec = ts.tv_sec; + gmtime_r(&sec, &tm_info); + + char tsStr[32]{}; + // Format: YYYYMMDD_HHmmss + snprintf(tsStr, sizeof(tsStr), + "%04d%02d%02d_%02d%02d%02d", + tm_info.tm_year + 1900, + tm_info.tm_mon + 1, + tm_info.tm_mday, + tm_info.tm_hour, + tm_info.tm_min, + tm_info.tm_sec); + + snprintf(out, static_cast(outLen), + "%s/yuview_crash_%s.log", logDir, tsStr); +} + +static const char *signalName(int sig) +{ + switch (sig) + { + case SIGSEGV: return "SIGSEGV (Segmentation Fault)"; + case SIGABRT: return "SIGABRT (Abort)"; + case SIGBUS: return "SIGBUS (Bus Error)"; + case SIGFPE: return "SIGFPE (Floating Point Exception)"; + case SIGILL: return "SIGILL (Illegal Instruction)"; + default: return "Unknown Signal"; + } +} + +// --------------------------------------------------------------------------- +// The actual signal handler +// --------------------------------------------------------------------------- +static void yuviewSignalHandler(int sig, siginfo_t * /*info*/, void * /*ctx*/) +{ + // Prevent re-entrance. + if (g_handlerActive) + return; + g_handlerActive = 1; + + const QString &logDir = CrashHandler::crashLogDirectory(); + const QByteArray logDirBytes = logDir.toUtf8(); + + char path[512]{}; + buildCrashLogPath(path, sizeof(path), logDirBytes.constData()); + + int fd = ::open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) + { + safeWrite(fd, "=== YUView Crash Report ===\n"); + safeWrite(fd, "Signal: "); + safeWrite(fd, signalName(sig)); + safeWrite(fd, "\n"); + safeWrite(fd, "PID: "); + safeWriteInt(fd, static_cast(getpid())); + safeWrite(fd, "\n"); + safeWrite(fd, "===========================\n"); + safeWrite(fd, "Stack Trace:\n"); + + // backtrace / backtrace_symbols_fd are async-signal-safe on most platforms. + void *frames[64]{}; + int count = backtrace(frames, 64); + backtrace_symbols_fd(frames, count, fd); + + safeWrite(fd, "===========================\n"); + ::close(fd); + } + + // Re-raise to let the OS produce a core file / crash report. + struct sigaction *old = nullptr; + switch (sig) + { + case SIGSEGV: old = &g_oldSIGSEGV; break; + case SIGABRT: old = &g_oldSIGABRT; break; + case SIGBUS: old = &g_oldSIGBUS; break; + case SIGFPE: old = &g_oldSIGFPE; break; + case SIGILL: old = &g_oldSIGILL; break; + default: break; + } + + if (old) + { + // Restore old handler then re-raise so the default action runs. + sigaction(sig, old, nullptr); + } + else + { + // Restore SIG_DFL and re-raise. + struct sigaction sa{}; + sa.sa_handler = SIG_DFL; + sigaction(sig, &sa, nullptr); + } + + ::raise(sig); +} + +} // anonymous namespace + +// --------------------------------------------------------------------------- +// CrashHandler::installPlatformHandlers (POSIX implementation) +// --------------------------------------------------------------------------- + +void CrashHandler::installPlatformHandlers() +{ + struct sigaction sa{}; + sa.sa_sigaction = yuviewSignalHandler; + sa.sa_flags = SA_SIGINFO | SA_RESETHAND; + sigemptyset(&sa.sa_mask); + + sigaction(SIGSEGV, &sa, &g_oldSIGSEGV); + sigaction(SIGABRT, &sa, &g_oldSIGABRT); + sigaction(SIGBUS, &sa, &g_oldSIGBUS); + sigaction(SIGFPE, &sa, &g_oldSIGFPE); + sigaction(SIGILL, &sa, &g_oldSIGILL); +} + +#endif // !_WIN32 diff --git a/YUViewLib/src/crash/CrashHandlerWindows.cpp b/YUViewLib/src/crash/CrashHandlerWindows.cpp new file mode 100644 index 000000000..3dcc14152 --- /dev/null +++ b/YUViewLib/src/crash/CrashHandlerWindows.cpp @@ -0,0 +1,438 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset + * + * Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + * + * You must obey the GNU General Public License in all respects for all + * of the code used other than OpenSSL. If you modify file(s) with this + * exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do + * so, delete this exception statement from your version. If you delete + * this exception statement from all source files in the program, then + * also delete it here. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// This file is compiled ONLY on Windows. +#if defined(Q_OS_WIN) || defined(_WIN32) + +# include "CrashHandler.h" + +// clang-format off +# include +# include +# include +// clang-format on + +# include + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +namespace +{ + +// A small fixed-size text buffer for the crash-log path that can be used +// without any heap allocation inside the exception filter. +static char g_crashLogPath[MAX_PATH * 2] = {}; +static char g_dumpPath[MAX_PATH * 2] = {}; + +// Write a DWORD as a decimal string into dest (no CRT needed). +static void dwordToStr(DWORD v, char *dest, int destLen) +{ + if (destLen <= 0) + return; + char tmp[20]; + int idx = 0; + if (v == 0) + { + tmp[idx++] = '0'; + } + else + { + while (v > 0 && idx < 19) + { + tmp[idx++] = '0' + static_cast(v % 10); + v /= 10; + } + } + // reverse + int out = 0; + for (int i = idx - 1; i >= 0 && out < destLen - 1; --i) + dest[out++] = tmp[i]; + dest[out] = '\0'; +} + +static void uint64ToHex(DWORD64 v, char *dest, int destLen) +{ + static const char hex[] = "0123456789ABCDEF"; + if (destLen < 19) + return; + dest[0] = '0'; + dest[1] = 'x'; + for (int i = 0; i < 16; ++i) + dest[2 + i] = hex[(v >> (60 - i * 4)) & 0xF]; + dest[18] = '\0'; +} + +// WriteFile wrapper (no CRT). +static void fileWrite(HANDLE hFile, const char *s) +{ + if (hFile == INVALID_HANDLE_VALUE || !s) + return; + DWORD written = 0; + WriteFile(hFile, s, static_cast(strlen(s)), &written, nullptr); +} + +// --------------------------------------------------------------------------- +// WriteMiniDump +// --------------------------------------------------------------------------- +static void writeMiniDump(EXCEPTION_POINTERS *ep, const char *dumpPath) +{ + // Load MiniDumpWriteDump dynamically so the app can still run if dbghelp + // is not installed (it always is on modern Windows, but be safe). + HMODULE hDbgHelp = LoadLibraryA("dbghelp.dll"); + if (!hDbgHelp) + return; + + using MiniDumpWriteDump_t = BOOL(WINAPI *)(HANDLE, + DWORD, + HANDLE, + MINIDUMP_TYPE, + PMINIDUMP_EXCEPTION_INFORMATION, + PMINIDUMP_USER_STREAM_INFORMATION, + PMINIDUMP_CALLBACK_INFORMATION); + auto pMiniDumpWriteDump = + reinterpret_cast(GetProcAddress(hDbgHelp, "MiniDumpWriteDump")); + + if (pMiniDumpWriteDump) + { + HANDLE hDump = CreateFileA(dumpPath, + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + if (hDump != INVALID_HANDLE_VALUE) + { + MINIDUMP_EXCEPTION_INFORMATION mei{}; + mei.ThreadId = GetCurrentThreadId(); + mei.ExceptionPointers = ep; + mei.ClientPointers = FALSE; + + pMiniDumpWriteDump(GetCurrentProcess(), + GetCurrentProcessId(), + hDump, + MiniDumpWithDataSegs, + &mei, + nullptr, + nullptr); + CloseHandle(hDump); + } + } + FreeLibrary(hDbgHelp); +} + +// --------------------------------------------------------------------------- +// WriteStackTrace (uses StackWalk64 from dbghelp) +// --------------------------------------------------------------------------- +static void writeStackTrace(HANDLE hFile, EXCEPTION_POINTERS *ep) +{ + HMODULE hDbgHelp = LoadLibraryA("dbghelp.dll"); + if (!hDbgHelp) + { + fileWrite(hFile, "(dbghelp.dll unavailable – no stack trace)\n"); + return; + } + + using SymInitialize_t = BOOL(WINAPI *)(HANDLE, PCSTR, BOOL); + using SymCleanup_t = BOOL(WINAPI *)(HANDLE); + using StackWalk64_t = BOOL(WINAPI *)(DWORD, + HANDLE, + HANDLE, + LPSTACKFRAME64, + PVOID, + PREAD_PROCESS_MEMORY_ROUTINE64, + PFUNCTION_TABLE_ACCESS_ROUTINE64, + PGET_MODULE_BASE_ROUTINE64, + PTRANSLATE_ADDRESS_ROUTINE64); + using SymFunctionTableAccess64_t = PVOID(WINAPI *)(HANDLE, DWORD64); + using SymGetModuleBase64_t = DWORD64(WINAPI *)(HANDLE, DWORD64); + using SymFromAddr_t = BOOL(WINAPI *)(HANDLE, DWORD64, PDWORD64, PSYMBOL_INFO); + using SymGetLineFromAddr64_t = BOOL(WINAPI *)(HANDLE, DWORD64, PDWORD, PIMAGEHLP_LINE64); + + auto pSymInitialize = reinterpret_cast(GetProcAddress(hDbgHelp, "SymInitialize")); + auto pSymCleanup = reinterpret_cast(GetProcAddress(hDbgHelp, "SymCleanup")); + auto pStackWalk64 = reinterpret_cast(GetProcAddress(hDbgHelp, "StackWalk64")); + auto pSymFunctionTableAccess64 = reinterpret_cast(GetProcAddress(hDbgHelp, "SymFunctionTableAccess64")); + auto pSymGetModuleBase64 = reinterpret_cast(GetProcAddress(hDbgHelp, "SymGetModuleBase64")); + auto pSymFromAddr = reinterpret_cast(GetProcAddress(hDbgHelp, "SymFromAddr")); + auto pSymGetLineFromAddr64 = reinterpret_cast(GetProcAddress(hDbgHelp, "SymGetLineFromAddr64")); + + if (!pSymInitialize || !pStackWalk64) + { + fileWrite(hFile, "(StackWalk64 unavailable)\n"); + FreeLibrary(hDbgHelp); + return; + } + + HANDLE hProcess = GetCurrentProcess(); + HANDLE hThread = GetCurrentThread(); + + pSymInitialize(hProcess, nullptr, TRUE); + + CONTEXT ctx{}; + ctx = *ep->ContextRecord; + + STACKFRAME64 frame{}; + DWORD machineType = IMAGE_FILE_MACHINE_AMD64; + +# if defined(_M_IX86) + machineType = IMAGE_FILE_MACHINE_I386; + frame.AddrPC.Offset = ctx.Eip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = ctx.Ebp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = ctx.Esp; + frame.AddrStack.Mode = AddrModeFlat; +# elif defined(_M_X64) + machineType = IMAGE_FILE_MACHINE_AMD64; + frame.AddrPC.Offset = ctx.Rip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = ctx.Rsp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = ctx.Rsp; + frame.AddrStack.Mode = AddrModeFlat; +# elif defined(_M_ARM64) + machineType = IMAGE_FILE_MACHINE_ARM64; + frame.AddrPC.Offset = ctx.Pc; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = ctx.Fp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = ctx.Sp; + frame.AddrStack.Mode = AddrModeFlat; +# endif + + // Symbol info buffer + constexpr int SYM_NAME_LEN = 256; + alignas(SYMBOL_INFO) char symBuf[sizeof(SYMBOL_INFO) + SYM_NAME_LEN * sizeof(TCHAR)]{}; + auto *sym = reinterpret_cast(symBuf); + sym->SizeOfStruct = sizeof(SYMBOL_INFO); + sym->MaxNameLen = SYM_NAME_LEN; + + int frameIdx = 0; + while (pStackWalk64(machineType, + hProcess, + hThread, + &frame, + &ctx, + nullptr, + pSymFunctionTableAccess64, + pSymGetModuleBase64, + nullptr) + && frameIdx < 64) + { + char addrBuf[20]; + uint64ToHex(frame.AddrPC.Offset, addrBuf, sizeof(addrBuf)); + + char idxBuf[8]; + dwordToStr(static_cast(frameIdx), idxBuf, sizeof(idxBuf)); + + fileWrite(hFile, "#"); + fileWrite(hFile, idxBuf); + fileWrite(hFile, " "); + fileWrite(hFile, addrBuf); + fileWrite(hFile, " "); + + DWORD64 displacement = 0; + if (pSymFromAddr && pSymFromAddr(hProcess, frame.AddrPC.Offset, &displacement, sym)) + { + fileWrite(hFile, sym->Name); + + if (pSymGetLineFromAddr64) + { + DWORD lineDisp = 0; + IMAGEHLP_LINE64 line{}; + line.SizeOfStruct = sizeof(IMAGEHLP_LINE64); + if (pSymGetLineFromAddr64(hProcess, frame.AddrPC.Offset, &lineDisp, &line)) + { + fileWrite(hFile, " ("); + fileWrite(hFile, line.FileName); + fileWrite(hFile, ":"); + char lineBuf[12]; + dwordToStr(line.LineNumber, lineBuf, sizeof(lineBuf)); + fileWrite(hFile, lineBuf); + fileWrite(hFile, ")"); + } + } + } + else + { + // No symbol – print module name if possible + char modName[MAX_PATH] = "(unknown)"; + DWORD64 modBase = pSymGetModuleBase64 ? pSymGetModuleBase64(hProcess, frame.AddrPC.Offset) : 0; + if (modBase) + { + HMODULE hMod = reinterpret_cast(modBase); + GetModuleFileNameA(hMod, modName, MAX_PATH); + // Keep only filename + char *slash = strrchr(modName, '\\'); + if (slash) + fileWrite(hFile, slash + 1); + else + fileWrite(hFile, modName); + } + else + { + fileWrite(hFile, "(no symbol)"); + } + } + + fileWrite(hFile, "\n"); + ++frameIdx; + } + + if (pSymCleanup) + pSymCleanup(hProcess); + + FreeLibrary(hDbgHelp); +} + +// --------------------------------------------------------------------------- +// Unhandled exception filter +// --------------------------------------------------------------------------- +static LONG WINAPI yuviewExceptionFilter(EXCEPTION_POINTERS *ep) +{ + // Build timestamp string without CRT date/time functions. + // We use GetLocalTime which is documented safe here. + SYSTEMTIME st{}; + GetLocalTime(&st); + + char ts[32]{}; + // Format: YYYYMMDD_HHmmss + { + auto appendPadded = [](char *buf, int &pos, WORD v, int digits) + { + char tmp[8]; + for (int d = digits - 1; d >= 0; --d) + { + tmp[d] = '0' + static_cast(v % 10); + v /= 10; + } + for (int i = 0; i < digits; ++i) + buf[pos++] = tmp[i]; + }; + int pos = 0; + appendPadded(ts, pos, st.wYear, 4); + appendPadded(ts, pos, st.wMonth, 2); + appendPadded(ts, pos, st.wDay, 2); + ts[pos++] = '_'; + appendPadded(ts, pos, st.wHour, 2); + appendPadded(ts, pos, st.wMinute, 2); + appendPadded(ts, pos, st.wSecond, 2); + ts[pos] = '\0'; + } + + // Build paths + { + const QString logDir = CrashHandler::crashLogDirectory(); + const QByteArray logDirBytes = logDir.toLocal8Bit(); + + snprintf(g_crashLogPath, sizeof(g_crashLogPath), + "%s\\yuview_crash_%s.log", logDirBytes.constData(), ts); + snprintf(g_dumpPath, sizeof(g_dumpPath), + "%s\\yuview_crash_%s.dmp", logDirBytes.constData(), ts); + } + + // ---- Write crash log ---- + HANDLE hLog = CreateFileA(g_crashLogPath, + GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hLog != INVALID_HANDLE_VALUE) + { + fileWrite(hLog, "=== YUView Crash Report ===\n"); + fileWrite(hLog, "Time: "); + fileWrite(hLog, ts); + fileWrite(hLog, "\n"); + fileWrite(hLog, "Version: "); + fileWrite(hLog, CrashHandler::appVersion().toUtf8().constData()); + fileWrite(hLog, "\n"); + + // Exception code + char codeBuf[20]; + uint64ToHex(static_cast(ep->ExceptionRecord->ExceptionCode), codeBuf, sizeof(codeBuf)); + fileWrite(hLog, "Exception code: "); + fileWrite(hLog, codeBuf); + + // Human-readable exception name + switch (ep->ExceptionRecord->ExceptionCode) + { + case EXCEPTION_ACCESS_VIOLATION: + fileWrite(hLog, " (Access Violation)"); + break; + case EXCEPTION_STACK_OVERFLOW: + fileWrite(hLog, " (Stack Overflow)"); + break; + case EXCEPTION_ILLEGAL_INSTRUCTION: + fileWrite(hLog, " (Illegal Instruction)"); + break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + fileWrite(hLog, " (Integer Divide by Zero)"); + break; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + fileWrite(hLog, " (Float Divide by Zero)"); + break; + default: + break; + } + fileWrite(hLog, "\n"); + + fileWrite(hLog, "===========================\n"); + fileWrite(hLog, "Stack Trace:\n"); + writeStackTrace(hLog, ep); + fileWrite(hLog, "===========================\n"); + fileWrite(hLog, "Minidump: "); + fileWrite(hLog, g_dumpPath); + fileWrite(hLog, "\n"); + + CloseHandle(hLog); + } + + // ---- Write minidump ---- + writeMiniDump(ep, g_dumpPath); + + // Allow Windows Error Reporting to also handle it (shows the crash dialog). + return EXCEPTION_CONTINUE_SEARCH; +} + +} // anonymous namespace + +// --------------------------------------------------------------------------- +// CrashHandler::installPlatformHandlers (Windows implementation) +// --------------------------------------------------------------------------- + +void CrashHandler::installPlatformHandlers() +{ + SetUnhandledExceptionFilter(yuviewExceptionFilter); +} + +#endif // Q_OS_WIN diff --git a/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp b/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp index 0f60bf9f2..a05c61bfb 100644 --- a/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp +++ b/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp @@ -324,10 +324,27 @@ void FFmpegVersionHandler::avLogCallback(void *, int level, const char *fmt, va_ { QString msg; msg.vasprintf(fmt, vargs); - auto now = QDateTime::currentDateTime(); - QMutexLocker locker(&FFmpegVersionHandler::logListMutex); - FFmpegVersionHandler::logListFFmpeg.append(now.toString("hh:mm:ss.zzz") + - QString(" - L%1 - ").arg(level) + msg); + + // Keep existing in-memory list for the "Show FFmpeg Log" UI dialog. + { + auto now = QDateTime::currentDateTime(); + QMutexLocker locker(&FFmpegVersionHandler::logListMutex); + FFmpegVersionHandler::logListFFmpeg.append(now.toString("hh:mm:ss.zzz") + + QString(" - L%1 - ").arg(level) + msg); + } + + // Also route through Qt's logging so the Logger / LogPanel can see it. + // AV_LOG_WARNING = 24, AV_LOG_ERROR = 16. Skip pure whitespace / newlines. + const QString trimmed = msg.trimmed(); + if (trimmed.isEmpty()) + return; + + const QString tagged = QString("[FFmpeg L%1] %2").arg(level).arg(trimmed); + // level <= AV_LOG_WARNING(24) → qWarning, otherwise qDebug + if (level <= 24) + qWarning().noquote() << tagged; + else + qDebug().noquote() << tagged; } void FFmpegVersionHandler::loadFFmpegLibraries() @@ -386,7 +403,10 @@ void FFmpegVersionHandler::loadFFmpegLibraries() } if (this->librariesLoaded) + { this->lib.avutil.av_log_set_callback(&FFmpegVersionHandler::avLogCallback); + this->lib.avutil.av_log_set_level(32); // AV_LOG_INFO + } } bool FFmpegVersionHandler::loadingSuccessfull() const diff --git a/YUViewLib/src/logging/LogPanel.cpp b/YUViewLib/src/logging/LogPanel.cpp new file mode 100644 index 000000000..69f2c6689 --- /dev/null +++ b/YUViewLib/src/logging/LogPanel.cpp @@ -0,0 +1,172 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset + * + * Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + * + * You must obey the GNU General Public License in all respects for all + * of the code used other than OpenSSL. If you modify file(s) with this + * exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do + * so, delete this exception statement from your version. If you delete + * this exception statement from all source files in the program, then + * also delete it here. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LogPanel.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Constructor +// --------------------------------------------------------------------------- + +LogPanel::LogPanel(QWidget *parent) : QDialog(parent) +{ + setWindowTitle(tr("Log Viewer")); + setWindowFlags(windowFlags() | Qt::WindowMinMaxButtonsHint); + resize(900, 550); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(8, 8, 8, 8); + mainLayout->setSpacing(6); + + // ---- Log view ---- + logView = new QPlainTextEdit(this); + logView->setReadOnly(true); + logView->setMaximumBlockCount(MAX_DISPLAY_LINES); + logView->setLineWrapMode(QPlainTextEdit::NoWrap); + QFont mono("Courier New", 9); + mono.setStyleHint(QFont::Monospace); + logView->setFont(mono); + mainLayout->addWidget(logView, /*stretch=*/1); + + // ---- "Write to file" checkboxes ---- + auto *fileGroup = new QGroupBox(tr("Write to file"), this); + auto *fileLayout = new QHBoxLayout(fileGroup); + fileLayout->setContentsMargins(8, 4, 8, 4); + + struct CatInfo + { + LogCategory cat; + const char *label; + const char *tooltip; + }; + const CatInfo cats[] = { + {LogCategory::App, + "Application", + "General qDebug / qWarning / qCritical messages from application code"}, + {LogCategory::FFmpeg, + "FFmpeg", + "FFmpeg library log messages (errors, warnings, codec info)"}, + }; + + for (const auto &info : cats) + { + const int idx = static_cast(info.cat); + auto *cb = new QCheckBox(tr(info.label), fileGroup); + cb->setChecked(Logger::instance().isCategoryFileEnabled(info.cat)); + cb->setToolTip(tr(info.tooltip)); + checkboxes[idx] = cb; + + connect(cb, &QCheckBox::checkStateChanged, this, + [this, cat = info.cat](Qt::CheckState state) + { onCategoryCheckChanged(cat, state == Qt::Checked); }); + + fileLayout->addWidget(cb); + } + fileLayout->addStretch(); + mainLayout->addWidget(fileGroup); + + // ---- Buttons ---- + auto *btnLayout = new QHBoxLayout; + btnLayout->setContentsMargins(0, 0, 0, 0); + + auto *clearBtn = new QPushButton(tr("Clear"), this); + clearBtn->setToolTip(tr("Clear the log view (does not affect the log file)")); + connect(clearBtn, &QPushButton::clicked, this, &LogPanel::onClearClicked); + btnLayout->addWidget(clearBtn); + + btnLayout->addStretch(); + + auto *folderBtn = new QPushButton(tr("Open Log Folder"), this); + folderBtn->setToolTip(tr("Open the directory containing log files")); + connect(folderBtn, &QPushButton::clicked, this, &LogPanel::onOpenLogFolderClicked); + btnLayout->addWidget(folderBtn); + + mainLayout->addLayout(btnLayout); + + // ---- Register UI callback with Logger ---- + // The callback is invoked from arbitrary threads, so we post back to the + // UI thread via Qt::QueuedConnection. + Logger::instance().setUiCallback( + [this](LogCategory /*cat*/, const QString &line) + { + QMetaObject::invokeMethod( + this, [this, line]() { appendLine(line); }, Qt::QueuedConnection); + }); +} + +// --------------------------------------------------------------------------- +// Destructor +// --------------------------------------------------------------------------- + +LogPanel::~LogPanel() +{ + Logger::instance().clearUiCallback(); +} + +// --------------------------------------------------------------------------- +// appendLine +// --------------------------------------------------------------------------- + +void LogPanel::appendLine(const QString &line) +{ + logView->appendPlainText(line); + // Auto-scroll to bottom. + auto *sb = logView->verticalScrollBar(); + sb->setValue(sb->maximum()); +} + +// --------------------------------------------------------------------------- +// Slots +// --------------------------------------------------------------------------- + +void LogPanel::onCategoryCheckChanged(LogCategory cat, bool checked) +{ + Logger::instance().setCategoryFileEnabled(cat, checked); +} + +void LogPanel::onClearClicked() +{ + logView->clear(); +} + +void LogPanel::onOpenLogFolderClicked() +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(Logger::instance().logDirectory())); +} diff --git a/YUViewLib/src/logging/LogPanel.h b/YUViewLib/src/logging/LogPanel.h new file mode 100644 index 000000000..f4bbce4b0 --- /dev/null +++ b/YUViewLib/src/logging/LogPanel.h @@ -0,0 +1,80 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset + * + * Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + * + * You must obey the GNU General Public License in all respects for all + * of the code used other than OpenSSL. If you modify file(s) with this + * exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do + * so, delete this exception statement from your version. If you delete + * this exception statement from all source files in the program, then + * also delete it here. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include + +// --------------------------------------------------------------------------- +// LogPanel +// +// Non-modal dialog that shows a live log view and lets the user control +// which categories are written to the log file. +// +// Layout: +// ┌─────────────────────────────────────────┐ +// │ [QPlainTextEdit – scrolling log view] │ +// ├─────────────────────────────────────────┤ +// │ Write to file: │ +// │ [x] Application [x] FFmpeg │ +// ├─────────────────────────────────────────┤ +// │ [Clear] [Open Log Folder] │ +// └─────────────────────────────────────────┘ +// --------------------------------------------------------------------------- + +class LogPanel : public QDialog +{ + Q_OBJECT + +public: + explicit LogPanel(QWidget *parent = nullptr); + ~LogPanel() override; + + // Append a formatted line to the text view (must be called on the UI thread). + void appendLine(const QString &line); + +private slots: + void onCategoryCheckChanged(LogCategory cat, bool checked); + void onClearClicked(); + void onOpenLogFolderClicked(); + +private: + QPlainTextEdit *logView{nullptr}; + QCheckBox *checkboxes[static_cast(LogCategory::COUNT)]{}; + + // Maximum number of lines kept in the view. + static constexpr int MAX_DISPLAY_LINES = 2000; +}; diff --git a/YUViewLib/src/logging/Logger.cpp b/YUViewLib/src/logging/Logger.cpp new file mode 100644 index 000000000..5568c7dcb --- /dev/null +++ b/YUViewLib/src/logging/Logger.cpp @@ -0,0 +1,317 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset + * + * Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + * + * You must obey the GNU General Public License in all respects for all + * of the code used other than OpenSSL. If you modify file(s) with this + * exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do + * so, delete this exception statement from your version. If you delete + * this exception statement from all source files in the program, then + * also delete it here. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Logger.h" + +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Singleton +// --------------------------------------------------------------------------- + +Logger &Logger::instance() +{ + static Logger inst; + return inst; +} + +// --------------------------------------------------------------------------- +// init +// --------------------------------------------------------------------------- + +void Logger::init() +{ + QMutexLocker lock(&mutex); + if (initialised) + return; + + // Determine log directory + const QString appData = + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + const QString logDir = appData + QDir::separator() + "logs"; + + QDir().mkpath(logDir); + rotateOldLogs(logDir); + + // Build timestamped file name + const QString ts = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"); + logFilePath = logDir + QDir::separator() + "yuview_" + ts + ".log"; + logFile.setFileName(logFilePath); + + if (!logFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append)) + { + // Can't open log file – silently continue without file logging. + logFilePath.clear(); + } + else + { + // Write a session header + QTextStream out(&logFile); + out << "=== YUView Session Started ===\n"; + out << "Time: " << QDateTime::currentDateTime().toString(Qt::ISODateWithMs) << "\n"; + out << "Version: " << qApp->applicationVersion() << "\n"; + out << "==============================\n"; + bytesWritten = logFile.size(); + } + + qInstallMessageHandler(&Logger::qtMessageHandler); + initialised = true; +} + +// --------------------------------------------------------------------------- +// shutdown +// --------------------------------------------------------------------------- + +void Logger::shutdown() +{ + QMutexLocker lock(&mutex); + if (!initialised) + return; + + qInstallMessageHandler(nullptr); // restore default handler + + if (logFile.isOpen()) + { + QTextStream out(&logFile); + out << "=== YUView Session Ended ===\n"; + logFile.flush(); + logFile.close(); + } + initialised = false; +} + +// --------------------------------------------------------------------------- +// Accessors +// --------------------------------------------------------------------------- + +QString Logger::logDirectory() const +{ + if (logFilePath.isEmpty()) + { + const QString appData = + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + return appData + QDir::separator() + "logs"; + } + return QFileInfo(logFilePath).absolutePath(); +} + +QString Logger::currentLogFilePath() const +{ + return logFilePath; +} + +// --------------------------------------------------------------------------- +// writeRaw (used by crash handler – must be async-signal-safe on POSIX) +// --------------------------------------------------------------------------- + +void Logger::writeRaw(const char *utf8Line) +{ + // NOTE: On POSIX this is called from a signal handler. + // We use the low-level POSIX write() instead of Qt I/O when the file + // descriptor is valid. + if (!logFile.isOpen()) + return; + +#if defined(Q_OS_WIN) + // On Windows SEH handler we are not in a signal context, so Qt I/O is fine. + QMutexLocker lock(&mutex); + logFile.write(utf8Line); + logFile.write("\n"); + logFile.flush(); +#else + // POSIX: use raw write() – async-signal-safe. + int fd = logFile.handle(); + if (fd < 0) + return; + ::write(fd, utf8Line, ::strlen(utf8Line)); + ::write(fd, "\n", 1); +#endif +} + +// --------------------------------------------------------------------------- +// Category detection +// --------------------------------------------------------------------------- + +LogCategory Logger::detectCategory(const QString &msg) +{ + if (msg.startsWith(QLatin1String("[FFmpeg"))) + return LogCategory::FFmpeg; + return LogCategory::App; +} + +// --------------------------------------------------------------------------- +// Per-category file enable / disable +// --------------------------------------------------------------------------- + +void Logger::setCategoryFileEnabled(LogCategory cat, bool enabled) +{ + QMutexLocker lock(&mutex); + categoryFileEnabled[static_cast(cat)] = enabled; +} + +bool Logger::isCategoryFileEnabled(LogCategory cat) const +{ + return categoryFileEnabled[static_cast(cat)]; +} + +// --------------------------------------------------------------------------- +// UI callback +// --------------------------------------------------------------------------- + +void Logger::setUiCallback(UiCallback cb) +{ + QMutexLocker lock(&mutex); + uiCallback = std::move(cb); +} + +void Logger::clearUiCallback() +{ + QMutexLocker lock(&mutex); + uiCallback = nullptr; +} + +// --------------------------------------------------------------------------- +// Qt message handler (static) +// --------------------------------------------------------------------------- + +void Logger::qtMessageHandler(QtMsgType type, + const QMessageLogContext &ctx, + const QString &msg) +{ + Logger::instance().writeEntry(type, ctx, msg); +} + +// --------------------------------------------------------------------------- +// writeEntry +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Messages that are too noisy to be useful and should be suppressed. +// Each entry is a substring that, if found in the message, causes it to be +// dropped entirely. Add new patterns here as needed. +// --------------------------------------------------------------------------- +static const char *const SUPPRESSED_PATTERNS[] = { + // Qt internal: emitted when a model calls dataChanged() with invalid indices. + // Happens frequently in playlist / tree view interactions and carries no + // actionable information for end-users. + "dataChanged() called with an invalid index range", +}; + +void Logger::writeEntry(QtMsgType type, const QMessageLogContext &ctx, const QString &msg) +{ + // Apply suppression filter before taking the mutex (cheap path for noisy msgs). + for (const char *pattern : SUPPRESSED_PATTERNS) + { + if (msg.contains(QLatin1String(pattern))) + return; + } + + const LogCategory cat = detectCategory(msg); + + const char *levelStr = "DEBUG "; + switch (type) + { + case QtDebugMsg: levelStr = "DEBUG "; break; + case QtInfoMsg: levelStr = "INFO "; break; + case QtWarningMsg: levelStr = "WARNING "; break; + case QtCriticalMsg: levelStr = "CRITICAL"; break; + case QtFatalMsg: levelStr = "FATAL "; break; + } + + QString location; + if (ctx.file && ctx.line > 0) + { + QString file = QString::fromUtf8(ctx.file); + const int sep = qMax(file.lastIndexOf('/'), file.lastIndexOf('\\')); + if (sep >= 0) + file = file.mid(sep + 1); + location = QString(" [%1:%2]").arg(file).arg(ctx.line); + } + + const QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"); + const QString line = QString("[%1] [%2]%3 %4").arg(ts, levelStr, location, msg); + + QMutexLocker lock(&mutex); + + // ---- UI callback (always fires regardless of file setting) ---- + if (uiCallback) + uiCallback(cat, line); + + // ---- File write (gated by per-category enable + size cap) ---- + if (logFile.isOpen() + && bytesWritten < MAX_LOG_FILE_BYTES + && categoryFileEnabled[static_cast(cat)]) + { + const QByteArray bytes = (line + '\n').toUtf8(); + logFile.write(bytes); + logFile.flush(); + bytesWritten += bytes.size(); + + if (bytesWritten >= MAX_LOG_FILE_BYTES) + { + const char cap[] = "[Logger] Log file size cap reached. Further output suppressed.\n"; + logFile.write(cap); + logFile.flush(); + } + } + + // For Fatal messages also print to stderr so the OS can capture it. + if (type == QtFatalMsg) + { + fprintf(stderr, "%s\n", line.toUtf8().constData()); + fflush(stderr); + } +} + +// --------------------------------------------------------------------------- +// rotateOldLogs +// --------------------------------------------------------------------------- + +void Logger::rotateOldLogs(const QString &logDir) +{ + QDir dir(logDir); + QFileInfoList files = + dir.entryInfoList({"yuview_*.log"}, QDir::Files, QDir::Time | QDir::Reversed); + + // Delete oldest files beyond the keep limit (MAX_LOG_FILES - 1 existing + 1 new). + while (files.size() >= MAX_LOG_FILES) + { + QFile::remove(files.first().absoluteFilePath()); + files.removeFirst(); + } +} diff --git a/YUViewLib/src/logging/Logger.h b/YUViewLib/src/logging/Logger.h new file mode 100644 index 000000000..7c50d87b9 --- /dev/null +++ b/YUViewLib/src/logging/Logger.h @@ -0,0 +1,130 @@ +/* This file is part of YUView - The YUV player with advanced analytics toolset + * + * Copyright (C) 2015 Institut für Nachrichtentechnik, RWTH Aachen University, GERMANY + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link the code of portions of this program with the + * OpenSSL library under certain conditions as described in each + * individual source file, and distribute linked combinations including + * the two. + * + * You must obey the GNU General Public License in all respects for all + * of the code used other than OpenSSL. If you modify file(s) with this + * exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do + * so, delete this exception statement from your version. If you delete + * this exception statement from all source files in the program, then + * also delete it here. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Logger +// +// Installs a Qt message handler that writes all qDebug / qWarning / qCritical +// / qFatal output to a timestamped log file under +// Windows: %APPDATA%/YUView/logs/ +// macOS: ~/Library/Application Support/YUView/logs/ +// +// Log rotation: keeps the 5 most recent files; older ones are deleted on init. +// The log file is also limited to MAX_LOG_FILE_BYTES to guard against runaway +// output during a single session. +// +// Log categories +// Messages are classified by prefix into one of the LogCategory values. +// Per-category file-write can be turned on/off at runtime. +// A UI callback can be registered to receive every incoming line for +// live display in a log panel (independent of the file-write setting). +// --------------------------------------------------------------------------- + +// Message categories – detected from the message prefix "[FFmpeg…]" etc. +enum class LogCategory +{ + App, // general qDebug / qWarning / qCritical from application code + FFmpeg, // messages prefixed with "[FFmpeg" + COUNT +}; + +class Logger +{ +public: + static Logger &instance(); + + // Callback invoked on the calling thread for every incoming log line. + // The panel should use QMetaObject::invokeMethod(..., Qt::QueuedConnection) + // to forward to the UI thread. + using UiCallback = std::function; + + // Install the Qt message handler and open the log file. + void init(); + + // Flush and close the log file; uninstall the message handler. + void shutdown(); + + // Per-category file-write enable / disable (default: all enabled). + void setCategoryFileEnabled(LogCategory cat, bool enabled); + bool isCategoryFileEnabled(LogCategory cat) const; + + // Register / clear the UI callback (thread-safe). + void setUiCallback(UiCallback cb); + void clearUiCallback(); + + // Path to the directory that contains log files. + QString logDirectory() const; + + // Path to the log file opened during this session (empty before init()). + QString currentLogFilePath() const; + + // Write a raw line directly to the log (used by the crash handler which + // cannot go through Qt's logging machinery safely). + void writeRaw(const char *utf8Line); + +private: + Logger() = default; + ~Logger() { shutdown(); } + + Logger(const Logger &) = delete; + Logger &operator=(const Logger &) = delete; + + static void qtMessageHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg); + static LogCategory detectCategory(const QString &msg); + + void rotateOldLogs(const QString &logDir); + void writeEntry(QtMsgType type, const QMessageLogContext &ctx, const QString &msg); + + QFile logFile; + QMutex mutex; + bool initialised{false}; + QString logFilePath; + + bool categoryFileEnabled[static_cast(LogCategory::COUNT)]{true, true}; + + UiCallback uiCallback; // protected by mutex + + // Per-session byte counter – stop writing when this exceeds the cap. + static constexpr qint64 MAX_LOG_FILE_BYTES = 10LL * 1024 * 1024; // 10 MB + qint64 bytesWritten{0}; + + // How many log files to keep. + static constexpr int MAX_LOG_FILES = 5; +}; diff --git a/YUViewLib/src/ui/Mainwindow.cpp b/YUViewLib/src/ui/Mainwindow.cpp index c82c80b87..f8f1be509 100644 --- a/YUViewLib/src/ui/Mainwindow.cpp +++ b/YUViewLib/src/ui/Mainwindow.cpp @@ -43,6 +43,8 @@ #include #include +#include +#include #include #include #include @@ -218,6 +220,27 @@ MainWindow::MainWindow(bool useAlternativeSources, QWidget *parent) : QMainWindo else ui.playlistTreeWidget->dropAutosavedPlaylist(); } + + // If a crash log from the previous session exists, notify the user. + if (CrashHandler::hasPendingCrashReport()) + { + const QString reportPath = CrashHandler::lastCrashReportPath(); + QMessageBox msgBox(this); + msgBox.setWindowTitle(tr("Crash Report Available")); + msgBox.setText(tr("YUView generated a crash report during the last session.")); + msgBox.setInformativeText( + tr("A crash log was saved to:\n%1\n\nWould you like to open the log folder?") + .arg(reportPath)); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Ignore); + msgBox.setDefaultButton(QMessageBox::Ignore); + msgBox.setIcon(QMessageBox::Warning); + + if (msgBox.exec() == QMessageBox::Yes) + QDesktopServices::openUrl(QUrl::fromLocalFile(CrashHandler::crashLogDirectory())); + + // Archive the report so we don't show it again on the next launch. + CrashHandler::archiveCrashReport(); + } // Start the timer now (and not in the constructor of rht playlistTreeWidget) so that the autosave // is not accidetly overwritten. ui.playlistTreeWidget->startAutosaveTimer(); @@ -471,6 +494,44 @@ void MainWindow::createMenusAndActions() addActionToMenu(helpMenu, "Reset Window Layout", this, &MainWindow::resetWindowLayout); addActionToMenu(helpMenu, "Clear Settings", this, &MainWindow::closeAndClearSettings); + helpMenu->addSeparator(); + addLambdaActionToMenu(helpMenu, + "Show Log Panel", + [this]() + { + if (!logPanel) + logPanel = new LogPanel(this); + logPanel->show(); + logPanel->raise(); + logPanel->activateWindow(); + }); + addLambdaActionToMenu(helpMenu, + "Open Log Folder", + []() + { + QDesktopServices::openUrl( + QUrl::fromLocalFile(Logger::instance().logDirectory())); + }); + // "Show Last Crash Report" – only enabled when a pending report exists. + { + auto *crashAction = new QAction(tr("Show Last Crash Report"), helpMenu); + crashAction->setEnabled(CrashHandler::hasPendingCrashReport()); + QObject::connect(crashAction, + &QAction::triggered, + [crashAction]() + { + const QString path = CrashHandler::lastCrashReportPath(); + if (!path.isEmpty()) + { + QDesktopServices::openUrl( + QUrl::fromLocalFile(CrashHandler::crashLogDirectory())); + CrashHandler::archiveCrashReport(); + crashAction->setEnabled(false); + } + }); + helpMenu->addAction(crashAction); + } + this->updateRecentFileActions(); } diff --git a/YUViewLib/src/ui/Mainwindow.h b/YUViewLib/src/ui/Mainwindow.h index cf26eb2a3..50dda3c3c 100644 --- a/YUViewLib/src/ui/Mainwindow.h +++ b/YUViewLib/src/ui/Mainwindow.h @@ -37,6 +37,7 @@ #include #include +#include #include #include - - Specify the individual FFmpeg libraries to use. These will always be tried first. - - - Specify the individual FFmpeg libraries to use. These will always be tried first. - - - FFmpeg Libraries - + + Select a folder containing the FFmpeg libraries. These will always be tried first. + + + Select a folder containing the FFmpeg libraries. These will always be tried first. + + + FFmpeg Libraries + @@ -1313,12 +1313,12 @@ 0 - - Specify the 4 required ffmpeg library files. These will always be tried first. - - - Specify the 4 required ffmpeg library files. These will always be tried first. - + + Select a folder containing the FFmpeg libraries. The four required libraries will be detected automatically. + + + Select a folder containing the FFmpeg libraries. The four required libraries will be detected automatically. + @@ -1336,12 +1336,12 @@ 0 - - Remove the ffmpeg library paths. - - - Remove the ffmpeg library paths. - + + Remove the FFmpeg library paths. + + + Remove the FFmpeg library paths. + From 597417b80172c3bf9fc41f1db8be1d45f6e8d2ae Mon Sep 17 00:00:00 2001 From: think3r Date: Tue, 14 Jul 2026 01:37:30 +0800 Subject: [PATCH 49/55] Remove duplicate toUnsigned/toInt declarations in Functions.h --- YUViewLib/src/common/Functions.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/YUViewLib/src/common/Functions.h b/YUViewLib/src/common/Functions.h index 9a26f89cf..b1c57b72b 100644 --- a/YUViewLib/src/common/Functions.h +++ b/YUViewLib/src/common/Functions.h @@ -104,7 +104,4 @@ template inline T clip(T val, Range range) return (val < T(range.min)) ? T(range.min) : (val > T(range.max)) ? T(range.max) : val; } -std::optional toUnsigned(const std::string_view text); -std::optional toInt(const std::string_view text); - } // namespace functions From 8b01d46631524bcedbc54b3185b41a753a2cbed6 Mon Sep 17 00:00:00 2001 From: think3r Date: Mon, 13 Jul 2026 23:06:48 +0800 Subject: [PATCH 50/55] Show QP statistics (min/max/avg) in Bitrate Plot hover info box Add per-frame QP aggregation for HEVC/AVC/VVC AnnexB parsers. Each AU collects QP (min/max/sum/count) across all slices, computed as: HEVC: 26 + init_qp_minus26 + slice_qp_delta AVC: 26 + pic_init_qp_minus26 + slice_qp_delta VVC: 26 + pps_init_qp_minus26 + (ph_qp_delta or sh_qp_delta) (VVC routes QP delta to PH or SH via pps_qp_delta_info_in_ph_flag) BitrateEntry gains qpMin/qpMax/qpAvg fields (default -1 = unavailable). getPointInfo shows 'QP: min / max / avg' row only when QP data is available. MPEG-2/AVFormat/AV1 paths leave QP unset (-1), so no QP row is shown for those sources. --- YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp | 32 +++++++++++++ YUViewLib/src/parser/AVC/ParserAnnexBAVC.h | 7 +++ .../src/parser/HEVC/ParserAnnexBHEVC.cpp | 25 +++++++++++ YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.h | 7 +++ YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp | 45 +++++++++++++++++++ YUViewLib/src/parser/VVC/ParserAnnexBVVC.h | 7 +++ .../src/parser/common/BitratePlotModel.cpp | 40 ++++++++++------- .../src/parser/common/BitratePlotModel.h | 4 ++ 8 files changed, 152 insertions(+), 15 deletions(-) diff --git a/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp b/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp index 9e2424813..4ef3fb837 100644 --- a/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp +++ b/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp @@ -34,6 +34,7 @@ #include #include +#include #include "NalUnitAVC.h" #include "SEI/buffering_period.h" @@ -222,6 +223,12 @@ ParserAnnexBAVC::parseAndAddNALUnit(int entry.keyframe = this->currentAUAllSlicesIntra; entry.frameType = QString::fromStdString(convertSliceCountsToString(this->currentAUSliceTypes)); + if (this->currentAUQpCount > 0) + { + entry.qpMin = this->currentAUQpMin; + entry.qpMax = this->currentAUQpMax; + entry.qpAvg = int(this->currentAUQpSum / this->currentAUQpCount); + } parseResult.bitrateEntry = entry; } } @@ -378,6 +385,21 @@ ParserAnnexBAVC::parseAndAddNALUnit(int currentSliceIntra = isRandomAccess; currentSliceType = to_string(newSliceHeader->slice_type); + // Compute slice QP = 26 + pic_init_qp_minus26 + slice_qp_delta and aggregate per AU. + { + const auto ppsID = newSliceHeader->pic_parameter_set_id; + if (this->activeParameterSets.ppsMap.count(ppsID) > 0) + { + const auto &pps = this->activeParameterSets.ppsMap.at(ppsID); + const auto sliceQp = 26 + pps->pic_init_qp_minus26 + + newSliceHeader->slice_qp_delta; + this->currentAUQpMin = std::min(this->currentAUQpMin, sliceQp); + this->currentAUQpMax = std::max(this->currentAUQpMax, sliceQp); + this->currentAUQpSum += sliceQp; + this->currentAUQpCount++; + } + } + DEBUG_AVC("ParserAnnexBAVC::parseAndAddNALUnit Parsed Slice (" << NalTypeMapper.getName(nalAVC->header.nal_unit_type) << ") POC " << newSliceHeader->globalPOC); @@ -535,6 +557,12 @@ ParserAnnexBAVC::parseAndAddNALUnit(int entry.keyframe = this->currentAUAllSlicesIntra; entry.frameType = QString::fromStdString(convertSliceCountsToString(this->currentAUSliceTypes)); + if (this->currentAUQpCount > 0) + { + entry.qpMin = this->currentAUQpMin; + entry.qpMax = this->currentAUQpMax; + entry.qpAvg = int(this->currentAUQpSum / this->currentAUQpCount); + } parseResult.bitrateEntry = entry; if (this->lastBufferingPeriodSEI && this->lastPicTimingSEI) @@ -562,6 +590,10 @@ ParserAnnexBAVC::parseAndAddNALUnit(int this->currentAUAssociatedSPS.reset(); this->currentAUPartitionASPS.reset(); this->curFrameData.reset(); + this->currentAUQpMin = std::numeric_limits::max(); + this->currentAUQpMax = std::numeric_limits::min(); + this->currentAUQpSum = 0; + this->currentAUQpCount = 0; } } curFrameData = diff --git a/YUViewLib/src/parser/AVC/ParserAnnexBAVC.h b/YUViewLib/src/parser/AVC/ParserAnnexBAVC.h index 7f0bae261..656576c66 100644 --- a/YUViewLib/src/parser/AVC/ParserAnnexBAVC.h +++ b/YUViewLib/src/parser/AVC/ParserAnnexBAVC.h @@ -44,6 +44,7 @@ #include #include +#include namespace parser { @@ -135,6 +136,12 @@ class ParserAnnexBAVC : public ParserAnnexB bool currentAUAllSlicesIntra{true}; std::map currentAUSliceTypes; + // QP aggregation for the current AU (across all slices). + int currentAUQpMin{std::numeric_limits::max()}; + int currentAUQpMax{std::numeric_limits::min()}; + long currentAUQpSum{0}; + unsigned currentAUQpCount{0}; + avc::HRD hrd; }; diff --git a/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.cpp b/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.cpp index 12e0c2e6d..bbed5a1ed 100644 --- a/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.cpp +++ b/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.cpp @@ -525,6 +525,21 @@ ParserAnnexBHEVC::parseAndAddNALUnit(int } currentSliceType = to_string(newSlice->sliceSegmentHeader.slice_type); + // Compute slice QP = 26 + init_qp_minus26 + slice_qp_delta and aggregate per AU. + { + const auto ppsID = newSlice->sliceSegmentHeader.slice_pic_parameter_set_id; + if (this->activeParameterSets.ppsMap.count(ppsID) > 0) + { + const auto &pps = this->activeParameterSets.ppsMap.at(ppsID); + const auto sliceQp = 26 + pps->init_qp_minus26 + + newSlice->sliceSegmentHeader.slice_qp_delta; + this->currentAUQpMin = std::min(this->currentAUQpMin, sliceQp); + this->currentAUQpMax = std::max(this->currentAUQpMax, sliceQp); + this->currentAUQpSum += sliceQp; + this->currentAUQpCount++; + } + } + const auto pocDetail = formatNalPOCDetail(poc, nalHEVC->header.nuh_layer_id); specificDescription << pocDetail; parseResult.nalTypeName = "Slice" + pocDetail; @@ -643,6 +658,12 @@ ParserAnnexBHEVC::parseAndAddNALUnit(int entry.bitrate = this->sizeCurrentAU; entry.keyframe = this->currentAUAllSlicesIntra; entry.frameType = QString::fromStdString(convertSliceCountsToString(this->currentAUSliceTypes)); + if (this->currentAUQpCount > 0) + { + entry.qpMin = this->currentAUQpMin; + entry.qpMax = this->currentAUQpMax; + entry.qpAvg = int(this->currentAUQpSum / this->currentAUQpCount); + } parseResult.bitrateEntry = entry; this->sizeCurrentAU = 0; @@ -651,6 +672,10 @@ ParserAnnexBHEVC::parseAndAddNALUnit(int this->firstAUInDecodingOrder = false; this->currentAUSliceTypes.clear(); this->currentAUAssociatedSPS.reset(); + this->currentAUQpMin = std::numeric_limits::max(); + this->currentAUQpMax = std::numeric_limits::min(); + this->currentAUQpSum = 0; + this->currentAUQpCount = 0; } if (this->lastFramePOC != this->curFramePOC) this->lastFramePOC = this->curFramePOC; diff --git a/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.h b/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.h index 0c72dac73..4d5640348 100644 --- a/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.h +++ b/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.h @@ -43,6 +43,7 @@ #include "nal_unit_header.h" #include +#include namespace parser { @@ -149,6 +150,12 @@ class ParserAnnexBHEVC : public ParserAnnexB size_t counterAU{0}; bool currentAUAllSlicesIntra{true}; std::map currentAUSliceTypes; + + // QP aggregation for the current AU (across all slices). + int currentAUQpMin{std::numeric_limits::max()}; + int currentAUQpMax{std::numeric_limits::min()}; + long currentAUQpSum{0}; + unsigned currentAUQpCount{0}; }; } // namespace parser \ No newline at end of file diff --git a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp index a579a8c7b..5ef966c68 100644 --- a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp +++ b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp @@ -87,6 +87,12 @@ createBitrateEntryForAU(ParsingState &parsingSta auto sliceTypes = convertSliceCountsToString(parsingState.currentAU.sliceTypes); if (!sliceTypes.empty()) entry.frameType = QString::fromStdString(sliceTypes); + if (parsingState.currentAU.qpCount > 0) + { + entry.qpMin = parsingState.currentAU.qpMin; + entry.qpMax = parsingState.currentAU.qpMax; + entry.qpAvg = int(parsingState.currentAU.qpSum / parsingState.currentAU.qpCount); + } return entry; } @@ -405,6 +411,23 @@ ParserAnnexBVVC::parseAndAddNALUnit(int newPictureHeader->picture_header_structure_instance; updatedParsingState.currentAU.poc = pictureHeader->globalPOC; + // If QP delta is signaled in PH, compute QP = 26 + pps_init_qp_minus26 + ph_qp_delta. + { + const auto ppsID = pictureHeader->ph_pic_parameter_set_id; + if (this->activeParameterSets.ppsMap.count(ppsID) > 0) + { + const auto &pps = this->activeParameterSets.ppsMap.at(ppsID); + if (pps->pps_qp_delta_info_in_ph_flag) + { + const auto qp = 26 + pps->pps_init_qp_minus26 + pictureHeader->ph_qp_delta; + updatedParsingState.currentAU.qpMin = std::min(updatedParsingState.currentAU.qpMin, qp); + updatedParsingState.currentAU.qpMax = std::max(updatedParsingState.currentAU.qpMax, qp); + updatedParsingState.currentAU.qpSum += qp; + updatedParsingState.currentAU.qpCount++; + } + } + } + // 8.3.1 auto TemporalId = nalVVC->header.nuh_temporal_id_plus1 - 1; if (TemporalId == 0 && !pictureHeader->ph_non_ref_pic_flag && nalType != NalType::RASL_NUT && @@ -482,6 +505,24 @@ ParserAnnexBVVC::parseAndAddNALUnit(int nalType == NalType::CRA_NUT); updatedParsingState.currentAU.sliceTypes[std::string(SliceTypeMapper.getName( newSliceLayer->slice_header_instance.sh_slice_type))]++; + // If QP delta is signaled in SH (not in PH), compute QP = 26 + pps_init_qp_minus26 + sh_qp_delta. + if (updatedParsingState.currentPictureHeaderStructure) + { + const auto ppsID = updatedParsingState.currentPictureHeaderStructure->ph_pic_parameter_set_id; + if (this->activeParameterSets.ppsMap.count(ppsID) > 0) + { + const auto &pps = this->activeParameterSets.ppsMap.at(ppsID); + if (!pps->pps_qp_delta_info_in_ph_flag) + { + const auto qp = 26 + pps->pps_init_qp_minus26 + + newSliceLayer->slice_header_instance.sh_qp_delta; + updatedParsingState.currentAU.qpMin = std::min(updatedParsingState.currentAU.qpMin, qp); + updatedParsingState.currentAU.qpMax = std::max(updatedParsingState.currentAU.qpMax, qp); + updatedParsingState.currentAU.qpSum += qp; + updatedParsingState.currentAU.qpCount++; + } + } + } if (updatedParsingState.currentAU.isKeyframe) { nalVVC->rawData = data; @@ -565,6 +606,10 @@ ParserAnnexBVVC::parseAndAddNALUnit(int updatedParsingState.currentAU.sizeBytes = 0; updatedParsingState.currentAU.counter++; updatedParsingState.currentAU.sliceTypes.clear(); + updatedParsingState.currentAU.qpMin = std::numeric_limits::max(); + updatedParsingState.currentAU.qpMax = std::numeric_limits::min(); + updatedParsingState.currentAU.qpSum = 0; + updatedParsingState.currentAU.qpCount = 0; } else if (nalStartEndPosFile) { diff --git a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.h b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.h index 8e22f98eb..f7f344400 100644 --- a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.h +++ b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.h @@ -39,6 +39,7 @@ #include "commonMaps.h" #include +#include namespace parser { @@ -69,6 +70,12 @@ struct ParsingState bool isKeyframe{}; std::optional fileStartEndPos; std::map sliceTypes; + + // QP aggregation across all slices/PH of this AU. + int qpMin{std::numeric_limits::max()}; + int qpMax{std::numeric_limits::min()}; + long qpSum{0}; + unsigned qpCount{0}; }; CurrentAU currentAU{}; diff --git a/YUViewLib/src/parser/common/BitratePlotModel.cpp b/YUViewLib/src/parser/common/BitratePlotModel.cpp index c6dd03199..0aa19cd04 100644 --- a/YUViewLib/src/parser/common/BitratePlotModel.cpp +++ b/YUViewLib/src/parser/common/BitratePlotModel.cpp @@ -145,21 +145,31 @@ BitratePlotModel::getPointInfo(unsigned streamIndex, unsigned plotIndex, unsigne .arg(entry.pts) .arg(entry.dts) .arg(this->calculateAverageValue(streamIndex, pointIndex)); - else - return QString("

Stream %1

" - "" - "" - "" - "" - "" - "" - "
PTS:%2
DTS:%3
Duration:%4
Bitrate:%5
Type:%6
") - .arg(streamIndex) - .arg(entry.pts) - .arg(entry.dts) - .arg(entry.duration) - .arg(entry.bitrate) - .arg(entry.frameType); + + // QP row: only shown when QP data is available (HEVC/AVC/VVC AnnexB). + QString qpRow; + if (entry.qpMin >= 0) + qpRow = QString("QP:%1 / %2 / %3") + .arg(entry.qpMin) + .arg(entry.qpMax) + .arg(entry.qpAvg); + + return QString("

Stream %1

" + "" + "" + "" + "" + "" + "" + "%7" + "
PTS:%2
DTS:%3
Duration:%4
Bitrate:%5
Type:%6
") + .arg(streamIndex) + .arg(entry.pts) + .arg(entry.dts) + .arg(entry.duration) + .arg(entry.bitrate) + .arg(entry.frameType) + .arg(qpRow); } std::optional BitratePlotModel::getReasonabelRangeToShowOnXAxisPer100Pixels() const diff --git a/YUViewLib/src/parser/common/BitratePlotModel.h b/YUViewLib/src/parser/common/BitratePlotModel.h index d0db7ad31..ff4238a4f 100644 --- a/YUViewLib/src/parser/common/BitratePlotModel.h +++ b/YUViewLib/src/parser/common/BitratePlotModel.h @@ -64,6 +64,10 @@ class BitratePlotModel : public PlotModel size_t bitrate{0}; bool keyframe{false}; QString frameType{}; + // QP statistics per AU (aggregated from all slices). -1 means unavailable. + int qpMin{-1}; + int qpMax{-1}; + int qpAvg{-1}; }; void addBitratePoint(int streamIndex, BitrateEntry &entry); From 93e864e3bd28e069b0cb71cd98aaf996af1f9138 Mon Sep 17 00:00:00 2001 From: think3r Date: Mon, 13 Jul 2026 23:26:21 +0800 Subject: [PATCH 51/55] Cache raw YUV bytes instead of RGB QImage for YUV video handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The YUV video handler previously cached converted RGB QImage objects. This meant that changing display-only parameters (color matrix, chroma interpolation, math parameters, component display mode) invalidated the entire cache, forcing a full recache that re-reads raw YUV from file (or re-decodes for compressed video) for every frame. This change introduces a rawDataCache (QMap) that stores raw YUV bytes per frame index. RGB conversion is now performed on-the-fly in drawFrame. This way: - Color matrix / interpolation / math / component changes only invalidate currentImage (one re-conversion), NOT the raw cache. No recache, no file I/O, no re-decoding. (RECACHE_NONE instead of RECACHE_CLEAR) - YUV format / frame size / file changes still clear the raw cache and trigger recache (raw data layout actually changed) - Memory per cached frame drops from ~8.3MB (RGB32, 1080p) to ~3.1MB (YUV 4:2:0), a 62% reduction, allowing more frames to be cached - Compressed video (mp4/h265) benefits most: skipping re-decode on color matrix change (decode is 10-50x slower than YUV→RGB conversion) Changes: - videoHandler.h: make cacheFrame/isInCache/getCachedFrames/ getNumberCachedFrames/invalidateAllBuffers virtual so YUV can override - videoHandlerYUV: add rawDataCache + rawDataCacheAccess mutex + rawDataCacheValid flag (mirrors imageCache pattern) - videoHandlerYUV: override cacheFrame (stores raw YUV), drawFrame (fetches from raw cache + converts on-the-fly), needsLoading, getCachingFrameSize (returns raw bytes), getCachedFrames/ getNumberCachedFrames/isInCache/removeFrameFromCache/ removeAllFrameFromCache/invalidateAllBuffers (operate on raw cache) - videoHandlerYUV: override setFrameSize to clear raw cache on resize - videoHandlerYUV: slotYUVControlChanged now distinguishes: - color matrix/interpolation/math/component: RECACHE_NONE (raw unchanged) - yuv format: RECACHE_CLEAR (raw layout changed) - videoHandlerYUV: setSrcPixelFormat clears raw cache (format changed) - videoHandlerYUV: new loadRawYUVDataForCaching for background caching - RGB handler unchanged (keeps imageCache) --- YUViewLib/src/video/videoHandler.h | 10 +- YUViewLib/src/video/yuv/videoHandlerYUV.cpp | 248 ++++++++++++++++++-- YUViewLib/src/video/yuv/videoHandlerYUV.h | 29 +++ 3 files changed, 266 insertions(+), 21 deletions(-) diff --git a/YUViewLib/src/video/videoHandler.h b/YUViewLib/src/video/videoHandler.h index 18fccf6b7..9c99277be 100644 --- a/YUViewLib/src/video/videoHandler.h +++ b/YUViewLib/src/video/videoHandler.h @@ -60,11 +60,11 @@ class videoHandler : public FrameHandler // --- Caching ---- // These methods are all thread-safe and can be invoked from any thread. int getNrFramesCached() const; - void cacheFrame(int frameIndex, bool testMode); + virtual void cacheFrame(int frameIndex, bool testMode); virtual unsigned getCachingFrameSize() const; - QList getCachedFrames() const; - int getNumberCachedFrames() const; - bool isInCache(int idx) const; + virtual QList getCachedFrames() const; + virtual int getNumberCachedFrames() const; + virtual bool isInCache(int idx) const; virtual void removeFrameFromCache(int frameIndex); virtual void removeAllFrameFromCache(); @@ -103,7 +103,7 @@ class videoHandler : public FrameHandler // If reloading a raw file (because it changed), this function will clear all buffers (also the // cache). With the next drawFrame(), the data will be reloaded from file. - void invalidateAllBuffers(); + virtual void invalidateAllBuffers(); // The user changed the frame. Do we need to load something before we can draw it? Do we need to // update the double buffer? loadRawValues: Do we also need to update the buffer of the raw values diff --git a/YUViewLib/src/video/yuv/videoHandlerYUV.cpp b/YUViewLib/src/video/yuv/videoHandlerYUV.cpp index d450a5f91..217e6ff2a 100644 --- a/YUViewLib/src/video/yuv/videoHandlerYUV.cpp +++ b/YUViewLib/src/video/yuv/videoHandlerYUV.cpp @@ -2395,9 +2395,142 @@ videoHandlerYUV::~videoHandlerYUV() unsigned videoHandlerYUV::getCachingFrameSize() const { - auto hasAlpha = this->srcPixelFormat.hasAlpha(); - auto bytes = functionsGui::bytesPerPixel(functionsGui::platformImageFormat(hasAlpha)); - return this->frameSize.width * this->frameSize.height * bytes; + // Return raw YUV bytes per frame (the cache stores raw YUV, not RGB). + return unsigned(this->getBytesPerFrame()); +} + +void videoHandlerYUV::setFrameSize(Size size) +{ + if (size != frameSize) + { + // Frame size changed: raw YUV data layout is different, clear the raw cache. + QMutexLocker lock(&rawDataCacheAccess); + rawDataCache.clear(); + rawDataCacheValid = true; + } + videoHandler::setFrameSize(size); +} + +void videoHandlerYUV::cacheFrame(int frameIdx, bool testMode) +{ + DEBUG_YUV("videoHandlerYUV::cacheFrame " << frameIdx << (testMode ? " testMode" : "")); + + if (rawDataCacheValid && isInCache(frameIdx) && !testMode) + { + DEBUG_YUV("videoHandlerYUV::cacheFrame frame " << frameIdx << " already in raw cache - returning"); + return; + } + + // Load raw YUV data for caching + QByteArray rawYUVData; + if (!loadRawYUVDataForCaching(frameIdx, rawYUVData)) + { + DEBUG_YUV("videoHandlerYUV::cacheFrame loading frame " << frameIdx << " for caching failed"); + return; + } + + if (testMode) + return; // testMode only measures load+convert speed, no insert + + // Put the raw YUV data into the cache + DEBUG_YUV("videoHandlerYUV::cacheFrame insert frame " << frameIdx << " into raw cache"); + QMutexLocker lock(&rawDataCacheAccess); + if (rawDataCacheValid) + rawDataCache.insert(frameIdx, rawYUVData); +} + +QList videoHandlerYUV::getCachedFrames() const +{ + QMutexLocker lock(&rawDataCacheAccess); + return rawDataCache.keys(); +} + +int videoHandlerYUV::getNumberCachedFrames() const +{ + QMutexLocker lock(&rawDataCacheAccess); + return rawDataCache.size(); +} + +bool videoHandlerYUV::isInCache(int idx) const +{ + QMutexLocker lock(&rawDataCacheAccess); + return rawDataCache.contains(idx); +} + +void videoHandlerYUV::removeFrameFromCache(int frameIdx) +{ + DEBUG_YUV("videoHandlerYUV::removeFrameFromCache " << frameIdx); + QMutexLocker lock(&rawDataCacheAccess); + rawDataCache.remove(frameIdx); +} + +void videoHandlerYUV::removeAllFrameFromCache() +{ + DEBUG_YUV("videoHandlerYUV::removeAllFrameFromCache"); + QMutexLocker lock(&rawDataCacheAccess); + rawDataCache.clear(); + rawDataCacheValid = true; +} + +void videoHandlerYUV::invalidateAllBuffers() +{ + currentFrameRawData_frameIndex = -1; + rawData_frameIndex = -1; + + currentImageIndex = -1; + currentImage_frameIndex = -1; + currentImageSetMutex.lock(); + currentImage = QImage(); + currentImageSetMutex.unlock(); + requestedFrame_idx = -1; + + QMutexLocker lock(&rawDataCacheAccess); + rawDataCache.clear(); + rawDataCacheValid = true; +} + +ItemLoadingState videoHandlerYUV::needsLoading(int frameIdx, bool loadRawValues) +{ + if (loadRawValues) + { + auto state = needsLoadingRawValues(frameIdx); + if (state != ItemLoadingState::LoadingNotNeeded) + return state; + } + + // Check if the current frame is already loaded + if (frameIdx == currentImageIndex) + { + if (doubleBufferImageFrameIndex == frameIdx + 1) + return ItemLoadingState::LoadingNotNeeded; + else if (rawDataCacheValid && isInCache(frameIdx + 1)) + return ItemLoadingState::LoadingNotNeeded; + else + return ItemLoadingState::LoadingNeededDoubleBuffer; + } + + // Check the double buffer + if (doubleBufferImageFrameIndex == frameIdx) + { + if (rawDataCacheValid && isInCache(frameIdx + 1)) + return ItemLoadingState::LoadingNotNeeded; + else + return ItemLoadingState::LoadingNeededDoubleBuffer; + } + + // Check the raw data cache + if (rawDataCacheValid && isInCache(frameIdx)) + { + if (doubleBufferImageFrameIndex == frameIdx + 1) + return ItemLoadingState::LoadingNotNeeded; + else if (rawDataCacheValid && isInCache(frameIdx + 1)) + return ItemLoadingState::LoadingNotNeeded; + else + return ItemLoadingState::LoadingNeededDoubleBuffer; + } + + // Frame not in cache. Request loading. + return ItemLoadingState::LoadingNeeded; } void videoHandlerYUV::loadValues(Size newFramesize, const QString &) @@ -2426,9 +2559,70 @@ void videoHandlerYUV::drawFrame(QPainter *painter, // Draw the text painter->drawText(textRect, QString::fromStdString(msg)); + return; } - else - videoHandler::drawFrame(painter, frameIdx, zoomFactor, drawRawData); + + // Check if the frameIdx changed and we have to load a new frame + if (frameIdx != currentImageIndex) + { + // Check the double buffer first (holds a converted RGB QImage) + if (frameIdx == doubleBufferImageFrameIndex) + { + currentImage = doubleBufferImage; + currentImageIndex = frameIdx; + DEBUG_YUV("videoHandlerYUV::drawFrame " << frameIdx << " loaded from double buffer"); + } + else + { + // Try to get raw YUV data from the raw data cache + QByteArray rawYUVData; + { + QMutexLocker lock(&rawDataCacheAccess); + if (rawDataCacheValid && rawDataCache.contains(frameIdx)) + rawYUVData = rawDataCache[frameIdx]; + } + + if (rawYUVData.isEmpty()) + { + // Cache miss: load raw YUV data via the interactive path + if (!loadRawYUVData(frameIdx)) + return; // Loading failed + rawYUVData = currentFrameRawData; + } + + // Convert YUV to RGB on-the-fly + QImage newImage; + try + { + convertYUVToImage(rawYUVData, + newImage, + this->srcPixelFormat, + this->frameSize, + this->conversionSettings); + } + catch (const std::bad_alloc &) + { + qWarning() << "Out of memory in drawFrame convertYUVToImage."; + return; + } + QMutexLocker setLock(¤tImageSetMutex); + currentImage = newImage; + currentImageIndex = frameIdx; + } + } + + // Create the video QRect with the size of the sequence and center it. + QRect videoRect; + videoRect.setSize(QSize(frameSize.width * zoomFactor, frameSize.height * zoomFactor)); + videoRect.moveCenter(QPoint(0, 0)); + + // Draw the current image + currentImageSetMutex.lock(); + painter->drawImage(videoRect, currentImage); + currentImageSetMutex.unlock(); + + if (drawRawData && zoomFactor >= SPLITVIEW_DRAW_VALUES_ZOOMFACTOR) + drawPixelValues(painter, frameIdx, videoRect, zoomFactor); } QLayout *videoHandlerYUV::createVideoHandlerControls(bool isSizeAndFormatFixed) @@ -2619,8 +2813,9 @@ void videoHandlerYUV::setSrcPixelFormat(PixelFormatYUV format, bool emitSignal) this->currentImageIndex = -1; this->currentImage_frameIndex = -1; - // Set the cache to invalid until it is cleared an recached - this->setCacheInvalid(); + // The pixel format changed. The raw YUV data must be re-interpreted, so the + // raw data cache is invalid. + this->setRawDataCacheInvalid(); if (srcPixelFormat.bytesPerFrame(frameSize) != oldFormatBytesPerFrame) // The number of bytes per frame changed. The raw YUV data buffer is also out of date @@ -2659,12 +2854,12 @@ void videoHandlerYUV::slotYUVControlChanged() this->conversionSettings.mathParameters[Component::Chroma].invert = ui.chromaInvertCheckBox->isChecked(); - // Set the current frame in the buffer to be invalid and clear the cache. - // Emit that this item needs redraw and the cache needs updating. + // These parameters only affect YUV→RGB conversion, not the raw YUV data. + // So we only invalidate the current displayed image (force re-conversion) and + // do NOT clear the raw data cache or trigger a recache. this->currentImageIndex = -1; this->currentImage_frameIndex = -1; - this->setCacheInvalid(); - emit signalHandlerChanged(true, RECACHE_CLEAR); + emit signalHandlerChanged(true, RECACHE_NONE); } else if (sender == ui.yuvFormatComboBox) { @@ -2673,14 +2868,13 @@ void videoHandlerYUV::slotYUVControlChanged() // Set the new YUV format // setSrcPixelFormat(yuvFormatList.getFromName(ui.yuvFormatComboBox->currentText())); - // Set the current frame in the buffer to be invalid and clear the cache. - // Emit that this item needs redraw and the cache needs updating. + // The YUV format changed. The raw YUV data must be re-interpreted, so the + // raw data cache is invalid. this->currentImageIndex = -1; this->currentImage_frameIndex = -1; if (this->srcPixelFormat.bytesPerFrame(frameSize) != oldFormatBytesPerFrame) - // The number of bytes per frame changed. The raw YUV data buffer also has to be updated. this->currentFrameRawData_frameIndex = -1; - this->setCacheInvalid(); + this->setRawDataCacheInvalid(); emit signalHandlerChanged(true, RECACHE_CLEAR); } } @@ -3262,7 +3456,7 @@ void videoHandlerYUV::loadFrameForCaching(int frameIndex, QImage &frameToCache) // Load the raw YUV data for the given frame index into currentFrameRawData. bool videoHandlerYUV::loadRawYUVData(int frameIndex) { - if (currentFrameRawData_frameIndex == frameIndex && cacheValid) + if (currentFrameRawData_frameIndex == frameIndex && rawDataCacheValid) // Buffer already up to date return true; @@ -3289,6 +3483,28 @@ bool videoHandlerYUV::loadRawYUVData(int frameIndex) return true; } +// Load raw YUV data for caching (background thread). Does not modify currentFrameRawData. +bool videoHandlerYUV::loadRawYUVDataForCaching(int frameIndex, QByteArray &outData) +{ + if (!isFormatValid()) + return false; + + DEBUG_YUV("videoHandlerYUV::loadRawYUVDataForCaching " << frameIndex); + + requestDataMutex.lock(); + emit signalRequestRawData(frameIndex, true); + outData = rawData; + requestDataMutex.unlock(); + + if (frameIndex != rawData_frameIndex || outData.isEmpty()) + { + DEBUG_YUV("videoHandlerYUV::loadRawYUVDataForCaching Loading failed"); + return false; + } + + return true; +} + yuv_t videoHandlerYUV::getPixelValue(const QPoint &pixelPos) const { if (!isFormatValid()) diff --git a/YUViewLib/src/video/yuv/videoHandlerYUV.h b/YUViewLib/src/video/yuv/videoHandlerYUV.h index b6b6f694b..50d7482d8 100644 --- a/YUViewLib/src/video/yuv/videoHandlerYUV.h +++ b/YUViewLib/src/video/yuv/videoHandlerYUV.h @@ -99,6 +99,18 @@ class videoHandlerYUV : public videoHandler virtual void drawFrame(QPainter *painter, int frameIdx, double zoomFactor, bool drawRawData) override; + // --- Caching overrides --- + // videoHandlerYUV caches raw YUV bytes (not converted RGB) so that color matrix + // changes only require re-conversion, not re-reading/re-decoding. + void cacheFrame(int frameIndex, bool testMode) override; + QList getCachedFrames() const override; + int getNumberCachedFrames() const override; + bool isInCache(int idx) const override; + void removeFrameFromCache(int frameIndex) override; + void removeAllFrameFromCache() override; + void invalidateAllBuffers() override; + ItemLoadingState needsLoading(int frameIndex, bool loadRawValues) override; + // Return the YUV values for the given pixel // If a second item is provided, return the difference values to that item at the given position. // If th second item cannot be cast to a videoHandlerYUV, we call the FrameHandler::getPixelValues @@ -125,6 +137,9 @@ class videoHandlerYUV : public videoHandler return srcPixelFormat.bytesPerFrame(frameSize); } + // Override: when the frame size changes, the raw YUV cache is invalid. + virtual void setFrameSize(Size size) override; + void guessAndSetPixelFormat(const filesource::frameFormatGuess::GuessedFrameFormat &frameFormat, const filesource::frameFormatGuess::FileInfoForGuess &fileInfo) override; @@ -233,6 +248,20 @@ class videoHandlerYUV : public videoHandler QByteArray diffYUV; PixelFormatYUV diffYUVFormat{}; + // --- Raw YUV data cache (primary cache) --- + // Stores raw YUV bytes per frame index. RGB conversion is done on-the-fly in + // drawFrame. This way, changing color matrix / interpolation / math parameters + // only invalidates currentImage (one re-conversion), not the whole cache. + QMutex mutable rawDataCacheAccess; + QMap rawDataCache; + bool rawDataCacheValid{true}; + + // Set the raw data cache to invalid (in-flight threads will not insert). + void setRawDataCacheInvalid() { rawDataCacheValid = false; } + + // Load raw YUV data for caching into the rawDataCache. Returns true on success. + bool loadRawYUVDataForCaching(int frameIndex, QByteArray &outData); + static std::vector formatPresetList; private slots: From 3bd11b3e8e7d182f13aa24bfd3860a47af9a230f Mon Sep 17 00:00:00 2001 From: think3r Date: Tue, 14 Jul 2026 11:04:43 +0800 Subject: [PATCH 52/55] Fix Unix CI build: disambiguate QDebug << std::string_view in AVC parser NalTypeMapper.getName() returns std::string_view. Piping it directly to LOG_DEBUG (QDebug) is ambiguous on GCC/Ubuntu: QDebug has both operator<<(QUtf8StringView) and operator<<(QByteArrayView), each constructible from std::string_view. Wrap with QString::fromStdString(std::string(...)) to match the existing pattern used in VVC/picture_header_structure.cpp. Also restore the toUnsigned declaration in Functions.h that was accidentally removed in 597417b8 (it was the only declaration, not a duplicate). --- YUViewLib/src/common/Functions.h | 1 + YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/YUViewLib/src/common/Functions.h b/YUViewLib/src/common/Functions.h index b1c57b72b..ef313ebb6 100644 --- a/YUViewLib/src/common/Functions.h +++ b/YUViewLib/src/common/Functions.h @@ -81,6 +81,7 @@ template QStringList toQStringList(const std::array toInt(const std::string_view str); +std::optional toUnsigned(const std::string_view str); std::vector splitString(const std::string_view str, const char delimiter); std::string_view stripWhitespace(std::string_view str); diff --git a/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp b/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp index 4ef3fb837..57726f561 100644 --- a/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp +++ b/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp @@ -401,7 +401,7 @@ ParserAnnexBAVC::parseAndAddNALUnit(int } DEBUG_AVC("ParserAnnexBAVC::parseAndAddNALUnit Parsed Slice (" - << NalTypeMapper.getName(nalAVC->header.nal_unit_type) + << QString::fromStdString(std::string(NalTypeMapper.getName(nalAVC->header.nal_unit_type))) << ") POC " << newSliceHeader->globalPOC); parseResult.nalTypeName = "Slice(POC " + std::to_string(newSliceHeader->globalPOC) + ") "; } From 2fb7babe74be0c32e5a2dddddeffec897842c49a Mon Sep 17 00:00:00 2001 From: think3r Date: Tue, 14 Jul 2026 15:15:28 +0800 Subject: [PATCH 53/55] Fix rawDataCacheValid data race and remove dead loadFrameForCaching code - Make rawDataCacheValid std::atomic to eliminate the data race between cacheFrame()'s unlocked read and setRawDataCacheInvalid()'s unlocked write. The insert path still re-checks under the mutex. - Remove the never-called videoHandlerYUV::loadFrameForCaching override (cacheFrame is fully overridden to store raw YUV bytes, so the base class RGB-image caching path is never used for YUV items). --- YUViewLib/src/video/yuv/videoHandlerYUV.cpp | 60 +++++---------------- YUViewLib/src/video/yuv/videoHandlerYUV.h | 15 +++--- 2 files changed, 20 insertions(+), 55 deletions(-) diff --git a/YUViewLib/src/video/yuv/videoHandlerYUV.cpp b/YUViewLib/src/video/yuv/videoHandlerYUV.cpp index 217e6ff2a..acaf2d04f 100644 --- a/YUViewLib/src/video/yuv/videoHandlerYUV.cpp +++ b/YUViewLib/src/video/yuv/videoHandlerYUV.cpp @@ -2406,7 +2406,7 @@ void videoHandlerYUV::setFrameSize(Size size) // Frame size changed: raw YUV data layout is different, clear the raw cache. QMutexLocker lock(&rawDataCacheAccess); rawDataCache.clear(); - rawDataCacheValid = true; + rawDataCacheValid.store(true, std::memory_order_release); } videoHandler::setFrameSize(size); } @@ -2415,7 +2415,7 @@ void videoHandlerYUV::cacheFrame(int frameIdx, bool testMode) { DEBUG_YUV("videoHandlerYUV::cacheFrame " << frameIdx << (testMode ? " testMode" : "")); - if (rawDataCacheValid && isInCache(frameIdx) && !testMode) + if (rawDataCacheValid.load(std::memory_order_acquire) && isInCache(frameIdx) && !testMode) { DEBUG_YUV("videoHandlerYUV::cacheFrame frame " << frameIdx << " already in raw cache - returning"); return; @@ -2435,7 +2435,7 @@ void videoHandlerYUV::cacheFrame(int frameIdx, bool testMode) // Put the raw YUV data into the cache DEBUG_YUV("videoHandlerYUV::cacheFrame insert frame " << frameIdx << " into raw cache"); QMutexLocker lock(&rawDataCacheAccess); - if (rawDataCacheValid) + if (rawDataCacheValid.load(std::memory_order_acquire)) rawDataCache.insert(frameIdx, rawYUVData); } @@ -2469,7 +2469,7 @@ void videoHandlerYUV::removeAllFrameFromCache() DEBUG_YUV("videoHandlerYUV::removeAllFrameFromCache"); QMutexLocker lock(&rawDataCacheAccess); rawDataCache.clear(); - rawDataCacheValid = true; + rawDataCacheValid.store(true, std::memory_order_release); } void videoHandlerYUV::invalidateAllBuffers() @@ -2486,7 +2486,7 @@ void videoHandlerYUV::invalidateAllBuffers() QMutexLocker lock(&rawDataCacheAccess); rawDataCache.clear(); - rawDataCacheValid = true; + rawDataCacheValid.store(true, std::memory_order_release); } ItemLoadingState videoHandlerYUV::needsLoading(int frameIdx, bool loadRawValues) @@ -2503,7 +2503,7 @@ ItemLoadingState videoHandlerYUV::needsLoading(int frameIdx, bool loadRawValues) { if (doubleBufferImageFrameIndex == frameIdx + 1) return ItemLoadingState::LoadingNotNeeded; - else if (rawDataCacheValid && isInCache(frameIdx + 1)) + else if (rawDataCacheValid.load(std::memory_order_acquire) && isInCache(frameIdx + 1)) return ItemLoadingState::LoadingNotNeeded; else return ItemLoadingState::LoadingNeededDoubleBuffer; @@ -2512,18 +2512,18 @@ ItemLoadingState videoHandlerYUV::needsLoading(int frameIdx, bool loadRawValues) // Check the double buffer if (doubleBufferImageFrameIndex == frameIdx) { - if (rawDataCacheValid && isInCache(frameIdx + 1)) + if (rawDataCacheValid.load(std::memory_order_acquire) && isInCache(frameIdx + 1)) return ItemLoadingState::LoadingNotNeeded; else return ItemLoadingState::LoadingNeededDoubleBuffer; } // Check the raw data cache - if (rawDataCacheValid && isInCache(frameIdx)) + if (rawDataCacheValid.load(std::memory_order_acquire) && isInCache(frameIdx)) { if (doubleBufferImageFrameIndex == frameIdx + 1) return ItemLoadingState::LoadingNotNeeded; - else if (rawDataCacheValid && isInCache(frameIdx + 1)) + else if (rawDataCacheValid.load(std::memory_order_acquire) && isInCache(frameIdx + 1)) return ItemLoadingState::LoadingNotNeeded; else return ItemLoadingState::LoadingNeededDoubleBuffer; @@ -2578,7 +2578,7 @@ void videoHandlerYUV::drawFrame(QPainter *painter, QByteArray rawYUVData; { QMutexLocker lock(&rawDataCacheAccess); - if (rawDataCacheValid && rawDataCache.contains(frameIdx)) + if (rawDataCacheValid.load(std::memory_order_acquire) && rawDataCache.contains(frameIdx)) rawYUVData = rawDataCache[frameIdx]; } @@ -3417,52 +3417,16 @@ void videoHandlerYUV::loadFrame(int frameIndex, bool loadToDoubleBuffer) } } -void videoHandlerYUV::loadFrameForCaching(int frameIndex, QImage &frameToCache) -{ - if (!isFormatValid()) - return; - - // Get the YUV format and the size here, so that the caching process does not crash if this - // changes. - const auto yuvFormat = this->srcPixelFormat; - const auto curFrameSize = this->frameSize; - const auto conversionSettings = this->conversionSettings; - - requestDataMutex.lock(); - emit signalRequestRawData(frameIndex, true); - QByteArray tmpBufferRawYUVDataCaching = rawData; - requestDataMutex.unlock(); - - if (frameIndex != rawData_frameIndex) - { - // Loading failed - DEBUG_YUV("videoHandlerYUV::loadFrameForCaching Loading failed"); - return; - } - - // Convert YUV to image. This can then be cached. - try - { - convertYUVToImage( - tmpBufferRawYUVDataCaching, frameToCache, yuvFormat, curFrameSize, conversionSettings); - } - catch (const std::bad_alloc &) - { - qWarning() << "Out of memory in loadFrameForCaching convertYUVToImage."; - frameToCache = QImage(); - } -} - // Load the raw YUV data for the given frame index into currentFrameRawData. bool videoHandlerYUV::loadRawYUVData(int frameIndex) { - if (currentFrameRawData_frameIndex == frameIndex && rawDataCacheValid) + if (currentFrameRawData_frameIndex == frameIndex && rawDataCacheValid.load(std::memory_order_acquire)) // Buffer already up to date return true; DEBUG_YUV("videoHandlerYUV::loadRawYUVData " << frameIndex); - // The function loadFrameForCaching also uses the signalRequesRawYUVData to request raw data. + // The raw data loading path uses signalRequestRawData to request raw data. // However, only one thread can use this at a time. requestDataMutex.lock(); emit signalRequestRawData(frameIndex, false); diff --git a/YUViewLib/src/video/yuv/videoHandlerYUV.h b/YUViewLib/src/video/yuv/videoHandlerYUV.h index 50d7482d8..3288e31bf 100644 --- a/YUViewLib/src/video/yuv/videoHandlerYUV.h +++ b/YUViewLib/src/video/yuv/videoHandlerYUV.h @@ -34,11 +34,13 @@ #include #include + #include