diff --git a/.gitignore b/.gitignore index 07d51c4ef..efe4600f2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ YUView.pro.user .vscode issues .DS_Store +build-asan/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c749a8405 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,114 @@ +# AGENTS.md + +Guidance for AI coding agents working on the YUView codebase. + +## 构建项目 + +根目录两个脚本封装环境初始化、qmake 配置和并行编译: + +### Windows(MSVC + Qt6 + jom) + +```bat +build.bat +``` + +自动 `vcvars64.bat` → `build/` → `qmake ..` → `jom`。WSL 下用 `cmd.exe /c build.bat`。默认**不启用**测试;如需: +```bat +mkdir build && cd build +qmake .. CONFIG+=UNITTESTS +jom +``` + +### Linux / macOS + +```bash +./build.sh # 默认启用单元测试 +./build.sh --asan # ASan 构建,禁用测试,产物到 build-asan/ +``` + +自动探测 `qmake6`/`qmake`,按 `nproc`/`sysctl` 并行;macOS 同此脚本。 + +### 启用单元测试 + +须加 `CONFIG+=UNITTESTS`,否则 `YUViewUnitTest` 和 googletest 不进 SUBDIRS。Linux/macOS 由 `build.sh` 默认加;Windows 需手动。 + +Windows 相关 target(见根 Makefile):`sub-submodules-googletest-qmake`(gtest.lib)、`sub-YUViewUnitTest`(测试 exe)。`jom sub-YUViewUnitTest` 不自动构建 gtest,需先 `jom sub-submodules-googletest-qmake` 或全量 `jom`。 + +## 运行测试 + +```bash +./build/YUViewUnitTest/YUViewUnitTest # Linux +QT_QPA_PLATFORM=offscreen ./build/YUViewUnitTest/YUViewUnitTest # CI/无显示 +build\YUViewUnitTest\YUViewUnitTest.exe # Windows +build\YUViewUnitTest\YUViewUnitTest.exe --gtest_filter=StatisticsFileCSV.* # 单套件 +``` + +套件名用 `--gtest_list_tests` 查看。`StatisticsFileCSVTest.cpp` 与 `StatisticsFileVTMBMSTest.cpp` 都定义了 `TEST(StatisticsFileCSV, testCSVFileParsing)`,同名注册为同套件两个用例。 + +## 子模块 + +```bash +git clone --recurse-submodules +# 或 +git submodule update --init --recursive +``` + +`submodules/googletest` 是测试依赖。SSH 克隆失败可临时用 HTTPS(用 `git -c url.<...>.insteadOf=<...> submodule update --init` 或直接 clone 后 checkout 到记录的 commit)。 + +## 项目结构 + +- **YUViewLib/** — 核心业务逻辑(静态库):`parser/`(HEVC/AVC/VVC/AV1/MPEG2)、`decoder/`(FFmpeg/Dav1d/libde265/HM/VTM)、`video/`(RGB/YUV)、`playlistitem/`、`ui/`、`statistics/` +- **YUViewApp/** — Qt 应用入口 +- **YUViewUnitTest/** — Google Test 单元测试 +- **submodules/** — googletest 与 googletest-qmake +- **deployment/** — Windows NSIS 安装包脚本 + +## 架构模式 + +1. **解码器插件**:`decoderBase` 抽象接口,`QLibrary` 动态加载 dll;`decoderFFmpeg`/`Dav1d`/`Libde265`/`HM`/`VTM` 为实现 +2. **播放列表项**:`playlistItem` 基类,子类对应不同媒体类型 +3. **视频处理器**:`videoHandler` 及子类(`Difference`、`Resample`) +4. **解析器**:按编码器在 `parser/` 下组织 +5. **统计绘制**:`stats::paintStatisticsData` 统一绘制,样式由 `StatisticsType::gridStyle`/`vectorStyle`(`LineDrawStyle`)控制 + +## 解码器库加载约定 + +- 运行时 `QLibrary` 动态加载,逻辑见 `decoderBase.cpp:80`(`loadDecoderLibrary`),候选名由 `getLibraryNames()` 返回按序尝试 +- **internals 支持由符号解析(`QLibrary::resolve`)决定,不是文件名**:libde265 的 `["libde265-internals", "libde265"]` 仅为加载优先级,真正判据是能否 resolve 出 `de265_internals_*`(见 `decoderLibde265.cpp:166`)。`libde265-internals.dll` 导出 internals 接口提供编码结构统计(CTU/CU/PB/IntraDir/TU),官方 `libde265.dll` 只能解码。VTM/HM 同理靠 `internalsSupported` 标志 + +## 代码规范 + +- C++20,`.clang-format`(2 空格、Allman 风格),UTF8/LF +- 成员变量 CamelCase,**无前缀**(不要 `m_`/`i`/`p`) +- 参数:基本类型按值,复杂类型按 const 引用;const 正确性 +- **核心代码优先 Qt-free**:业务逻辑用 `std` 而非 Qt 类型,Qt 仅用于 GUI +- 详细规范见 `HACKING.md` + +### Include 顺序(`.cpp`) + +1. `"foo.h"`(对应头文件,必须第一个,后空一行) +2. `` 标准库(C 头用 `` 而非 ``) +3. 系统头 → 4. 其它库头 → 5. Qt 头 → 6. YUView 本地头 + +`.h` 只 include 声明所必需的,按值持有必须 include,按指针/引用持有可前向声明。 + +## 提交约定 + +- 分支 `develop`;英文祈使句,首行简短,正文解释 why +- 示例:`Fix rawDataCacheValid data race and remove dead loadFrameForCaching code` +- **不要主动 commit**,除非用户明确要求 + +## 常见陷阱 + +- **改全局 git config**:临时 url 重写用 `git -c`,不要 `--global` +- **cmd.exe 下的 gtest_filter**:冒号 `:` 会被误解析,分多次运行或用单个 filter +- **`jom sub-YUViewUnitTest` 不构建 gtest**:需先 `sub-submodules-googletest-qmake` 或全量 `jom` +- **测试同名冲突**:两文件都定义 `TEST(StatisticsFileCSV, testCSVFileParsing)`,修改时两个都要同步 +- **`StatisticsType` 默认值变更**:构造函数改默认值后 `setInitialState()` 捕获的 init 状态同步变化,相关单测断言可能需同步更新 + +## 依赖 + +- Qt6(qt6-base-dev / Qt 6.9.0 Windows 预编译包) +- FFmpeg(视频解码) +- libde265(HEVC 解码,可选;internals 变体提供统计) +- Google Test(git 子模块) diff --git a/YUViewApp/YUViewApp.pro b/YUViewApp/YUViewApp.pro index 561012031..ee69e0fde 100644 --- a/YUViewApp/YUViewApp.pro +++ b/YUViewApp/YUViewApp.pro @@ -59,7 +59,7 @@ macx { isEmpty(PREFIX) { PREFIX = / } - isEmpty(BIINDIR) { + isEmpty(BINDIR) { BINDIR = Applications } @@ -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/YUViewApp/src/yuviewapp.cpp b/YUViewApp/src/yuviewapp.cpp index f63628294..75f1e89f5 100644 --- a/YUViewApp/src/yuviewapp.cpp +++ b/YUViewApp/src/yuviewapp.cpp @@ -31,8 +31,13 @@ */ #include +#include + +#include +#include #include +#include #include int main(int argc, char *argv[]) @@ -45,8 +50,33 @@ int main(int argc, char *argv[]) QCoreApplication::setAttribute(Qt::AA_SynthesizeTouchForUnhandledMouseEvents,false); qRegisterMetaType("recacheIndicator"); - - YUViewApplication app(argc, argv); - return app.returnCode; + try + { + YUViewApplication app(argc, argv); +#if defined(Q_OS_MAC) && QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) + LOG_DEBUG(logApp) << "main: app constructed, about to exit"; + // Workaround for a Qt regression on macOS >=6.10: ~QApplication() crashes with + // a null pointer dereference inside the Cocoa platform plugin during teardown. + // All application-layer cleanup (Logger, MainWindow, threads) has already + // completed in the YUViewApplication constructor body above. + fflush(stderr); + std::_Exit(app.returnCode); +#else + return app.returnCode; +#endif + } + catch (const std::bad_alloc &) + { + try + { + if (QCoreApplication::instance()) + QMessageBox::critical(nullptr, "YUView", "Out of memory."); + } + catch (...) + { + qWarning() << "YUView: Out of memory."; + } + return 1; + } } 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..ef313ebb6 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); @@ -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); @@ -104,7 +105,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 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/common/FunctionsGui.h b/YUViewLib/src/common/FunctionsGui.h index 73f2bd035..585cb513d 100644 --- a/YUViewLib/src/common/FunctionsGui.h +++ b/YUViewLib/src/common/FunctionsGui.h @@ -38,6 +38,9 @@ #include #include #include +#include + +#include /* This functions class is called "GUI" because you must link to the gui module of QT @@ -109,4 +112,15 @@ void setupUi(void *ui, void (*setupUi)(void *ui, QWidget *widget)); // Return the icon/pixmap from the given file path (inverted if necessary) QIcon convertIcon(QString iconPath); +// Convert a QString to a std::filesystem::path using the wide-string interface. +// This is necessary because QString::toStdString() returns UTF-8 bytes, which on +// Windows are misinterpreted as the system ANSI codepage (e.g. GBK) by +// std::filesystem::path(const std::string&) and std::ifstream(const std::string&). +// Using toStdWString() (UTF-16 on Windows, UTF-32 on Linux/macOS) is decoded +// correctly on all platforms. +inline std::filesystem::path toFileSystemPath(const QString &str) +{ + return std::filesystem::path(str.toStdWString()); +} + } // namespace functionsGui diff --git a/YUViewLib/src/common/Typedef.h b/YUViewLib/src/common/Typedef.h index 155e6f11d..47631f75e 100644 --- a/YUViewLib/src/common/Typedef.h +++ b/YUViewLib/src/common/Typedef.h @@ -184,6 +184,8 @@ struct Ratio struct Size { + static constexpr unsigned MAX_DIMENSION = 16384; + constexpr Size(unsigned width, unsigned height) : width(width), height(height) {} constexpr Size() = default; @@ -196,7 +198,11 @@ struct Size return this->width != other.width || this->height != other.height; } explicit operator bool() const { return this->isValid(); } - constexpr bool isValid() const { return this->width > 0 && this->height > 0; } + constexpr bool isValid() const + { + return this->width > 0 && this->width <= MAX_DIMENSION && + this->height > 0 && this->height <= MAX_DIMENSION; + } unsigned width{}; unsigned height{}; }; diff --git a/YUViewLib/src/crash/CrashHandler.cpp b/YUViewLib/src/crash/CrashHandler.cpp new file mode 100644 index 000000000..87fd28a54 --- /dev/null +++ b/YUViewLib/src/crash/CrashHandler.cpp @@ -0,0 +1,239 @@ +/* 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 WriteFile via a lambda — CRT write() is not async-signal-safe + // and we are in a terminate handler. + 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 / std::terminate, so they use +// safeWrite() (raw write()/WriteFile) instead of buffered I/O. Note: +// s_appVersion.toUtf8() below is NOT async-signal-safe (it may allocate); +// this is a known trade-off so that a version string appears in the report. +// --------------------------------------------------------------------------- + +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..5606d1bcb --- /dev/null +++ b/YUViewLib/src/crash/CrashHandlerMac.cpp @@ -0,0 +1,252 @@ +/* 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 + +// --------------------------------------------------------------------------- +// Internal helpers (all async-signal-safe unless noted) +// --------------------------------------------------------------------------- + +namespace +{ + +// Pre-converted (at init time) UTF-8 copy of the log directory path. +// Using a plain char[] avoids any heap allocation inside the signal handler. +static char g_logDirBuf[PATH_MAX]{}; + +// 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 (re-entrant; in + // practice async-signal-safe on glibc/Bionic, though not guaranteed by + // strict POSIX signal-safety(7)). + 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; + + // g_logDirBuf was pre-populated in installPlatformHandlers() so that no + // heap allocation (QString copy / toUtf8()) is needed here. + char path[512]{}; + buildCrashLogPath(path, sizeof(path), g_logDirBuf); + + 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() +{ + // Pre-convert the log directory path to a plain C string so that the signal + // handler can use it without calling any heap-allocating Qt functions. + { + const QByteArray dirBytes = crashLogDirectory().toUtf8(); + strncpy(g_logDirBuf, dirBytes.constData(), sizeof(g_logDirBuf) - 1); + g_logDirBuf[sizeof(g_logDirBuf) - 1] = '\0'; + } + + 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/dataSource/DataSourceLocalFile.cpp b/YUViewLib/src/dataSource/DataSourceLocalFile.cpp index 34becf6a1..08b57fc9b 100644 --- a/YUViewLib/src/dataSource/DataSourceLocalFile.cpp +++ b/YUViewLib/src/dataSource/DataSourceLocalFile.cpp @@ -60,7 +60,7 @@ getLastWriteTime(const std::filesystem::path &filePath) noexcept DataSourceLocalFile::DataSourceLocalFile(const std::filesystem::path &filePath) { this->filePath = filePath; - this->file.open(this->filePath.string(), std::ios_base::in | std::ios_base::binary); + this->file.open(this->filePath, std::ios_base::in | std::ios_base::binary); if (this->isOk()) this->lastWriteTime = getLastWriteTime(this->filePath); } @@ -114,7 +114,7 @@ void DataSourceLocalFile::clearFileCache() CreateFile(file, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL); CloseHandle(hFile); - this->file.open(this->filePath.string(), std::ios_base::in | std::ios_base::binary); + this->file.open(this->filePath, std::ios_base::in | std::ios_base::binary); #endif } @@ -132,7 +132,7 @@ bool DataSourceLocalFile::wasSourceModified() const void DataSourceLocalFile::reloadAndResetDataSource() { this->file.close(); - this->file.open(this->filePath.string(), std::ios_base::in | std::ios_base::binary); + this->file.open(this->filePath, std::ios_base::in | std::ios_base::binary); if (this->isOk()) this->lastWriteTime = getLastWriteTime(this->filePath); } diff --git a/YUViewLib/src/decoder/decoderBase.cpp b/YUViewLib/src/decoder/decoderBase.cpp index 22688152e..88734d20e 100644 --- a/YUViewLib/src/decoder/decoderBase.cpp +++ b/YUViewLib/src/decoder/decoderBase.cpp @@ -31,6 +31,7 @@ */ #include "decoderBase.h" +#include #include #include @@ -38,29 +39,8 @@ namespace decoder { -// Debug the decoder ( 0:off 1:interactive decoder only 2:caching decoder only 3:both) -#define DECODERBASE_DEBUG_OUTPUT 0 -#if DECODERBASE_DEBUG_OUTPUT && !NDEBUG -#include -#if DECODERBASE_DEBUG_OUTPUT == 1 -#define DEBUG_HEVCDECODERBASE \ - if (!isCachingDecoder) \ - qDebug -#elif DECODERBASE_DEBUG_OUTPUT == 2 -#define DEBUG_HEVCDECODERBASE \ - if (isCachingDecoder) \ - qDebug -#elif DECODERBASE_DEBUG_OUTPUT == 3 -#define DEBUG_HEVCDECODERBASE \ - if (isCachingDecoder) \ - qDebug("c:"); \ - else \ - qDebug("i:"); \ - qDebug -#endif -#else -#define DEBUG_DECODERBASE(fmt, ...) ((void)0) -#endif +#define DEBUG_DECODERBASE(...) qCDebug(logDecoder, __VA_ARGS__) +#define DEBUG_HEVCDECODERBASE(msg) LOG_DEBUG(logDecoder) << msg decoderBase::decoderBase(bool cachingDecoder) { diff --git a/YUViewLib/src/decoder/decoderDav1d.cpp b/YUViewLib/src/decoder/decoderDav1d.cpp index 7792a9625..e76a02760 100644 --- a/YUViewLib/src/decoder/decoderDav1d.cpp +++ b/YUViewLib/src/decoder/decoderDav1d.cpp @@ -31,6 +31,7 @@ */ #include "decoderDav1d.h" +#include #include #include @@ -46,29 +47,7 @@ namespace decoder using Subsampling = video::yuv::Subsampling; -// Debug the decoder (0:off 1:interactive decoder only 2:caching decoder only 3:both) -#define DECODERDAV1D_DEBUG_OUTPUT 0 -#if DECODERDAV1D_DEBUG_OUTPUT && !NDEBUG -#include -#if DECODERDAV1D_DEBUG_OUTPUT == 1 -#define DEBUG_DAV1D \ - if (!isCachingDecoder) \ - qDebug -#elif DECODERDAV1D_DEBUG_OUTPUT == 2 -#define DEBUG_DAV1D \ - if (isCachingDecoder) \ - qDebug -#elif DECODERDAV1D_DEBUG_OUTPUT == 3 -#define DEBUG_DAV1D \ - if (isCachingDecoder) \ - qDebug("c:"); \ - else \ - qDebug("i:"); \ - qDebug -#endif -#else -#define DEBUG_DAV1D(fmt, ...) ((void)0) -#endif +#define DEBUG_DAV1D(...) qCDebug(logDecoder, __VA_ARGS__) namespace { diff --git a/YUViewLib/src/decoder/decoderFFmpeg.cpp b/YUViewLib/src/decoder/decoderFFmpeg.cpp index 622e56ac6..b9f4b27fa 100644 --- a/YUViewLib/src/decoder/decoderFFmpeg.cpp +++ b/YUViewLib/src/decoder/decoderFFmpeg.cpp @@ -31,16 +31,11 @@ */ #include "decoderFFmpeg.h" +#include #include -#define DECODERFFMPEG_DEBUG_OUTPUT 0 -#if DECODERFFMPEG_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_FFMPEG(f) qDebug() << f -#else -#define DEBUG_FFMPEG(f) ((void)0) -#endif +#define DEBUG_FFMPEG(msg) LOG_DEBUG(logDecoder) << msg namespace decoder { @@ -348,7 +343,6 @@ bool decoderFFmpeg::pushAVPacket(FFmpeg::AVPacketWrapper &pkt) if (retPush < 0 && retPush != AVERROR(EAGAIN)) { -#if DECODERFFMPEG_DEBUG_OUTPUT { QString meaning = QString("decoderFFmpeg::pushAVPacket: Error sending packet - err %1").arg(retPush); @@ -364,9 +358,8 @@ bool decoderFFmpeg::pushAVPacket(FFmpeg::AVPacketWrapper &pkt) meaning += QString(" %1").arg(b, 2, 16); } meaning += ")"; - qDebug() << meaning; + DEBUG_FFMPEG(meaning); } -#endif this->setError(QStringLiteral("Error sending packet (avcodec_send_packet)")); return false; } diff --git a/YUViewLib/src/decoder/decoderHM.cpp b/YUViewLib/src/decoder/decoderHM.cpp index 4f9444e7f..9f7ec1946 100644 --- a/YUViewLib/src/decoder/decoderHM.cpp +++ b/YUViewLib/src/decoder/decoderHM.cpp @@ -31,6 +31,7 @@ */ #include "decoderHM.h" +#include #include #include @@ -43,29 +44,7 @@ namespace decoder { -// Debug the decoder ( 0:off 1:interactive decoder only 2:caching decoder only 3:both) -#define DECODERHM_DEBUG_OUTPUT 0 -#if DECODERHM_DEBUG_OUTPUT && !NDEBUG -#include -#if DECODERHM_DEBUG_OUTPUT == 1 -#define DEBUG_DECHM \ - if (!isCachingDecoder) \ - qDebug -#elif DECODERHM_DEBUG_OUTPUT == 2 -#define DEBUG_DECHM \ - if (isCachingDecoder) \ - qDebug -#elif DECODERHM_DEBUG_OUTPUT == 3 -#define DEBUG_DECHM \ - if (isCachingDecoder) \ - qDebug("c:"); \ - else \ - qDebug("i:"); \ - qDebug -#endif -#else -#define DEBUG_DECHM(fmt, ...) ((void)0) -#endif +#define DEBUG_DECHM(...) qCDebug(logDecoder, __VA_ARGS__) // Restrict is basically a promise to the compiler that for the scope of the pointer, the target of // the pointer will only be accessed through that pointer (and pointers copied from it). diff --git a/YUViewLib/src/decoder/decoderLibde265.cpp b/YUViewLib/src/decoder/decoderLibde265.cpp index e87a82ef1..0dd84132f 100644 --- a/YUViewLib/src/decoder/decoderLibde265.cpp +++ b/YUViewLib/src/decoder/decoderLibde265.cpp @@ -31,6 +31,7 @@ */ #include "decoderLibde265.h" +#include #include #include @@ -43,29 +44,7 @@ namespace decoder { -// Debug the decoder ( 0:off 1:interactive decoder only 2:caching decoder only 3:both) -#define DECODERLIBD265_DEBUG_OUTPUT 0 -#if DECODERLIBD265_DEBUG_OUTPUT && !NDEBUG -#include -#if DECODERLIBD265_DEBUG_OUTPUT == 1 -#define DEBUG_LIBDE265 \ - if (!isCachingDecoder) \ - qDebug -#elif DECODERLIBD265_DEBUG_OUTPUT == 2 -#define DEBUG_LIBDE265 \ - if (isCachingDecoder) \ - qDebug -#elif DECODERLIBD265_DEBUG_OUTPUT == 3 -#define DEBUG_LIBDE265 \ - if (isCachingDecoder) \ - qDebug("c:"); \ - else \ - qDebug("i:"); \ - qDebug -#endif -#else -#define DEBUG_LIBDE265(fmt, ...) ((void)0) -#endif +#define DEBUG_LIBDE265(...) qCDebug(logDecoder, __VA_ARGS__) using Subsampling = video::yuv::Subsampling; diff --git a/YUViewLib/src/decoder/decoderTarga.cpp b/YUViewLib/src/decoder/decoderTarga.cpp index 2560fbae7..ac37fd1a5 100644 --- a/YUViewLib/src/decoder/decoderTarga.cpp +++ b/YUViewLib/src/decoder/decoderTarga.cpp @@ -35,6 +35,8 @@ #include "decoderTarga.h" +#include + #include #include @@ -604,7 +606,7 @@ void postProcessImage(dec::Targa::Image &image, const Header &header) } // namespace -std::optional dec::Targa::loadTgaFromFile(std::string filename) +std::optional dec::Targa::loadTgaFromFile(const std::filesystem::path &filename) { std::ifstream tgaFile(filename, std::ios::binary); @@ -620,7 +622,7 @@ std::optional dec::Targa::loadTgaFromFile(std::string filenam } catch (const std::exception &e) { - std::cerr << e.what() << '\n'; + qWarning() << e.what(); } return {}; diff --git a/YUViewLib/src/decoder/decoderTarga.h b/YUViewLib/src/decoder/decoderTarga.h index 2a19b808f..c52721246 100644 --- a/YUViewLib/src/decoder/decoderTarga.h +++ b/YUViewLib/src/decoder/decoderTarga.h @@ -34,6 +34,7 @@ #include +#include #include namespace dec::Targa @@ -46,6 +47,6 @@ struct Image Size size{}; }; -std::optional loadTgaFromFile(std::string filename); +std::optional loadTgaFromFile(const std::filesystem::path &filename); } // namespace dec::Targa diff --git a/YUViewLib/src/decoder/decoderVTM.cpp b/YUViewLib/src/decoder/decoderVTM.cpp index 8d4eb4285..fa2c3d65b 100644 --- a/YUViewLib/src/decoder/decoderVTM.cpp +++ b/YUViewLib/src/decoder/decoderVTM.cpp @@ -31,6 +31,7 @@ */ #include "decoderVTM.h" +#include #include #include @@ -43,29 +44,7 @@ namespace decoder { -// Debug the decoder ( 0:off 1:interactive decoder only 2:caching decoder only 3:both) -#define DECODERVTM_DEBUG_OUTPUT 0 -#if DECODERVTM_DEBUG_OUTPUT && !NDEBUG -#include -#if DECODERVTM_DEBUG_OUTPUT == 1 -#define DEBUG_DECVTM \ - if (!isCachingDecoder) \ - qDebug -#elif DECODERVTM_DEBUG_OUTPUT == 2 -#define DEBUG_DECVTM \ - if (isCachingDecoder) \ - qDebug -#elif DECODERVTM_DEBUG_OUTPUT == 3 -#define DEBUG_DECVTM \ - if (isCachingDecoder) \ - qDebug("c:"); \ - else \ - qDebug("i:"); \ - qDebug -#endif -#else -#define DEBUG_DECVTM(fmt, ...) ((void)0) -#endif +#define DEBUG_DECVTM(...) qCDebug(logDecoder, __VA_ARGS__) // Restrict is basically a promise to the compiler that for the scope of the pointer, the target of // the pointer will only be accessed through that pointer (and pointers copied from it). diff --git a/YUViewLib/src/decoder/decoderVVDec.cpp b/YUViewLib/src/decoder/decoderVVDec.cpp index 78b0fd8dd..117b4f8fc 100644 --- a/YUViewLib/src/decoder/decoderVVDec.cpp +++ b/YUViewLib/src/decoder/decoderVVDec.cpp @@ -31,6 +31,7 @@ */ #include "decoderVVDec.h" +#include #include @@ -39,29 +40,7 @@ #include #include -// Debug the decoder ( 0:off 1:interactive decoder only 2:caching decoder only 3:both) -#define decoderVVDec_DEBUG_OUTPUT 0 -#if decoderVVDec_DEBUG_OUTPUT && !NDEBUG -#include -#if decoderVVDec_DEBUG_OUTPUT == 1 -#define DEBUG_vvdec \ - if (!isCachingDecoder) \ - qDebug -#elif decoderVVDec_DEBUG_OUTPUT == 2 -#define DEBUG_vvdec \ - if (isCachingDecoder) \ - qDebug -#elif decoderVVDec_DEBUG_OUTPUT == 3 -#define DEBUG_vvdec \ - if (isCachingDecoder) \ - qDebug("c:"); \ - else \ - qDebug("i:"); \ - qDebug -#endif -#else -#define DEBUG_vvdec(fmt, ...) ((void)0) -#endif +#define DEBUG_vvdec(...) qCDebug(logDecoder, __VA_ARGS__) // Restrict is basically a promise to the compiler that for the scope of the pointer, the target of // the pointer will only be accessed through that pointer (and pointers copied from it). diff --git a/YUViewLib/src/ffmpeg/AVCodecContextWrapper.cpp b/YUViewLib/src/ffmpeg/AVCodecContextWrapper.cpp index 890296b5c..51bd666ad 100644 --- a/YUViewLib/src/ffmpeg/AVCodecContextWrapper.cpp +++ b/YUViewLib/src/ffmpeg/AVCodecContextWrapper.cpp @@ -444,6 +444,39 @@ typedef struct AVCodecContext_61 enum AVChromaLocation chroma_sample_location; } AVCodecContext_61; +// avcodec 62 (FFmpeg 8.x): ticks_per_frame (int, 4 bytes) was removed. +typedef struct AVCodecContext_62 +{ + const AVClass *av_class; + int log_level_offset; + enum AVMediaType codec_type; + const struct AVCodec *codec; + enum AVCodecID codec_id; + unsigned int codec_tag; + void *priv_data; + struct AVCodecInternal *internal; + void *opaque; + int64_t bit_rate; + int flags; + int flags2; + uint8_t *extradata; + int extradata_size; + AVRational time_base; + AVRational pkt_timebase; + AVRational framerate; + int delay; + int width, height; + int coded_width, coded_height; + AVRational sample_aspect_ratio; + enum AVPixelFormat pix_fmt; + enum AVPixelFormat sw_pix_fmt; + enum AVColorPrimaries color_primaries; + enum AVColorTransferCharacteristic color_trc; + enum AVColorSpace colorspace; + enum AVColorRange color_range; + enum AVChromaLocation chroma_sample_location; +} AVCodecContext_62; + } // namespace AVCodecContextWrapper::AVCodecContextWrapper() @@ -932,6 +965,91 @@ void AVCodecContextWrapper::update() this->color_range = p->color_range; this->chroma_sample_location = p->chroma_sample_location; } + else if (libVer.avcodec.major == 62) + { + // avcodec 62 (FFmpeg 8.x): ticks_per_frame removed; all other fields identical to 61. + auto p = reinterpret_cast(this->codec); + this->codec_type = p->codec_type; + this->codec_name = QString("Not supported in AVCodec >= 58"); + this->codec_id = p->codec_id; + this->codec_tag = p->codec_tag; + this->stream_codec_tag = -1; + this->bit_rate = p->bit_rate; + this->bit_rate_tolerance = -1; + this->global_quality = -1; + this->compression_level = -1; + this->flags = p->flags; + this->flags2 = p->flags2; + this->extradata = QByteArray((const char *)p->extradata, p->extradata_size); + this->time_base = p->time_base; + this->ticks_per_frame = -1; + this->delay = p->delay; + this->width = p->width; + this->height = p->height; + this->coded_width = p->coded_width; + this->coded_height = p->coded_height; + this->gop_size = -1; + this->pix_fmt = p->pix_fmt; + this->me_method = -1; + this->max_b_frames = -1; + this->b_quant_factor = -1; + this->rc_strategy = -1; + this->b_frame_strategy = -1; + this->b_quant_offset = -1; + this->has_b_frames = -1; + this->mpeg_quant = -1; + this->i_quant_factor = -1; + this->i_quant_offset = -1; + this->lumi_masking = -1; + this->temporal_cplx_masking = -1; + this->spatial_cplx_masking = -1; + this->p_masking = -1; + this->dark_masking = -1; + this->slice_count = -1; + this->prediction_method = -1; + this->sample_aspect_ratio = p->sample_aspect_ratio; + this->me_cmp = -1; + this->me_sub_cmp = -1; + this->mb_cmp = -1; + this->ildct_cmp = -1; + this->dia_size = -1; + this->last_predictor_count = -1; + this->pre_me = -1; + this->me_pre_cmp = -1; + this->pre_dia_size = -1; + this->me_subpel_quality = -1; + this->dtg_active_format = -1; + this->me_range = -1; + this->intra_quant_bias = -1; + this->inter_quant_bias = -1; + this->slice_flags = -1; + this->xvmc_acceleration = -1; + this->mb_decision = -1; + this->scenechange_threshold = -1; + this->noise_reduction = -1; + this->me_threshold = -1; + this->mb_threshold = -1; + this->intra_dc_precision = -1; + this->skip_top = -1; + this->skip_bottom = -1; + this->border_masking = -1; + this->mb_lmin = -1; + this->mb_lmax = -1; + this->me_penalty_compensation = -1; + this->bidir_refine = -1; + this->brd_scale = -1; + this->keyint_min = -1; + this->refs = -1; + this->chromaoffset = -1; + this->scenechange_factor = -1; + this->mv0_threshold = -1; + this->b_sensitivity = -1; + this->color_primaries = p->color_primaries; + this->color_trc = p->color_trc; + this->colorspace = p->colorspace; + this->color_range = p->color_range; + this->chroma_sample_location = p->chroma_sample_location; + } else throw std::runtime_error("Invalid library version"); } diff --git a/YUViewLib/src/ffmpeg/AVCodecParametersWrapper.cpp b/YUViewLib/src/ffmpeg/AVCodecParametersWrapper.cpp index e8e040bb0..c083bd25f 100644 --- a/YUViewLib/src/ffmpeg/AVCodecParametersWrapper.cpp +++ b/YUViewLib/src/ffmpeg/AVCodecParametersWrapper.cpp @@ -159,100 +159,125 @@ QStringPairList AVCodecParametersWrapper::getInfoText() } this->update(); + // Fields common to all stream types info.append({"Codec Tag", QString::number(this->codec_tag)}); - info.append({"Format", QString::number(this->format)}); info.append({"Bitrate", QString::number(this->bit_rate)}); info.append({"Bits per coded sample", QString::number(this->bits_per_coded_sample)}); info.append({"Bits per Raw sample", QString::number(this->bits_per_raw_sample)}); info.append({"Profile", QString::number(this->profile)}); info.append({"Level", QString::number(this->level)}); - info.append({"Width/Height", QString("%1/%2").arg(this->width).arg(this->height)}); - info.append( - {"Sample aspect ratio", - QString("%1:%2").arg(this->sample_aspect_ratio.num).arg(this->sample_aspect_ratio.den)}); - auto fieldOrders = QStringList() << "Unknown" - << "Progressive" - << "Top coded_first, top displayed first" - << "Bottom coded first, bottom displayed first" - << "Top coded first, bottom displayed first" - << "Bottom coded first, top displayed first"; - info.append( - {"Field Order", - fieldOrders.at(functions::clip(int(this->codec_type), 0, int(fieldOrders.count())))}); - auto colorRanges = QStringList() << "Unspecified" - << "The normal 219*2^(n-8) MPEG YUV ranges" - << "The normal 2^n-1 JPEG YUV ranges" - << "Not part of ABI"; - info.append( - {"Color Range", - colorRanges.at(functions::clip(int(this->color_range), 0, int(colorRanges.count())))}); - auto colorPrimaries = - QStringList() - << "Reserved" - << "BT709 / ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B" - << "Unspecified" - << "Reserved" - << "BT470M / FCC Title 47 Code of Federal Regulations 73.682 (a)(20)" - << "BT470BG / ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM" - << "SMPTE170M / also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC" - << "SMPTE240M" - << "FILM - colour filters using Illuminant C" - << "ITU-R BT2020" - << "SMPTE ST 428-1 (CIE 1931 XYZ)" - << "SMPTE ST 431-2 (2011)" - << "SMPTE ST 432-1 D65 (2010)" - << "Not part of ABI"; - info.append(QStringPair("Color Primaries", colorPrimaries.at((int)this->color_primaries))); - auto colorTransfers = - QStringList() - << "Reserved" - << "BT709 / ITU-R BT1361" - << "Unspecified" - << "Reserved" - << "Gamma22 / ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM" - << "Gamma28 / ITU-R BT470BG" - << "SMPTE170M / ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC" - << "SMPTE240M" - << "Linear transfer characteristics" - << "Logarithmic transfer characteristic (100:1 range)" - << "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)" - << "IEC 61966-2-4" - << "ITU-R BT1361 Extended Colour Gamut" - << "IEC 61966-2-1 (sRGB or sYCC)" - << "ITU-R BT2020 for 10-bit system" - << "ITU-R BT2020 for 12-bit system" - << "SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems" - << "SMPTE ST 428-1" - << "ARIB STD-B67, known as Hybrid log-gamma" - << "Not part of ABI"; - info.append({"Color Transfer", colorTransfers.at((int)this->color_trc)}); - auto colorSpaces = QStringList() - << "RGB - order of coefficients is actually GBR, also IEC 61966-2-1 (sRGB)" - << "BT709 / ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / SMPTE RP177 Annex B" - << "Unspecified" - << "Reserved" - << "FCC Title 47 Code of Federal Regulations 73.682 (a)(20)" - << "BT470BG / ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & " - "SECAM / IEC 61966-2-4 xvYCC601" - << "SMPTE170M / ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC" - << "SMPTE240M" - << "YCOCG - Used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16" - << "ITU-R BT2020 non-constant luminance system" - << "ITU-R BT2020 constant luminance system" - << "SMPTE 2085, Y'D'zD'x" - << "Not part of ABI"; - info.append({"Color Space", colorSpaces.at((int)this->color_space)}); - auto chromaLocations = QStringList() - << "Unspecified" - << "Left / MPEG-2/4 4:2:0, H.264 default for 4:2:0" - << "Center / MPEG-1 4:2:0, JPEG 4:2:0, H.263 4:2:0" - << "Top Left / ITU-R 601, SMPTE 274M 296M S314M(DV 4:1:1), mpeg2 4:2:2" - << "Top" - << "Bottom Left" - << "Bottom" - << "Not part of ABI"; - info.append({"Chroma Location", chromaLocations.at((int)this->chroma_location)}); - info.append({"Video Delay", QString::number(this->video_delay)}); + + if (this->codec_type == AVMEDIA_TYPE_VIDEO) + { + info.append({"Format", QString::number(this->format)}); + info.append({"Width/Height", QString("%1/%2").arg(this->width).arg(this->height)}); + info.append( + {"Sample aspect ratio", + QString("%1:%2").arg(this->sample_aspect_ratio.num).arg(this->sample_aspect_ratio.den)}); + + auto fieldOrders = QStringList() << "Unknown" + << "Progressive" + << "Top coded first, top displayed first" + << "Bottom coded first, bottom displayed first" + << "Top coded first, bottom displayed first" + << "Bottom coded first, top displayed first"; + info.append( + {"Field Order", + fieldOrders.at(functions::clip(int(this->field_order), 0, int(fieldOrders.count())))}); + + auto colorRanges = QStringList() << "Unspecified" + << "The normal 219*2^(n-8) MPEG YUV ranges" + << "The normal 2^n-1 JPEG YUV ranges" + << "Not part of ABI"; + info.append( + {"Color Range", + colorRanges.at(functions::clip(int(this->color_range), 0, int(colorRanges.count())))}); + + auto colorPrimaries = + QStringList() + << "Reserved" + << "BT709 / ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B" + << "Unspecified" + << "Reserved" + << "BT470M / FCC Title 47 Code of Federal Regulations 73.682 (a)(20)" + << "BT470BG / ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM" + << "SMPTE170M / also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC" + << "SMPTE240M" + << "FILM - colour filters using Illuminant C" + << "ITU-R BT2020" + << "SMPTE ST 428-1 (CIE 1931 XYZ)" + << "SMPTE ST 431-2 (2011)" + << "SMPTE ST 432-1 D65 (2010)" + << "Not part of ABI"; + info.append({"Color Primaries", + colorPrimaries.at( + functions::clip(int(this->color_primaries), 0, int(colorPrimaries.count())))}); + + auto colorTransfers = + QStringList() + << "Reserved" + << "BT709 / ITU-R BT1361" + << "Unspecified" + << "Reserved" + << "Gamma22 / ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM" + << "Gamma28 / ITU-R BT470BG" + << "SMPTE170M / ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC" + << "SMPTE240M" + << "Linear transfer characteristics" + << "Logarithmic transfer characteristic (100:1 range)" + << "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)" + << "IEC 61966-2-4" + << "ITU-R BT1361 Extended Colour Gamut" + << "IEC 61966-2-1 (sRGB or sYCC)" + << "ITU-R BT2020 for 10-bit system" + << "ITU-R BT2020 for 12-bit system" + << "SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems" + << "SMPTE ST 428-1" + << "ARIB STD-B67, known as Hybrid log-gamma" + << "Not part of ABI"; + info.append({"Color Transfer", + colorTransfers.at( + functions::clip(int(this->color_trc), 0, int(colorTransfers.count())))}); + + auto colorSpaces = + QStringList() + << "RGB - order of coefficients is actually GBR, also IEC 61966-2-1 (sRGB)" + << "BT709 / ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / SMPTE RP177 Annex B" + << "Unspecified" + << "Reserved" + << "FCC Title 47 Code of Federal Regulations 73.682 (a)(20)" + << "BT470BG / ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC " + "61966-2-4 xvYCC601" + << "SMPTE170M / ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC" + << "SMPTE240M" + << "YCOCG - Used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16" + << "ITU-R BT2020 non-constant luminance system" + << "ITU-R BT2020 constant luminance system" + << "SMPTE 2085, Y'D'zD'x" + << "Not part of ABI"; + info.append( + {"Color Space", + colorSpaces.at(functions::clip(int(this->color_space), 0, int(colorSpaces.count())))}); + + auto chromaLocations = + QStringList() << "Unspecified" + << "Left / MPEG-2/4 4:2:0, H.264 default for 4:2:0" + << "Center / MPEG-1 4:2:0, JPEG 4:2:0, H.263 4:2:0" + << "Top Left / ITU-R 601, SMPTE 274M 296M S314M(DV 4:1:1), mpeg2 4:2:2" + << "Top" + << "Bottom Left" + << "Bottom" + << "Not part of ABI"; + info.append({"Chroma Location", + chromaLocations.at( + functions::clip(int(this->chroma_location), 0, int(chromaLocations.count())))}); + + info.append({"Video Delay", QString::number(this->video_delay)}); + } + else if (this->codec_type == AVMEDIA_TYPE_AUDIO) + { + info.append({"Format", QString::number(this->format)}); + } return info; } @@ -290,7 +315,7 @@ void AVCodecParametersWrapper::setClearValues() p->chroma_location = AVCHROMA_LOC_UNSPECIFIED; p->video_delay = 0; } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->param); p->codec_type = AVMEDIA_TYPE_UNKNOWN; @@ -327,7 +352,7 @@ void AVCodecParametersWrapper::setAVMediaType(AVMediaType type) { if (this->libVer.avformat.major == 57 || this->libVer.avformat.major == 58 || this->libVer.avformat.major == 59 || this->libVer.avformat.major == 60 || - this->libVer.avformat.major == 61) + this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->param); p->codec_type = type; @@ -344,7 +369,7 @@ void AVCodecParametersWrapper::setAVCodecID(AVCodecID id) p->codec_id = id; this->codec_id = id; } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->param); p->codec_id = id; @@ -356,14 +381,14 @@ void AVCodecParametersWrapper::setExtradata(QByteArray data) { if (this->libVer.avformat.major == 57 || this->libVer.avformat.major == 58 || this->libVer.avformat.major == 59 || this->libVer.avformat.major == 60 || - this->libVer.avformat.major == 61) + this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { this->extradata = data; auto p = reinterpret_cast(this->param); p->extradata = reinterpret_cast(this->extradata.data()); p->extradata_size = this->extradata.length(); } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { this->extradata = data; auto p = reinterpret_cast(this->param); @@ -383,7 +408,7 @@ void AVCodecParametersWrapper::setSize(Size size) this->width = size.width; this->height = size.height; } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->param); p->width = size.width; @@ -402,7 +427,7 @@ void AVCodecParametersWrapper::setAVPixelFormat(AVPixelFormat format) p->format = format; this->format = format; } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->param); p->format = format; @@ -421,7 +446,7 @@ void AVCodecParametersWrapper::setProfileLevel(int profile, int level) this->profile = profile; this->level = level; } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->param); p->profile = profile; @@ -443,7 +468,7 @@ void AVCodecParametersWrapper::setSampleAspectRatio(int num, int den) p->sample_aspect_ratio = ratio; this->sample_aspect_ratio = ratio; } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->param); AVRational ratio; @@ -490,7 +515,7 @@ void AVCodecParametersWrapper::update() this->chroma_location = p->chroma_location; this->video_delay = p->video_delay; } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->param); 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/AVFormatContextWrapper.cpp b/YUViewLib/src/ffmpeg/AVFormatContextWrapper.cpp index 99e4338d5..261ed05c2 100644 --- a/YUViewLib/src/ffmpeg/AVFormatContextWrapper.cpp +++ b/YUViewLib/src/ffmpeg/AVFormatContextWrapper.cpp @@ -400,7 +400,7 @@ void AVFormatContextWrapper::update() this->iformat = AVInputFormatWrapper(p->iformat, this->libVer); } - else if (this->libVer.avformat.major == 61) + else if (this->libVer.avformat.major == 61 || this->libVer.avformat.major == 62) { auto p = reinterpret_cast(this->ctx); this->ctx_flags = p->ctx_flags; 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; } diff --git a/YUViewLib/src/ffmpeg/AVPacketWrapper.cpp b/YUViewLib/src/ffmpeg/AVPacketWrapper.cpp index af8929700..b74f4de19 100644 --- a/YUViewLib/src/ffmpeg/AVPacketWrapper.cpp +++ b/YUViewLib/src/ffmpeg/AVPacketWrapper.cpp @@ -150,7 +150,8 @@ AVPacketWrapper::AVPacketWrapper(LibraryVersion libVersion, AVPacket *packet) } else if (this->libVer.avcodec.major == 59 || // this->libVer.avcodec.major == 60 || - this->libVer.avcodec.major == 61) + this->libVer.avcodec.major == 61 || + this->libVer.avcodec.major == 62) { auto p = reinterpret_cast(packet); p->data = nullptr; @@ -187,7 +188,8 @@ void AVPacketWrapper::setData(QByteArray &set_data) } else if (this->libVer.avcodec.major == 59 || // this->libVer.avcodec.major == 60 || - this->libVer.avcodec.major == 61) + this->libVer.avcodec.major == 61 || + this->libVer.avcodec.major == 62) { auto p = reinterpret_cast(this->pkt); p->data = (uint8_t *)set_data.data(); @@ -215,7 +217,8 @@ void AVPacketWrapper::setPTS(int64_t pts) } else if (this->libVer.avcodec.major == 59 || // this->libVer.avcodec.major == 60 || - this->libVer.avcodec.major == 61) + this->libVer.avcodec.major == 61 || + this->libVer.avcodec.major == 62) { auto p = reinterpret_cast(this->pkt); p->pts = pts; @@ -241,7 +244,8 @@ void AVPacketWrapper::setDTS(int64_t dts) } else if (this->libVer.avcodec.major == 59 || // this->libVer.avcodec.major == 60 || - this->libVer.avcodec.major == 61) + this->libVer.avcodec.major == 61 || + this->libVer.avcodec.major == 62) { auto p = reinterpret_cast(this->pkt); p->dts = dts; @@ -397,7 +401,8 @@ void AVPacketWrapper::update() } else if (this->libVer.avcodec.major == 59 || // this->libVer.avcodec.major == 60 || - this->libVer.avcodec.major == 61) + this->libVer.avcodec.major == 61 || + this->libVer.avcodec.major == 62) { auto p = reinterpret_cast(this->pkt); diff --git a/YUViewLib/src/ffmpeg/AVStreamWrapper.cpp b/YUViewLib/src/ffmpeg/AVStreamWrapper.cpp index 33f92ab4a..982a60db9 100644 --- a/YUViewLib/src/ffmpeg/AVStreamWrapper.cpp +++ b/YUViewLib/src/ffmpeg/AVStreamWrapper.cpp @@ -229,6 +229,29 @@ typedef struct AVStream_60_61 int pts_wrap_bits; } AVStream_60_61; +// avformat 62 (FFmpeg 8.x): side_data and nb_side_data were removed from AVStream. +typedef struct AVStream_62 +{ + const AVClass *av_class; + int index; + int id; + AVCodecParameters *codecpar; + void *priv_data; + AVRational time_base; + int64_t start_time; + int64_t duration; + int64_t nb_frames; + int disposition; + enum AVDiscard discard; + AVRational sample_aspect_ratio; + AVDictionary *metadata; + AVRational avg_frame_rate; + AVPacket_59_60_61 attached_pic; + int event_flags; + AVRational r_frame_rate; + int pts_wrap_bits; +} AVStream_62; + } // namespace AVStreamWrapper::AVStreamWrapper(AVStream *src_str, LibraryVersion v) @@ -432,6 +455,24 @@ void AVStreamWrapper::update() this->event_flags = p->event_flags; this->codecpar = AVCodecParametersWrapper(p->codecpar, libVer); } + else if (libVer.avformat.major == 62) + { + // avformat 62 (FFmpeg 8.x): side_data / nb_side_data removed from AVStream. + auto p = reinterpret_cast(this->stream); + this->index = p->index; + this->id = p->id; + this->time_base = p->time_base; + this->start_time = p->start_time; + this->duration = p->duration; + this->nb_frames = p->nb_frames; + this->disposition = p->disposition; + this->discard = p->discard; + this->sample_aspect_ratio = p->sample_aspect_ratio; + this->avg_frame_rate = p->avg_frame_rate; + this->nb_side_data = 0; + this->event_flags = p->event_flags; + this->codecpar = AVCodecParametersWrapper(p->codecpar, libVer); + } else throw std::runtime_error("Invalid library version"); } @@ -480,7 +521,7 @@ QStringPairList AVStreamWrapper::getInfoText(AVCodecIDWrapper &codecIdWrapper) if (this->disposition & 0x0040) dispText += QString("Forced "); if (this->disposition & 0x0080) - dispText += QString("Hearing_Imparied "); + dispText += QString("Hearing_Impaired "); if (this->disposition & 0x0100) dispText += QString("Visual_Impaired "); if (this->disposition & 0x0200) @@ -500,18 +541,23 @@ QStringPairList AVStreamWrapper::getInfoText(AVCodecIDWrapper &codecIdWrapper) info.append(QStringPair("Disposition", dispText)); } - info.append(QStringPair( - "Sample Aspect Ratio", - QString("%1:%2").arg(this->sample_aspect_ratio.num).arg(this->sample_aspect_ratio.den))); - - auto divFrameRate = 0.0; - if (this->avg_frame_rate.den > 0) - divFrameRate = double(this->avg_frame_rate.num) / double(this->avg_frame_rate.den); - info.append(QStringPair("Average Frame Rate", - QString("%1/%2 (%3)") - .arg(this->avg_frame_rate.num) - .arg(this->avg_frame_rate.den) - .arg(divFrameRate, 0, 'f', 2))); + const auto mediaType = getCodecType(); + + if (mediaType == AVMEDIA_TYPE_VIDEO) + { + info.append(QStringPair( + "Sample Aspect Ratio", + QString("%1:%2").arg(this->sample_aspect_ratio.num).arg(this->sample_aspect_ratio.den))); + + auto divFrameRate = 0.0; + if (this->avg_frame_rate.den > 0) + divFrameRate = double(this->avg_frame_rate.num) / double(this->avg_frame_rate.den); + info.append(QStringPair("Average Frame Rate", + QString("%1/%2 (%3)") + .arg(this->avg_frame_rate.num) + .arg(this->avg_frame_rate.den) + .arg(divFrameRate, 0, 'f', 2))); + } info += this->codecpar.getInfoText(); return info; diff --git a/YUViewLib/src/ffmpeg/FFmpegLibraryFunctions.cpp b/YUViewLib/src/ffmpeg/FFmpegLibraryFunctions.cpp index d4ffa014c..51cbc0145 100644 --- a/YUViewLib/src/ffmpeg/FFmpegLibraryFunctions.cpp +++ b/YUViewLib/src/ffmpeg/FFmpegLibraryFunctions.cpp @@ -96,8 +96,9 @@ bool bindLibraryFunctions(QLibrary & lib, return false; if (!resolveFunction(lib, functions.avcodec_free_context, "avcodec_free_context", log)) return false; - if (!resolveFunction(lib, functions.av_init_packet, "av_init_packet", log)) - return false; + // av_init_packet is deprecated since FFmpeg 5.x and removed in FFmpeg 9.x (avcodec >= 63). + // Treat it as optional so that FFmpeg 8.x libraries still load correctly. + resolveFunction(lib, functions.av_init_packet, "av_init_packet", log); if (!resolveFunction(lib, functions.av_packet_alloc, "av_packet_alloc", log)) return false; if (!resolveFunction(lib, functions.av_packet_free, "av_packet_free", log)) diff --git a/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp b/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp index 714cf8bfe..f6ac16253 100644 --- a/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp +++ b/YUViewLib/src/ffmpeg/FFmpegVersionHandler.cpp @@ -34,6 +34,9 @@ #include #include +#include +#include + namespace FFmpeg { @@ -102,6 +105,7 @@ LibraryVersion addMinorAndMicroVersion(FFmpegLibraryFunctions &lib, LibraryVersi // the following libraries in this order: Util, codec, format, swresample // The versions are sorted from newest to oldest, so that we try to open the newest ones first. auto SupportedLibraryVersionCombinations = { + LibraryVersion(60, 62, 62, 6), LibraryVersion(59, 61, 61, 5), LibraryVersion(58, 60, 60, 4), LibraryVersion(57, 59, 59, 4), @@ -307,6 +311,7 @@ void FFmpegVersionHandler::flush_buffers(AVCodecContextWrapper &decCtx) } QStringList FFmpegVersionHandler::logListFFmpeg; +QMutex FFmpegVersionHandler::logListMutex; FFmpegVersionHandler::FFmpegVersionHandler() { @@ -319,13 +324,58 @@ FFmpegVersionHandler::~FFmpegVersionHandler() this->lib.setLogList(nullptr); } -void FFmpegVersionHandler::avLogCallback(void *, int level, const char *fmt, va_list vargs) +// Format an FFmpeg log message (printf-style + va_list) into a QString. +// QString::vasprintf is unreliable on Windows/MSVC when the va_list originates +// from FFmpeg's callback (it returns empty strings). Use vsnprintf instead: +// first call with nullptr/0 to get the length, then allocate and format. +static QString formatFFmpegMessage(const char *fmt, va_list vargs) +{ + va_list argsCopy; + va_copy(argsCopy, vargs); + int len = vsnprintf(nullptr, 0, fmt, argsCopy); + va_end(argsCopy); + + if (len < 0) + return {}; + + QByteArray ba(len + 1, '\0'); + va_copy(argsCopy, vargs); + vsnprintf(ba.data(), ba.size(), fmt, argsCopy); + va_end(argsCopy); + + return QString::fromUtf8(ba.constData(), len); +} + +void FFmpegVersionHandler::avLogCallback(void *ptr, int level, const char *fmt, va_list vargs) { - QString msg; - msg.vasprintf(fmt, vargs); - auto now = QDateTime::currentDateTime(); - FFmpegVersionHandler::logListFFmpeg.append(now.toString("hh:mm:ss.zzz") + - QString(" - L%1 - ").arg(level) + msg); + Q_UNUSED(ptr) + const QString msg = formatFFmpegMessage(fmt, vargs); + + // 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. + // 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); + // Map FFmpeg severity to the closest Qt message type. + // FFmpeg levels: FATAL=8 ERROR=16 WARNING=24 INFO=32 VERBOSE=40 DEBUG=48 TRACE=56 + if (level <= 16) // ERROR / FATAL + qCritical().noquote() << tagged; + else if (level <= 24) // WARNING + qWarning().noquote() << tagged; + else if (level <= 32) // INFO + qInfo().noquote() << tagged; + else // VERBOSE / DEBUG / TRACE + qDebug().noquote() << tagged; } void FFmpegVersionHandler::loadFFmpegLibraries() @@ -384,7 +434,13 @@ void FFmpegVersionHandler::loadFFmpegLibraries() } if (this->librariesLoaded) + { this->lib.avutil.av_log_set_callback(&FFmpegVersionHandler::avLogCallback); + // Let FFmpeg emit all messages (up to TRACE); the Logger / LogPanel level + // filter will decide what reaches the user. This keeps FFmpeg's verbosity + // under the same runtime control as the rest of the application. + this->lib.avutil.av_log_set_level(56); // AV_LOG_TRACE + } } bool FFmpegVersionHandler::loadingSuccessfull() const @@ -641,7 +697,9 @@ void FFmpegVersionHandler::freeFrame(AVFrameWrapper &frame) AVPacketWrapper FFmpegVersionHandler::allocatePacket() { auto rawPacket = this->lib.avcodec.av_packet_alloc(); - this->lib.avcodec.av_init_packet(rawPacket); + // av_init_packet is optional: it was deprecated in FFmpeg 5.x and removed in FFmpeg 9.x. + if (this->lib.avcodec.av_init_packet) + this->lib.avcodec.av_init_packet(rawPacket); return AVPacketWrapper(this->libVersion, rawPacket); } 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); }; diff --git a/YUViewLib/src/filesource/FileSource.cpp b/YUViewLib/src/filesource/FileSource.cpp index e5df613a9..3f165a64c 100644 --- a/YUViewLib/src/filesource/FileSource.cpp +++ b/YUViewLib/src/filesource/FileSource.cpp @@ -64,7 +64,7 @@ bool FileSource::openFile(const std::filesystem::path &filePath) if (this->isFileOpened && this->srcFile.isOpen()) this->srcFile.close(); - this->srcFile.setFileName(QString::fromStdString(filePath.string())); + this->srcFile.setFileName(QString::fromStdWString(filePath.wstring())); this->isFileOpened = this->srcFile.open(QIODevice::ReadOnly); if (!this->isFileOpened) return false; @@ -83,6 +83,9 @@ int64_t FileSource::readBytes(QByteArray &targetBuffer, int64_t startPos, int64_ if (!this->isOk()) return 0; + if (nrBytes <= 0) + return 0; + if (targetBuffer.size() < nrBytes) targetBuffer.resize(nrBytes); @@ -103,7 +106,7 @@ std::vector FileSource::getFileInfoList() const // For now we still use the QFileInfo. There is no easy cross platform formatting // for the std::filesystem::file_time_type. This is added in C++ 20. - QFileInfo fileInfo(QString::fromStdString(this->fullFilePath.string())); + QFileInfo fileInfo(QString::fromStdWString(this->fullFilePath.wstring())); std::vector infoList; diff --git a/YUViewLib/src/filesource/FileSourceAnnexBFile.cpp b/YUViewLib/src/filesource/FileSourceAnnexBFile.cpp index 2d9dc6337..c945e648c 100644 --- a/YUViewLib/src/filesource/FileSourceAnnexBFile.cpp +++ b/YUViewLib/src/filesource/FileSourceAnnexBFile.cpp @@ -31,14 +31,9 @@ */ #include "FileSourceAnnexBFile.h" +#include -#define ANNEXBFILE_DEBUG_OUTPUT 0 -#if ANNEXBFILE_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_ANNEXBFILE(f) qDebug() << f -#else -#define DEBUG_ANNEXBFILE(f) ((void)0) -#endif +#define DEBUG_ANNEXBFILE(msg) LOG_DEBUG(logFileSource) << msg const auto BUFFERSIZE = 500000; const auto STARTCODE = QByteArrayLiteral("\x00\x00\x01"); @@ -57,7 +52,7 @@ FileSourceAnnexBFile::FileSourceAnnexBFile(const std::filesystem::path &filePath // Open the file and fill the read buffer. bool FileSourceAnnexBFile::openFile(const std::filesystem::path &fileName) { - DEBUG_ANNEXBFILE("FileSourceAnnexBFile::openFile fileName " << fileName); + DEBUG_ANNEXBFILE("FileSourceAnnexBFile::openFile fileName " << QString::fromStdString(fileName.string())); // Open the input file (again) FileSource::openFile(fileName); diff --git a/YUViewLib/src/filesource/FileSourceFFmpegFile.cpp b/YUViewLib/src/filesource/FileSourceFFmpegFile.cpp index 41afa62ec..3bd2f8f3d 100644 --- a/YUViewLib/src/filesource/FileSourceFFmpegFile.cpp +++ b/YUViewLib/src/filesource/FileSourceFFmpegFile.cpp @@ -31,6 +31,7 @@ */ #include "FileSourceFFmpegFile.h" +#include #include #include @@ -41,13 +42,7 @@ #include #include -#define FILESOURCEFFMPEGFILE_DEBUG_OUTPUT 0 -#if FILESOURCEFFMPEGFILE_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_FFMPEG qDebug -#else -#define DEBUG_FFMPEG(fmt, ...) ((void)0) -#endif +#define DEBUG_FFMPEG(...) qCDebug(logFileSource, __VA_ARGS__) using SubByteReaderLogging = parser::reader::SubByteReaderLogging; using namespace FFmpeg; @@ -245,7 +240,7 @@ ByteVector FileSourceFFmpegFile::getLhvCData() End }; - std::ifstream inputFile(this->fileName.toStdString(), std::ios::binary); + std::ifstream inputFile(std::filesystem::path(this->fileName.toStdWString()), std::ios::binary); for (const auto searchPosition : {SearchPosition::Beginning, SearchPosition::End}) { constexpr auto NR_SEARCH_BYTES = 5120; diff --git a/YUViewLib/src/handler/ItemMemoryHandler.cpp b/YUViewLib/src/handler/ItemMemoryHandler.cpp index 009701c52..d0b7be0ed 100644 --- a/YUViewLib/src/handler/ItemMemoryHandler.cpp +++ b/YUViewLib/src/handler/ItemMemoryHandler.cpp @@ -31,17 +31,12 @@ */ #include "ItemMemoryHandler.h" +#include #include #include -#define ITEMMEMORYHANDLER_DEBUG 0 -#if ITEMMEMORYHANDLER_DEBUG && !NDEBUG -#include -#define DEBUG_MEMORY(msg) qDebug() << msg -#else -#define DEBUG_MEMORY(msg) ((void)0) -#endif +#define DEBUG_MEMORY(msg) LOG_DEBUG(logApp) << msg namespace itemMemoryHandler { diff --git a/YUViewLib/src/handler/SingleInstanceHandler.cpp b/YUViewLib/src/handler/SingleInstanceHandler.cpp index 1456df080..dd2c149f2 100644 --- a/YUViewLib/src/handler/SingleInstanceHandler.cpp +++ b/YUViewLib/src/handler/SingleInstanceHandler.cpp @@ -31,15 +31,9 @@ */ #include "SingleInstanceHandler.h" +#include -// Activate this if you want to know when which difference is loaded -#define SINGLEINSTANCEHANDLER_DEBUG 0 -#if SINGLEINSTANCEHANDLER_DEBUG && !NDEBUG -#include -#define DEBUG_SINGLEISNTANCE qDebug -#else -#define DEBUG_SINGLEISNTANCE(fmt, ...) ((void)0) -#endif +#define DEBUG_SINGLEISNTANCE(...) qCDebug(logApp, __VA_ARGS__) singleInstanceHandler::singleInstanceHandler(QObject *parent) : QObject(parent) { diff --git a/YUViewLib/src/handler/UpdateHandler.cpp b/YUViewLib/src/handler/UpdateHandler.cpp index ff0b31690..ea5bd21d4 100644 --- a/YUViewLib/src/handler/UpdateHandler.cpp +++ b/YUViewLib/src/handler/UpdateHandler.cpp @@ -31,6 +31,7 @@ */ #include "UpdateHandler.h" +#include #include "UpdateHandlerFile.h" @@ -57,13 +58,7 @@ // ONLY USE THIS FOR DEBGGING #define ALLOW_UNENCRYPTED_CONNECTIONS 0 -#define UPDATER_DEBUG_OUTPUT 0 -#if UPDATER_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_UPDATE(msg) qDebug() << msg -#else -#define DEBUG_UPDATE(msg) ((void)0) -#endif +#define DEBUG_UPDATE(msg) LOG_DEBUG(logUpdater) << msg #define UPDATEFILEHANDLER_FILE_NAME "versioninfo.txt" #if ALLOW_UNENCRYPTED_CONNECTIONS diff --git a/YUViewLib/src/handler/UpdateHandlerFile.cpp b/YUViewLib/src/handler/UpdateHandlerFile.cpp index 034e849c7..df11d91d4 100644 --- a/YUViewLib/src/handler/UpdateHandlerFile.cpp +++ b/YUViewLib/src/handler/UpdateHandlerFile.cpp @@ -31,17 +31,12 @@ */ #include "UpdateHandlerFile.h" +#include #include #include -#define UPDATER_DEBUG_FILE 0 -#if UPDATER_DEBUG_FILE && !NDEBUG -#include -#define DEBUG_UPDATE_FILE(msg) qDebug() << msg -#else -#define DEBUG_UPDATE_FILE(msg) ((void)0) -#endif +#define DEBUG_UPDATE_FILE(msg) LOG_DEBUG(logUpdater) << msg const auto UPDATEFILEHANDLER_FILE_NAME = "versioninfo.txt"; diff --git a/YUViewLib/src/logging/LogPanel.cpp b/YUViewLib/src/logging/LogPanel.cpp new file mode 100644 index 000000000..9a005eef8 --- /dev/null +++ b/YUViewLib/src/logging/LogPanel.cpp @@ -0,0 +1,298 @@ +/* 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 +#include +#include + +// --------------------------------------------------------------------------- +// Constructor +// --------------------------------------------------------------------------- + +LogPanel::LogPanel(QWidget *parent) : QDialog(parent) +{ + setWindowTitle(tr("Log Viewer")); + setWindowFlags(windowFlags() | Qt::WindowMinMaxButtonsHint); + // Delete on close so that ~LogPanel() saves settings and the QPointer in + // MainWindow is reset automatically. + setAttribute(Qt::WA_DeleteOnClose); + resize(900, 550); + + // Restore window geometry. Logger settings (minLevel, fileWriteEnabled, + // per-category) were already loaded by Logger::init() at application + // startup, so the UI sync code below picks them up automatically. + loadSettings(); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(8, 8, 8, 8); + mainLayout->setSpacing(6); + + // ---- Log view ---- + logView = new QPlainTextEdit(this); + logView->setReadOnly(true); + logView->document()->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); + + // ---- Control row: file switches (left) + minimum level (right) ---- + auto *levelLayout = new QHBoxLayout; + levelLayout->setContentsMargins(0, 0, 0, 0); + + // File-write master switch + per-category checkboxes, grouped on the left. + fileWriteMasterCheck = new QCheckBox(tr("File"), this); + fileWriteMasterCheck->setChecked(Logger::instance().isFileWriteEnabled()); + fileWriteMasterCheck->setToolTip(tr("Master switch for writing log messages to disk. " + "When off, no messages are written regardless of " + "the per-category checkboxes.")); + connect(fileWriteMasterCheck, &QCheckBox::toggled, this, &LogPanel::onFileWriteToggle); + levelLayout->addWidget(fileWriteMasterCheck); + + struct CatInfo + { + LogCategory cat; + const char *label; + const char *tooltip; + }; + const CatInfo cats[] = { + {LogCategory::App, + "App", + "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), this); + cb->setChecked(Logger::instance().isCategoryFileEnabled(info.cat)); + cb->setToolTip(tr(info.tooltip)); + cb->setEnabled(Logger::instance().isFileWriteEnabled()); + checkboxes[idx] = cb; + + connect(cb, &QCheckBox::stateChanged, this, + [this, cat = info.cat](int state) + { onCategoryCheckChanged(cat, state == Qt::Checked); }); + + levelLayout->addWidget(cb); + } + + levelLayout->addStretch(); + + auto *levelLabel = new QLabel(tr("Minimum level:"), this); + levelLayout->addWidget(levelLabel); + + levelCombo = new QComboBox(this); + levelCombo->addItem(tr("Debug"), static_cast(LogLevel::Debug)); + levelCombo->addItem(tr("Info"), static_cast(LogLevel::Info)); + levelCombo->addItem(tr("Warning"), static_cast(LogLevel::Warning)); + levelCombo->addItem(tr("Critical"), static_cast(LogLevel::Critical)); + levelCombo->addItem(tr("Fatal"), static_cast(LogLevel::Fatal)); + levelCombo->setToolTip(tr("Messages below this level are discarded entirely " + "(neither written to file nor shown here).")); + // Sync combo to current Logger level. + { + const auto cur = Logger::instance().minLevel(); + for (int i = 0; i < levelCombo->count(); ++i) + { + if (levelCombo->itemData(i).toInt() == static_cast(cur)) + { + levelCombo->setCurrentIndex(i); + break; + } + } + } + connect(levelCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &LogPanel::onMinLevelChanged); + levelLayout->addWidget(levelCombo); + mainLayout->addLayout(levelLayout); + + // ---- 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); + + auto *cleanBtn = new QPushButton(tr("Clean old logs"), this); + cleanBtn->setToolTip(tr("Delete old log files, keeping only the most recent few.")); + connect(cleanBtn, &QPushButton::clicked, this, &LogPanel::onCleanOldLogsClicked); + btnLayout->addWidget(cleanBtn); + + 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: invoked from arbitrary threads, posted back to the + // UI thread via QueuedConnection. ~LogPanel calls clearUiCallback() so no + // new callbacks are queued after destruction. + Logger::instance().setUiCallback( + [this](LogCategory /*cat*/, const QString &line) + { + QMetaObject::invokeMethod( + this, [this, line]() { appendLine(line); }, Qt::QueuedConnection); + }); +} + +// --------------------------------------------------------------------------- +// Destructor +// --------------------------------------------------------------------------- + +LogPanel::~LogPanel() +{ + saveSettings(); + 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::onMinLevelChanged(int index) +{ + const auto level = static_cast(levelCombo->itemData(index).toInt()); + Logger::instance().setMinLevel(level); +} + +void LogPanel::onFileWriteToggle(bool checked) +{ + Logger::instance().setFileWriteEnabled(checked); + // Enable/disable the per-category checkboxes to reflect the master state. + for (auto *cb : checkboxes) + { + if (cb) + cb->setEnabled(checked); + } +} + +void LogPanel::onClearClicked() +{ + logView->clear(); +} + +void LogPanel::onCleanOldLogsClicked() +{ + Logger::instance().cleanOldLogs(); + QMessageBox::information(this, tr("Clean old logs"), + tr("Old log files have been cleaned up. " + "Only the most recent log files are kept.")); +} + +void LogPanel::onOpenLogFolderClicked() +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(Logger::instance().logDirectory())); +} + +// --------------------------------------------------------------------------- +// closeEvent +// --------------------------------------------------------------------------- + +void LogPanel::closeEvent(QCloseEvent *event) +{ + // WA_DeleteOnClose is set, so closing will trigger ~LogPanel() which + // calls saveSettings(). Nothing extra to do here. + QDialog::closeEvent(event); +} + +// --------------------------------------------------------------------------- +// loadSettings — restore window geometry. +// Logger settings (minLevel, fileWriteEnabled, per-category) are already +// loaded by Logger::init(), so the UI sync code in the constructor picks +// them up automatically. We only restore the window geometry here. +// --------------------------------------------------------------------------- + +void LogPanel::loadSettings() +{ + // Window geometry uses a flat key (same style as mainWindow/geometry). + QSettings settings; + restoreGeometry(settings.value("LogPanel/geometry").toByteArray()); +} + +// --------------------------------------------------------------------------- +// saveSettings — persist current Logger state to QSettings +// --------------------------------------------------------------------------- + +void LogPanel::saveSettings() +{ + QSettings settings; + settings.beginGroup("LogPanel"); + + settings.setValue("MinLevel", static_cast(Logger::instance().minLevel())); + settings.setValue("FileWriteEnabled", Logger::instance().isFileWriteEnabled()); + + settings.beginGroup("CategoryFileEnabled"); + settings.setValue("App", Logger::instance().isCategoryFileEnabled(LogCategory::App)); + settings.setValue("FFmpeg", Logger::instance().isCategoryFileEnabled(LogCategory::FFmpeg)); + settings.endGroup(); + + settings.endGroup(); + + // Window geometry uses a flat key (same style as mainWindow/geometry). + settings.setValue("LogPanel/geometry", saveGeometry()); +} diff --git a/YUViewLib/src/logging/LogPanel.h b/YUViewLib/src/logging/LogPanel.h new file mode 100644 index 000000000..1e80f07ab --- /dev/null +++ b/YUViewLib/src/logging/LogPanel.h @@ -0,0 +1,91 @@ +/* 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 + +// --------------------------------------------------------------------------- +// 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] │ +// ├────────────────────────────────────────────┤ +// │ [File] [App] [FFmpeg] ←stretch→ Min: [▼]│ +// ├────────────────────────────────────────────┤ +// │ [Clear] [Clean old logs] ←stretch→ [Open]│ +// └────────────────────────────────────────────┘ +// --------------------------------------------------------------------------- + +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); + +protected: + void closeEvent(QCloseEvent *event) override; + +private slots: + void onCategoryCheckChanged(LogCategory cat, bool checked); + void onMinLevelChanged(int index); + void onFileWriteToggle(bool checked); + void onClearClicked(); + void onCleanOldLogsClicked(); + void onOpenLogFolderClicked(); + +private: + void loadSettings(); + void saveSettings(); + + QPlainTextEdit *logView{nullptr}; + QComboBox *levelCombo{nullptr}; + QCheckBox *fileWriteMasterCheck{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..09f71a587 --- /dev/null +++ b/YUViewLib/src/logging/Logger.cpp @@ -0,0 +1,442 @@ +/* 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 +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Singleton +// --------------------------------------------------------------------------- + +Logger &Logger::instance() +{ + static Logger inst; + return inst; +} + +// --------------------------------------------------------------------------- +// init +// --------------------------------------------------------------------------- + +void Logger::init() +{ + QMutexLocker lock(&mutex); + if (initialised) + return; + + // Load persisted settings (fileWriteEnabled, minLevel, per-category + // enables) so that the user's preferences take effect from the very + // first message — before LogPanel is ever opened. + loadSettings(); + + // Determine log directory + const QString appData = + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + const QString logDir = appData + QDir::separator() + "logs"; + + QDir().mkpath(logDir); + rotateOldLogs(logDir); + + // Open the log file only if file writing is enabled. + if (fileWriteEnabled.load(std::memory_order_seq_cst)) + openLogFile(); + + // Configure QLoggingCategory filter rules so that category-based debug + // output (qCDebug / LOG_DEBUG) actually reaches the message handler. + // If the user set QT_LOGGING_RULES, honour it; otherwise enable yuv.* debug. + { + const auto env = QProcessEnvironment::systemEnvironment(); + if (env.value(QStringLiteral("QT_LOGGING_RULES")).isEmpty()) + QLoggingCategory::setFilterRules(QStringLiteral("yuv.*.debug=true\nyuv.*.info=true")); + } + + 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; +} + +// --------------------------------------------------------------------------- +// openLogFile – create/open the timestamped log file and write a header +// --------------------------------------------------------------------------- + +void Logger::openLogFile() +{ + // Caller must hold the mutex. + const QString appData = + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + const QString logDir = appData + QDir::separator() + "logs"; + + 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)) + { + logFilePath.clear(); + return; + } + + 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(); +} + +// --------------------------------------------------------------------------- +// loadSettings – restore persisted settings from QSettings +// --------------------------------------------------------------------------- + +void Logger::loadSettings() +{ + // Caller must hold the mutex. + QSettings settings; + settings.beginGroup("LogPanel"); + + fileWriteEnabled.store(settings.value("FileWriteEnabled", true).toBool(), + std::memory_order_seq_cst); + + currentMinLevel.store( + static_cast(settings.value("MinLevel", static_cast(LogLevel::Info)).toInt()), + std::memory_order_seq_cst); + + settings.beginGroup("CategoryFileEnabled"); + categoryFileEnabled[static_cast(LogCategory::App)].store( + settings.value("App", true).toBool(), std::memory_order_seq_cst); + categoryFileEnabled[static_cast(LogCategory::FFmpeg)].store( + settings.value("FFmpeg", true).toBool(), std::memory_order_seq_cst); + settings.endGroup(); + + settings.endGroup(); +} + +// --------------------------------------------------------------------------- +// 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; +} + +// --------------------------------------------------------------------------- +// 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)].store(enabled, std::memory_order_seq_cst); +} + +bool Logger::isCategoryFileEnabled(LogCategory cat) const +{ + return categoryFileEnabled[static_cast(cat)].load(std::memory_order_seq_cst); +} + +// --------------------------------------------------------------------------- +// Global file-write master switch +// --------------------------------------------------------------------------- + +void Logger::setFileWriteEnabled(bool enabled) +{ + QMutexLocker lock(&mutex); + if (fileWriteEnabled.load(std::memory_order_seq_cst) == enabled) + return; + fileWriteEnabled.store(enabled, std::memory_order_seq_cst); + + if (enabled) + { + // User re-enabled file logging — open a fresh log file. + if (!logFile.isOpen()) + openLogFile(); + } + else + { + // User disabled file logging — close the file immediately so that + // no further writes are possible (even ones already in flight). + if (logFile.isOpen()) + { + QTextStream out(&logFile); + out << "=== File logging disabled by user ===\n"; + logFile.flush(); + logFile.close(); + logFilePath.clear(); + bytesWritten = 0; + } + } +} + +bool Logger::isFileWriteEnabled() const +{ + return fileWriteEnabled.load(std::memory_order_seq_cst); +} + +// --------------------------------------------------------------------------- +// Minimum severity level filter +// --------------------------------------------------------------------------- + +void Logger::setMinLevel(LogLevel level) +{ + QMutexLocker lock(&mutex); + currentMinLevel.store(level, std::memory_order_seq_cst); +} + +LogLevel Logger::minLevel() const +{ + return currentMinLevel.load(std::memory_order_seq_cst); +} + +// --------------------------------------------------------------------------- +// cleanOldLogs – public wrapper around rotateOldLogs +// --------------------------------------------------------------------------- + +void Logger::cleanOldLogs() +{ + QMutexLocker lock(&mutex); + rotateOldLogs(logDirectory()); +} + +// --------------------------------------------------------------------------- +// typeToLevel – map Qt message type to our LogLevel enum +// --------------------------------------------------------------------------- + +LogLevel Logger::typeToLevel(QtMsgType type) +{ + switch (type) + { + case QtDebugMsg: return LogLevel::Debug; + case QtInfoMsg: return LogLevel::Info; + case QtWarningMsg: return LogLevel::Warning; + case QtCriticalMsg: return LogLevel::Critical; + case QtFatalMsg: return LogLevel::Fatal; + } + return LogLevel::Debug; +} + +// --------------------------------------------------------------------------- +// 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; + } + + // Drop messages below the configured minimum level (before any formatting + // or I/O – cheapest possible rejection). currentMinLevel is atomic, so this + // read is safe without the mutex. + if (typeToLevel(type) < currentMinLevel.load(std::memory_order_seq_cst)) + 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 tid = QString("0x%1").arg(reinterpret_cast(QThread::currentThreadId()), + 8, 16, QChar('0')); + const QString line = QString("[%1] [%2] [%3]%4 %5").arg(ts, levelStr, tid, location, msg); + + QMutexLocker lock(&mutex); + + // ---- UI callback (always fires regardless of file setting) ---- + if (uiCallback) + uiCallback(cat, line); + + // ---- File write (gated by master switch + per-category enable + size cap) ---- + if (logFile.isOpen() + && fileWriteEnabled.load(std::memory_order_seq_cst) + && bytesWritten < MAX_LOG_FILE_BYTES + && categoryFileEnabled[static_cast(cat)].load(std::memory_order_seq_cst)) + { + 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 +// +// Ensures at most MAX_LOG_FILES - 1 log files remain on disk after this call. +// The caller is responsible for creating the new session file (init path) or +// not (cleanOldLogs path — current session file already counts toward the +// total). Sorts by modification time (newest first) and deletes the oldest +// entries that exceed the keep limit. +// --------------------------------------------------------------------------- + +void Logger::rotateOldLogs(const QString &logDir) +{ + QDir dir(logDir); + QFileInfoList files = + dir.entryInfoList({"yuview_*.log"}, QDir::Files, QDir::Time | QDir::Reversed); + + // Keep at most MAX_LOG_FILES - 1 files so that, after the caller opens a new + // session log, the total never exceeds MAX_LOG_FILES. + 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..63a6d4bc6 --- /dev/null +++ b/YUViewLib/src/logging/Logger.h @@ -0,0 +1,176 @@ +/* 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 +#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/ +// Linux: ~/.local/share//YUView/logs/ +// +// Log rotation: keeps at most 5 log files (4 old + 1 current session); older +// ones are deleted on init. cleanOldLogs() applies the same cap without +// creating a new file. +// 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 +}; + +// Log severity levels (ordered lowest → highest). +// Messages below the configured minimum level are discarded entirely +// (neither written to file nor forwarded to the UI panel). +enum class LogLevel +{ + Debug, + Info, + Warning, + Critical, + Fatal +}; + +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; + + // Global file-write master switch (default: enabled). + // When disabled, no messages are written to the log file, regardless of + // per-category settings. The UI panel still receives messages. + void setFileWriteEnabled(bool enabled); + bool isFileWriteEnabled() const; + + // Minimum severity level filter (default: Debug = show everything). + // Messages with a level below this are discarded before reaching the + // file or the UI panel. + void setMinLevel(LogLevel level); + LogLevel minLevel() const; + + // Manually trigger cleanup of old log files (keeps the most recent + // MAX_LOG_FILES). Called automatically on init(); exposed publicly so + // the LogPanel can offer a "Clean old logs" button. + void cleanOldLogs(); + + // 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; + +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); + static LogLevel typeToLevel(QtMsgType type); + + void rotateOldLogs(const QString &logDir); + void writeEntry(QtMsgType type, const QMessageLogContext &ctx, const QString &msg); + + // Open (or reopen) the log file, write a session header, and reset the + // byte counter. Called from init() and from setFileWriteEnabled(true). + void openLogFile(); + // Load persisted settings (fileWriteEnabled, minLevel, per-category + // enables) from QSettings. Called from init(). + void loadSettings(); + + QFile logFile; + QMutex mutex; + bool initialised{false}; + QString logFilePath; + + // Atomic so accessors (isCategoryFileEnabled / isFileWriteEnabled) can read + // them without the mutex — they are touched from the UI thread while worker + // threads may call the setters concurrently. + std::array, static_cast(LogCategory::COUNT)> categoryFileEnabled{ + true, true}; + std::atomic fileWriteEnabled{true}; + // Atomic so writeEntry() can read it without the mutex for a cheap early + // rejection of messages below the configured level. + std::atomic currentMinLevel{LogLevel::Info}; + + UiCallback uiCallback; // protected by mutex + + // Per-session byte counter – stop writing when this exceeds the cap. + static constexpr qint64 MAX_LOG_FILE_BYTES = 50LL * 1024 * 1024; // 50 MB + qint64 bytesWritten{0}; + + // How many log files to keep. + static constexpr int MAX_LOG_FILES = 5; +}; diff --git a/YUViewLib/src/logging/Macros.cpp b/YUViewLib/src/logging/Macros.cpp new file mode 100644 index 000000000..66011dc1a --- /dev/null +++ b/YUViewLib/src/logging/Macros.cpp @@ -0,0 +1,46 @@ +/* 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 + * 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 "Macros.h" + +// Category definitions. The string literal is the category name used in +// logging rules (e.g. QT_LOGGING_RULES="yuv.decoder.debug=true"). +Q_LOGGING_CATEGORY(logApp, "yuv.app") +Q_LOGGING_CATEGORY(logParser, "yuv.parser") +Q_LOGGING_CATEGORY(logDecoder, "yuv.decoder") +Q_LOGGING_CATEGORY(logVideo, "yuv.video") +Q_LOGGING_CATEGORY(logCache, "yuv.cache") +Q_LOGGING_CATEGORY(logUI, "yuv.ui") +Q_LOGGING_CATEGORY(logFFmpeg, "yuv.ffmpeg") +Q_LOGGING_CATEGORY(logStats, "yuv.stats") +Q_LOGGING_CATEGORY(logFileSource, "yuv.filesource") +Q_LOGGING_CATEGORY(logUpdater, "yuv.updater") diff --git a/YUViewLib/src/logging/Macros.h b/YUViewLib/src/logging/Macros.h new file mode 100644 index 000000000..b560f0504 --- /dev/null +++ b/YUViewLib/src/logging/Macros.h @@ -0,0 +1,79 @@ +/* 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 + * 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 + +// --------------------------------------------------------------------------- +// Unified logging macros +// +// Replace the dozens of per-file `#define XXX_DEBUG_OUTPUT 0` switches with +// QLoggingCategory-based macros that can be toggled at runtime. +// +// Categories are organised by module. Each category can be enabled/disabled +// individually via: +// * Environment variable: QT_LOGGING_RULES="yuv.decoder.debug=true" +// * Code: QLoggingCategory::setFilterRules("yuv.*.debug=true") +// * Multiple rules: "yuv.decoder.debug=true;yuv.cache.debug=false" +// +// File / line / function context is captured automatically by Qt's +// QMessageLogContext and forwarded to Logger::writeEntry, so the macros +// themselves stay minimal. +// +// Usage: +// LOG_DEBUG(logDecoder) << "frame" << idx << "decoded"; +// LOG_WARNING(logVideo) << "out of memory, retrying"; +// LOG_ERROR(logParser) << "parse failed at offset" << offset; +// --------------------------------------------------------------------------- + +#include + +// Module categories. Names are prefixed with "yuv." so that logging rules +// can target the whole application ("yuv.*.debug=true") or a single module +// ("yuv.decoder.debug=true"). +Q_DECLARE_LOGGING_CATEGORY(logApp) // application / startup / misc +Q_DECLARE_LOGGING_CATEGORY(logParser) // bitstream parsers (HEVC/AVC/VVC/AV1/MPEG2) +Q_DECLARE_LOGGING_CATEGORY(logDecoder) // decoder base + all decoder backends +Q_DECLARE_LOGGING_CATEGORY(logVideo) // video handlers (RGB/YUV/resample/diff) +Q_DECLARE_LOGGING_CATEGORY(logCache) // frame caching and worker threads +Q_DECLARE_LOGGING_CATEGORY(logUI) // widgets, views, playback, main window +Q_DECLARE_LOGGING_CATEGORY(logFFmpeg) // FFmpeg library bridging +Q_DECLARE_LOGGING_CATEGORY(logStats) // statistics file parsing & overlay +Q_DECLARE_LOGGING_CATEGORY(logFileSource) // file source / data source +Q_DECLARE_LOGGING_CATEGORY(logUpdater) // update handler + +// Convenience macros. Using .noquote() so that string values appear in the +// log without surrounding double quotes, keeping the output readable. +#define LOG_DEBUG(cat) qCDebug(cat).noquote() +#define LOG_INFO(cat) qCInfo(cat).noquote() +#define LOG_WARNING(cat) qCWarning(cat).noquote() +#define LOG_ERROR(cat) qCCritical(cat).noquote() +#define LOG_FATAL(cat) qCFatal(cat).noquote() 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/HRD.cpp b/YUViewLib/src/parser/AVC/HRD.cpp index 64f5deb18..bb7939690 100644 --- a/YUViewLib/src/parser/AVC/HRD.cpp +++ b/YUViewLib/src/parser/AVC/HRD.cpp @@ -31,6 +31,7 @@ */ #include "HRD.h" +#include #include "SEI/buffering_period.h" #include "SEI/pic_timing.h" @@ -38,13 +39,7 @@ #include -#define PARSER_AVC_HRD_DEBUG_OUTPUT 0 -#if PARSER_AVC_HRD_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_AVC_HRD(msg) qDebug() << msg -#else -#define DEBUG_AVC_HRD(fmt) ((void)0) -#endif +#define DEBUG_AVC_HRD(msg) LOG_DEBUG(logParser) << msg namespace parser::avc diff --git a/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp b/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp index f904b0b6c..57726f561 100644 --- a/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp +++ b/YUViewLib/src/parser/AVC/ParserAnnexBAVC.cpp @@ -31,8 +31,10 @@ */ #include "ParserAnnexBAVC.h" +#include #include +#include #include "NalUnitAVC.h" #include "SEI/buffering_period.h" @@ -47,13 +49,7 @@ #include "slice_rbsp.h" #include -#define PARSER_AVC_DEBUG_OUTPUT 0 -#if PARSER_AVC_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_AVC(msg) qDebug() << msg -#else -#define DEBUG_AVC(fmt) ((void)0) -#endif +#define DEBUG_AVC(msg) LOG_DEBUG(logParser) << msg namespace parser { @@ -211,6 +207,30 @@ ParserAnnexBAVC::parseAndAddNALUnit(int DEBUG_AVC("ParserAnnexBAVC::parseAndAddNALUnit Adding start/end NA/NA - POC " << *this->curFrameData->poc << (this->curFrameData->isRandomAccess ? " - ra" : "")); + + // Emit the bitrate entry for the last AU (which is never emitted on the + // start-of-next-AU path because there is no next AU). + if (this->sizeCurrentAU > 0) + { + DEBUG_AVC("ParserAnnexBAVC::parseAndAddNALUnit End of file. Adding bitrate " + << this->sizeCurrentAU); + + BitratePlotModel::BitrateEntry entry; + entry.pts = this->lastFramePOC; + entry.dts = this->counterAU; + entry.duration = 1; + 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; + } } // The file ended this->hrd.endOfFile(this->getHRDPlotModel()); @@ -365,8 +385,23 @@ 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) + << QString::fromStdString(std::string(NalTypeMapper.getName(nalAVC->header.nal_unit_type))) << ") POC " << newSliceHeader->globalPOC); parseResult.nalTypeName = "Slice(POC " + std::to_string(newSliceHeader->globalPOC) + ") "; } @@ -522,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) @@ -549,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/AVC/slice_header.cpp b/YUViewLib/src/parser/AVC/slice_header.cpp index b5b64f29a..6cfc2d00a 100644 --- a/YUViewLib/src/parser/AVC/slice_header.cpp +++ b/YUViewLib/src/parser/AVC/slice_header.cpp @@ -31,6 +31,7 @@ */ #include "slice_header.h" +#include #include @@ -44,13 +45,7 @@ #include -#define PARSER_AVC_SLICEHEADER_DEBUG_OUTPUT 0 -#if PARSER_AVC_SLICEHEADER_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_AVC(msg) qDebug() << msg -#else -#define DEBUG_AVC(fmt) ((void)0) -#endif +#define DEBUG_AVC(msg) LOG_DEBUG(logParser) << msg namespace parser::avc { @@ -59,16 +54,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/AVFormat/ParserAVFormat.cpp b/YUViewLib/src/parser/AVFormat/ParserAVFormat.cpp index e0f7e22e2..3e98c674b 100644 --- a/YUViewLib/src/parser/AVFormat/ParserAVFormat.cpp +++ b/YUViewLib/src/parser/AVFormat/ParserAVFormat.cpp @@ -31,6 +31,7 @@ */ #include "ParserAVFormat.h" +#include #include #include @@ -46,13 +47,7 @@ #include "parser/common/SubByteReaderLogging.h" #include -#define PARSERAVCFORMAT_DEBUG_OUTPUT 0 -#if PARSERAVCFORMAT_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_AVFORMAT qDebug -#else -#define DEBUG_AVFORMAT(fmt, ...) ((void)0) -#endif +#define DEBUG_AVFORMAT(...) qCDebug(logParser, __VA_ARGS__) using namespace std::string_literals; using namespace FFmpeg; @@ -82,8 +77,17 @@ vector ParserAVFormat::getStreamInfo() for (int i = 1; i < this->streamInfoAllStreams.count(); i++) { - QTreeWidgetItem *streamInfo = - new QTreeWidgetItem(QStringList() << QString("Stream %1").arg(i - 1)); + // Extract the codec type name (e.g. "Video", "Audio", "Subtitle") from the stored info list + // so the top-level node can be labelled "Video Stream 0" instead of the generic "Stream 0". + QString typeName; + for (const auto &p : this->streamInfoAllStreams[i]) + if (p.first == "Codec Type") { typeName = p.second; break; } + + const QString label = typeName.isEmpty() + ? QString("Stream %1").arg(i - 1) + : QString("%1 Stream %2").arg(typeName).arg(i - 1); + + QTreeWidgetItem *streamInfo = new QTreeWidgetItem(QStringList() << label); for (QStringPair p : this->streamInfoAllStreams[i]) new QTreeWidgetItem(streamInfo, QStringList() << p.first << p.second); info.push_back(streamInfo); @@ -482,7 +486,7 @@ bool ParserAVFormat::parseAVPacket(unsigned packetID, auto [nrBytesRead, name] = subtitle::dvb::parseDVBSubtitleSegment(data, itemTree); (void)name; DEBUG_AVFORMAT( - "ParserAVFormat::parseAVPacket parsed DVB segment %d - %d bytes", obuID, nrBytesRead); + "ParserAVFormat::parseAVPacket parsed DVB segment %d - %d bytes", segmentID, nrBytesRead); constexpr auto minDVBSegmentSize = 6u; auto remaining = std::distance(posInData, avpacketData.end()); @@ -535,8 +539,8 @@ bool ParserAVFormat::parseAVPacket(unsigned packetID, { (void)e; DEBUG_AVFORMAT( - "ParserAVFormat::parseAVPacket Exception occured while parsing generic packet data: " - << e.what()); + "ParserAVFormat::parseAVPacket Exception occured while parsing generic packet data: %s", + e.what()); } } @@ -581,7 +585,7 @@ bool ParserAVFormat::runParsingOfFile(const std::filesystem::path &compressedFil // Open the file but don't parse it yet. FileSourceFFmpegFile ffmpegFile; if (!ffmpegFile.openFile( - QString::fromStdString(compressedFilePath.string()), nullptr, nullptr, false)) + QString::fromStdWString(compressedFilePath.wstring()), nullptr, nullptr, false)) { emit backgroundParsingDone("Error opening the ffmpeg file."); return false; diff --git a/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.cpp b/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.cpp index 390d85a05..bbed5a1ed 100644 --- a/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.cpp +++ b/YUViewLib/src/parser/HEVC/ParserAnnexBHEVC.cpp @@ -31,6 +31,7 @@ */ #include "ParserAnnexBHEVC.h" +#include #include #include @@ -47,13 +48,7 @@ #include "video_parameter_set_rbsp.h" #include -#define PARSER_HEVC_DEBUG_OUTPUT 0 -#if PARSER_HEVC_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_HEVC(msg) qDebug() << msg -#else -#define DEBUG_HEVC(msg) ((void)0) -#endif +#define DEBUG_HEVC(msg) LOG_DEBUG(logParser) << msg namespace parser { @@ -313,6 +308,21 @@ ParserAnnexBHEVC::parseAndAddNALUnit(int DEBUG_HEVC("ParserAnnexBHEVC::parseAndAddNALUnit Adding start/end NA/NA - POC " << curFramePOC << " layer " << curFrameLayerID << (curFrameIsRandomAccess ? " - ra" : "")); + + // Emit the bitrate entry for the last AU (which is never emitted on the + // start-of-next-AU path because there is no next AU). + if (this->sizeCurrentAU > 0) + { + BitratePlotModel::BitrateEntry entry; + entry.pts = this->lastFramePOC; + entry.dts = int(this->counterAU); + entry.duration = 1; + entry.bitrate = this->sizeCurrentAU; + entry.keyframe = this->currentAUAllSlicesIntra; + entry.frameType = QString::fromStdString( + convertSliceCountsToString(this->currentAUSliceTypes)); + parseResult.bitrateEntry = entry; + } } // The file ended return parseResult; @@ -515,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; @@ -633,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; @@ -641,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/Mpeg2/ParserAnnexBMpeg2.cpp b/YUViewLib/src/parser/Mpeg2/ParserAnnexBMpeg2.cpp index 34ce90ea0..ee038a258 100644 --- a/YUViewLib/src/parser/Mpeg2/ParserAnnexBMpeg2.cpp +++ b/YUViewLib/src/parser/Mpeg2/ParserAnnexBMpeg2.cpp @@ -31,6 +31,7 @@ */ #include "ParserAnnexBMpeg2.h" +#include #include "NalUnitMpeg2.h" #include "group_of_pictures_header.h" @@ -43,13 +44,7 @@ #include -#define PARSER_MPEG2_DEBUG_OUTPUT 0 -#if PARSER_MPEG2_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_MPEG2(msg) qDebug() << msg -#else -#define DEBUG_MPEG2(msg) ((void)0) -#endif +#define DEBUG_MPEG2(msg) LOG_DEBUG(logParser) << msg namespace parser { @@ -65,6 +60,29 @@ ParserAnnexBMpeg2::parseAndAddNALUnit(int { ParserAnnexB::ParseResult parseResult; + if (nalID == -1 && data.empty()) + { + // The file ended. Emit the bitrate entry for the last AU (which is never + // emitted on the start-of-next-AU path because there is no next AU). + if (this->lastFramePOC >= 0 && this->sizeCurrentAU > 0) + { + DEBUG_MPEG2("ParserAnnexBMpeg2::parseAndAddNALUnit End of file. Adding bitrate " + << this->sizeCurrentAU); + + BitratePlotModel::BitrateEntry entry; + entry.pts = this->lastFramePOC; + entry.dts = this->counterAU; + entry.duration = 1; + entry.bitrate = this->sizeCurrentAU; + entry.keyframe = this->currentAUAllSlicesIntra; + entry.frameType = + QString::fromStdString(convertSliceCountsToString(this->currentAUSliceCounts)); + parseResult.bitrateEntry = entry; + } + parseResult.success = true; + return parseResult; + } + // Skip the NAL unit header unsigned readOffset = 0; if (data.at(0) == (char)0 && data.at(1) == (char)0 && data.at(2) == (char)1) 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 diff --git a/YUViewLib/src/parser/Parser.cpp b/YUViewLib/src/parser/Parser.cpp index 72a410693..1098ffa34 100644 --- a/YUViewLib/src/parser/Parser.cpp +++ b/YUViewLib/src/parser/Parser.cpp @@ -31,16 +31,11 @@ */ #include "Parser.h" +#include #include -#define BASE_DEBUG_OUTPUT 0 -#if BASE_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_PARSER qDebug -#else -#define DEBUG_PARSER(fmt, ...) ((void)0) -#endif +#define DEBUG_PARSER(...) qCDebug(logParser, __VA_ARGS__) namespace parser { diff --git a/YUViewLib/src/parser/ParserAnnexB.cpp b/YUViewLib/src/parser/ParserAnnexB.cpp index 21f669b29..9cc0c90c2 100644 --- a/YUViewLib/src/parser/ParserAnnexB.cpp +++ b/YUViewLib/src/parser/ParserAnnexB.cpp @@ -31,6 +31,7 @@ */ #include "ParserAnnexB.h" +#include #include #include @@ -39,13 +40,7 @@ #include #include -#define PARSERANNEXB_DEBUG_OUTPUT 0 -#if PARSERANNEXB_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_ANNEXB(msg) qDebug() << msg -#else -#define DEBUG_ANNEXB(msg) ((void)0) -#endif +#define DEBUG_ANNEXB(msg) LOG_DEBUG(logParser) << msg namespace parser { @@ -258,6 +253,8 @@ bool ParserAnnexB::parseAnnexBFile(std::unique_ptr &file, if (!parseResult.success) DEBUG_ANNEXB( "ParserAnnexB::parseAndAddNALUnit Error finalizing parsing. This should not happen."); + else if (parseResult.bitrateEntry) + this->bitratePlotModel->addBitratePoint(0, *parseResult.bitrateEntry); } catch (...) { diff --git a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp index 1ca5163c8..5ef966c68 100644 --- a/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp +++ b/YUViewLib/src/parser/VVC/ParserAnnexBVVC.cpp @@ -31,11 +31,14 @@ */ #include "ParserAnnexBVVC.h" +#include #include #include #include +#include + #include "SEI/buffering_period.h" #include "SEI/sei_message.h" #include "access_unit_delimiter_rbsp.h" @@ -52,13 +55,7 @@ #include "slice_layer_rbsp.h" #include "video_parameter_set_rbsp.h" -#define PARSER_VVC_DEBUG_OUTPUT 0 -#if PARSER_VVC_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_VVC(msg) qDebug() << msg -#else -#define DEBUG_VVC(msg) ((void)0) -#endif +#define DEBUG_VVC(msg) LOG_DEBUG(logParser) << msg namespace parser { @@ -85,8 +82,17 @@ 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); + 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 && @@ -480,6 +503,26 @@ 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 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; @@ -562,6 +605,11 @@ ParserAnnexBVVC::parseAndAddNALUnit(int updatedParsingState.currentAU.fileStartEndPos = nalStartEndPosFile; 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 43edf03ef..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 { @@ -68,6 +69,13 @@ struct ParsingState unsigned layerID{0}; 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/VVC/picture_header_structure.cpp b/YUViewLib/src/parser/VVC/picture_header_structure.cpp index dd62ef8ca..9e412acfd 100644 --- a/YUViewLib/src/parser/VVC/picture_header_structure.cpp +++ b/YUViewLib/src/parser/VVC/picture_header_structure.cpp @@ -31,19 +31,14 @@ */ #include "picture_header_structure.h" +#include #include "pic_parameter_set_rbsp.h" #include "seq_parameter_set_rbsp.h" #include "slice_layer_rbsp.h" #include "video_parameter_set_rbsp.h" -#define PARSER_VVC_PICTURE_HEADER_DEBUG_OUTPUT 0 -#if PARSER_VVC_PICTURE_HEADER_DEBUG_OUTPUT && !NDEBUG -#include -#define DEBUG_PICHEADER(msg) qDebug() << msg -#else -#define DEBUG_PICHEADER(msg) ((void)0) -#endif +#define DEBUG_PICHEADER(msg) LOG_DEBUG(logParser) << msg namespace parser::vvc { @@ -512,7 +507,7 @@ void picture_header_structure::calculatePictureOrderCount( DEBUG_PICHEADER("calculatePictureOrderCount PicOrderCntVal " << this->PicOrderCntVal << " ph_pic_order_cnt_lsb " << this->ph_pic_order_cnt_lsb << " PicOrderCntMsb " << this->PicOrderCntMsb << " NAL " - << QString::fromStdString(NalTypeMapper.getName(nalType)) + << QString::fromStdString(std::string(NalTypeMapper.getName(nalType))) << (isCLVSSPicture ? " CLVS" : "")); reader.logCalculatedValue("PicOrderCntMsb", int64_t(this->PicOrderCntMsb)); diff --git a/YUViewLib/src/parser/common/BitratePlotModel.cpp b/YUViewLib/src/parser/common/BitratePlotModel.cpp index a99dbd798..0aa19cd04 100644 --- a/YUViewLib/src/parser/common/BitratePlotModel.cpp +++ b/YUViewLib/src/parser/common/BitratePlotModel.cpp @@ -31,15 +31,10 @@ * along with this program. If not, see . */ -#define BITRATE_PLOT_MODE_DEBUG 0 -#if BITRATE_PLOT_MODE_DEBUG && !NDEBUG -#include -#define DEBUG_PLOT(msg) qDebug() << msg -#else -#define DEBUG_PLOT(msg) ((void)0) -#endif +#define DEBUG_PLOT(msg) LOG_DEBUG(logParser) << msg #include "BitratePlotModel.h" +#include #include @@ -52,17 +47,19 @@ PlotModel::StreamParameter BitratePlotModel::getStreamParameter(unsigned streamI { QMutexLocker locker(&this->dataMutex); - if (this->dataPerStream.contains(streamIndex)) + auto it = this->dataPerStream.constFind(streamIndex); + if (it != this->dataPerStream.constEnd()) { + const auto &list = *it; PlotModel::StreamParameter streamParameter; streamParameter.xRange.min = (sortMode == SortMode::DECODE_ORDER) ? double(this->rangeDts.min) - : double(this->rangePts.min); + : double(this->rangePts.min); streamParameter.xRange.max = (sortMode == SortMode::DECODE_ORDER) ? double(this->rangeDts.max) - : double(this->rangePts.max); + : double(this->rangePts.max); streamParameter.yRange.min = double(this->rangeBitratePerStream[streamIndex].min); streamParameter.yRange.max = double(this->rangeBitratePerStream[streamIndex].max); - const auto nrPoints = unsigned(this->dataPerStream[streamIndex].size()); + const auto nrPoints = unsigned(list.size()); streamParameter.plotParameters.append({PlotType::Bar, nrPoints}); streamParameter.plotParameters.append({PlotType::Line, nrPoints}); @@ -71,29 +68,53 @@ PlotModel::StreamParameter BitratePlotModel::getStreamParameter(unsigned streamI return {}; } +// Parse the first frame-type token from a string like "I", "P(2) B", "Keyframe". +// Returns Unknown if the type cannot be determined. +static FrameType parseFrameType(const QString &frameTypeStr, bool keyframe) +{ + // AVFormat only provides "Keyframe" / "Frame". + if (frameTypeStr.isEmpty()) + return keyframe ? FrameType::I : FrameType::Unknown; + const auto first = frameTypeStr.at(0); + if (first == 'I' || first == 'i') + return FrameType::I; + if (first == 'P' || first == 'p') + return FrameType::P; + if (first == 'B' || first == 'b') + return FrameType::B; + // "Keyframe" starts with 'K' + if (first == 'K' || first == 'k') + return FrameType::I; + return keyframe ? FrameType::I : FrameType::Unknown; +} + PlotModel::Point BitratePlotModel::getPlotPoint(unsigned streamIndex, unsigned plotIndex, unsigned pointIndex) const { QMutexLocker locker(&this->dataMutex); - if (!this->dataPerStream.contains(streamIndex)) + auto it = this->dataPerStream.constFind(streamIndex); + if (it == this->dataPerStream.constEnd()) return {}; - if (pointIndex < unsigned(this->dataPerStream[streamIndex].size())) + const auto &list = *it; + if (pointIndex < unsigned(list.size())) { + const auto &entry = list[pointIndex]; PlotModel::Point point; if (this->sortMode == SortMode::DECODE_ORDER) - point.x = this->dataPerStream[streamIndex][pointIndex].dts; + point.x = entry.dts; else - point.x = this->dataPerStream[streamIndex][pointIndex].pts; - point.intra = this->dataPerStream[streamIndex][pointIndex].keyframe; + point.x = entry.pts; + point.intra = entry.keyframe; + point.frameType = parseFrameType(entry.frameType, entry.keyframe); const auto isAveragePlot = (plotIndex == 1); if (isAveragePlot) point.y = this->calculateAverageValue(streamIndex, pointIndex); else - point.y = this->dataPerStream[streamIndex][pointIndex].bitrate; - point.width = this->dataPerStream[streamIndex][pointIndex].duration; + point.y = entry.bitrate; + point.width = entry.duration; return point; } @@ -106,11 +127,11 @@ BitratePlotModel::getPointInfo(unsigned streamIndex, unsigned plotIndex, unsigne { QMutexLocker locker(&this->dataMutex); - if (!this->dataPerStream.contains(streamIndex) || - unsigned(this->dataPerStream[streamIndex].size()) <= pointIndex) + auto it = this->dataPerStream.constFind(streamIndex); + if (it == this->dataPerStream.constEnd() || unsigned(it->size()) <= pointIndex) return {}; - auto entry = this->dataPerStream[streamIndex][pointIndex]; + auto entry = it->at(pointIndex); const auto isAveragePlot = (plotIndex == 1); if (isAveragePlot) @@ -119,26 +140,36 @@ 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); - else - return QString("

Stream %1

" - "" - "" - "" - "" - "" "
PTS:%2
DTS:%3
Duration:%4
Bitrate:%5
") .arg(streamIndex) .arg(entry.pts) .arg(entry.dts) - .arg(entry.duration) - .arg(entry.bitrate); + .arg(this->calculateAverageValue(streamIndex, pointIndex)); + + // 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 @@ -180,7 +211,12 @@ void BitratePlotModel::addBitratePoint(int streamIndex, BitrateEntry &entry) const auto newStream = !this->dataPerStream.contains(streamIndex); - if (this->dataPerStream[streamIndex].empty()) + // rangeDts/rangePts are global (shared across all streams). Only initialize + // them from the very first point of the very first stream; for subsequent + // points (including the first point of a later stream) use min/max update. + const bool noDataYet = this->dataPerStream.empty() || + (this->dataPerStream.size() == 1 && this->dataPerStream.begin()->isEmpty()); + if (noDataYet) { rangeDts.min = entry.dts; rangeDts.max = entry.dts; @@ -251,14 +287,19 @@ void BitratePlotModel::setBitrateSortingIndex(int index) } unsigned int BitratePlotModel::calculateAverageValue(unsigned streamIndex, - unsigned pointIndex) const + unsigned pointIndex) const { + auto it = this->dataPerStream.constFind(streamIndex); + if (it == this->dataPerStream.constEnd()) + return 0; + + const auto &list = *it; const unsigned int averageRange = 10; unsigned averageBitrate = 0; const unsigned start = std::max(unsigned(0), pointIndex - averageRange); const unsigned end = - std::min(pointIndex + averageRange, unsigned(this->dataPerStream[streamIndex].size())); + std::min(pointIndex + averageRange, unsigned(list.size())); for (unsigned i = start; i < end; i++) - averageBitrate += unsigned(this->dataPerStream[streamIndex][i].bitrate); + averageBitrate += unsigned(list[i].bitrate); return averageBitrate / (end - start); } 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); diff --git a/YUViewLib/src/parser/common/HRDPlotModel.cpp b/YUViewLib/src/parser/common/HRDPlotModel.cpp index 209fe2cf4..d77c0357b 100644 --- a/YUViewLib/src/parser/common/HRDPlotModel.cpp +++ b/YUViewLib/src/parser/common/HRDPlotModel.cpp @@ -31,15 +31,10 @@ * along with this program. If not, see . */ -#define HRD_PLOT_MODE_DEBUG 0 -#if HRD_PLOT_MODE_DEBUG && !NDEBUG -#include -#define DEBUG_PLOT(msg) qDebug() << msg -#else -#define DEBUG_PLOT(msg) ((void)0) -#endif +#define DEBUG_PLOT(msg) LOG_DEBUG(logParser) << msg #include "HRDPlotModel.h" +#include #include #include @@ -84,7 +79,7 @@ PlotModel::Point HRDPlotModel::getPlotPoint(unsigned streamIndex, unsigned, unsi return {}; if (pointIndex == 0) - return {0, 0, 0, false}; + return {0, 0, 0, false, FrameType::Unknown}; QMutexLocker locker(&this->dataMutex); diff --git a/YUViewLib/src/parser/common/PacketItemModel.cpp b/YUViewLib/src/parser/common/PacketItemModel.cpp index 5b5e9b4d1..a26b33f50 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 @@ -62,7 +64,37 @@ 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) + +// Dark-theme palette: all 200-series colors (the light-theme list above mixes +// 200 and 600 series; this one stays fully soft for dark backgrounds). +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) { @@ -89,10 +121,24 @@ QVariant PacketItemModel::data(const QModelIndex &index, int role) const return {}; auto item = static_cast(index.internalPointer()); + + // Evaluate once per data() call to avoid repeated palette queries. + const bool darkTheme = isDarkTheme(); + if (role == Qt::ForegroundRole) { if (item->isError()) return QVariant(QBrush(QColor(255, 0, 0))); + // Use dark text only when the row has a stream background color (Material 200 + // pastels are light regardless of theme, so dark text always gives good contrast). + // For plain rows without a stream background let QPalette::Text decide — + // it will be light on dark themes and dark on light themes automatically. + if (useColorCoding) + { + const int idx = item->getStreamIndex(); + if (idx >= 0 || idx == -1) + return QVariant(QBrush(QColor(40, 40, 40))); + } return QVariant(QBrush()); } if (role == Qt::BackgroundRole) @@ -101,8 +147,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 = darkTheme ? streamIndexColorsDark : streamIndexColors; + return QVariant(QBrush(functionsGui::toQColor(colors.at(idx % colors.size())))); + } + else if (idx == -1) + { + const auto &rawColor = darkTheme ? rawStreamColorDark : rawStreamColor; + return QVariant(QBrush(functionsGui::toQColor(rawColor))); + } return QVariant(QBrush()); } else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) diff --git a/YUViewLib/src/parser/common/SubByteReaderLogging.cpp b/YUViewLib/src/parser/common/SubByteReaderLogging.cpp index 41b348fec..3b4325250 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); } } } @@ -131,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, @@ -153,6 +144,7 @@ SubByteReaderLogging::SubByteReaderLogging(const ByteVector & inArr, { if (item) { + item->setRawData(inArr); if (new_sub_item_name.empty()) this->currentTreeLevel = item; else @@ -191,8 +183,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 +198,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 +213,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 +228,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 +243,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 +259,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 +275,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 +295,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 +310,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 +320,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/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/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/playlistitem/playlistItemCompressedVideo.cpp b/YUViewLib/src/playlistitem/playlistItemCompressedVideo.cpp index 30dd3dfc5..f7f18d637 100644 --- a/YUViewLib/src/playlistitem/playlistItemCompressedVideo.cpp +++ b/YUViewLib/src/playlistitem/playlistItemCompressedVideo.cpp @@ -31,8 +31,10 @@ */ #include "playlistItemCompressedVideo.h" +#include #include +#include #include #include @@ -61,13 +63,7 @@ using namespace functions; using namespace decoder; -#define COMPRESSED_VIDEO_DEBUG_OUTPUT 0 -#if COMPRESSED_VIDEO_DEBUG_OUTPUT -#include -#define DEBUG_COMPRESSED(f) qDebug() << f -#else -#define DEBUG_COMPRESSED(f) ((void)0) -#endif +#define DEBUG_COMPRESSED(msg) LOG_DEBUG(logDecoder) << msg using namespace std::string_view_literals; @@ -145,7 +141,7 @@ playlistItemCompressedVideo::playlistItemCompressedVideo(const QString &compress { // Open file DEBUG_COMPRESSED("playlistItemCompressedVideo::playlistItemCompressedVideo Open annexB file"); - const auto filePath = std::filesystem::path(compressedFilePath.toStdString()); + const auto filePath = functionsGui::toFileSystemPath(compressedFilePath); this->inputFileAnnexBLoading = std::make_unique(filePath); if (this->cachingEnabled) this->inputFileAnnexBCaching = std::make_unique(filePath); @@ -602,6 +598,8 @@ void playlistItemCompressedVideo::loadRawData(int frameIdx, bool caching) { if (caching && !this->cachingEnabled) return; + if (!caching && !this->loadingDecoder) + return; if (!caching && this->loadingDecoder->state() == decoder::DecoderState::Error) { if (frameIdx < this->currentFrameIdx[0]) @@ -612,7 +610,8 @@ void playlistItemCompressedVideo::loadRawData(int frameIdx, bool caching) else return; } - if (caching && this->cachingDecoder->state() == decoder::DecoderState::Error) + if (caching && + (!this->cachingDecoder || this->cachingDecoder->state() == decoder::DecoderState::Error)) return; DEBUG_COMPRESSED("playlistItemCompressedVideo::loadRawData " << frameIdx @@ -823,7 +822,7 @@ void playlistItemCompressedVideo::loadRawData(int frameIdx, bool caching) // reload when the frame number changes. this->video->rawData_frameIndex = frameIdx; } - else if (this->loadingDecoder->state() == decoder::DecoderState::Error) + else if (this->loadingDecoder && this->loadingDecoder->state() == decoder::DecoderState::Error) { this->infoText = "There was an error in the decoder: \n"; this->infoText += this->loadingDecoder->decoderErrorString(); @@ -1103,7 +1102,7 @@ void playlistItemCompressedVideo::loadStatistics(int frameIdx) DEBUG_COMPRESSED("playlistItemCompressedVideo::loadStatisticToCache Request statistics for frame " << frameIdx); - if (!this->loadingDecoder->statisticsSupported()) + if (!this->loadingDecoder || !this->loadingDecoder->statisticsSupported()) return; if (!this->loadingDecoder->statisticsEnabled()) { @@ -1116,7 +1115,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 @@ -1137,7 +1136,8 @@ ValuePairListSets playlistItemCompressedVideo::getPixelValues(const QPoint &pixe ValuePairListSets newSet; newSet.append("YUV", this->video->getPixelValues(pixelPos, frameIdx)); - if (this->loadingDecoder->statisticsSupported() && this->loadingDecoder->statisticsEnabled()) + if (this->loadingDecoder && this->loadingDecoder->statisticsSupported() && + this->loadingDecoder->statisticsEnabled()) newSet.append("Stats", this->statisticsData.getValuesAt(pixelPos)); return newSet; @@ -1242,6 +1242,10 @@ void playlistItemCompressedVideo::loadFrame(int frameIdx, // The current thread must never be the main thread but one of the interactive threads. Q_ASSERT(QThread::currentThread() != QApplication::instance()->thread()); + // Protect the loadingDecoder from being deleted/mutated (e.g. decoder engine switch) while we + // are using it. + QMutexLocker loadingLocker(&this->loadingMutex); + auto stateYUV = this->video->needsLoading(frameIdx, loadRawdata); auto stateStat = this->statisticsData.needsLoading(frameIdx); @@ -1290,14 +1294,20 @@ void playlistItemCompressedVideo::displaySignalComboBoxChanged(int idx) { if (this->loadingDecoder && idx != this->loadingDecoder->getDecodeSignal()) { + // Wait for any in-flight caching/loading job to finish before mutating the decoders. + QMutexLocker cachingLocker(&this->cachingMutex); + QMutexLocker loadingLocker(&this->loadingMutex); + bool resetDecoder = false; this->loadingDecoder->setDecodeSignal(idx, resetDecoder); - this->cachingDecoder->setDecodeSignal(idx, resetDecoder); + if (this->cachingDecoder) + this->cachingDecoder->setDecodeSignal(idx, resetDecoder); if (resetDecoder) { this->loadingDecoder->resetDecoder(); - this->cachingDecoder->resetDecoder(); + if (this->cachingDecoder) + this->cachingDecoder->resetDecoder(); // Reset the decoded frame indices so that decoding of the current frame is triggered this->currentFrameIdx[0] = -1; @@ -1319,6 +1329,10 @@ void playlistItemCompressedVideo::decoderComboxBoxChanged(int idx) auto e = this->possibleDecoders.at(idx); if (e != this->decoderEngine) { + // Wait for any in-flight caching/loading job to finish before deleting the decoders. + QMutexLocker cachingLocker(&this->cachingMutex); + QMutexLocker loadingLocker(&this->loadingMutex); + // Allocate a new decoder of the new type this->decoderEngine = e; this->allocateDecoder(); diff --git a/YUViewLib/src/playlistitem/playlistItemCompressedVideo.h b/YUViewLib/src/playlistitem/playlistItemCompressedVideo.h index e6c74aa9c..82ddc5b47 100644 --- a/YUViewLib/src/playlistitem/playlistItemCompressedVideo.h +++ b/YUViewLib/src/playlistitem/playlistItemCompressedVideo.h @@ -162,6 +162,10 @@ class playlistItemCompressedVideo : public playlistItemWithVideo // TODO: Could we somehow make shure that caching is always performed in display order? QMutex cachingMutex; + // Protect the loadingDecoder from being deleted/mutated (e.g. when the decoder engine is + // switched in the properties panel) while an interactive loading thread is using it. + QMutex loadingMutex; + stats::StatisticUIHandler statisticsUIHandler; stats::StatisticsData statisticsData; diff --git a/YUViewLib/src/playlistitem/playlistItemDifference.cpp b/YUViewLib/src/playlistitem/playlistItemDifference.cpp index 0918d4018..ae63bf484 100644 --- a/YUViewLib/src/playlistitem/playlistItemDifference.cpp +++ b/YUViewLib/src/playlistitem/playlistItemDifference.cpp @@ -31,18 +31,13 @@ */ #include "playlistItemDifference.h" +#include #include #include -// Activate this if you want to know when which difference is loaded -#define PLAYLISTITEMDIFFERENCE_DEBUG_LOADING 0 -#if PLAYLISTITEMDIFFERENCE_DEBUG_LOADING && !NDEBUG -#define DEBUG_DIFF qDebug -#else -#define DEBUG_DIFF(fmt, ...) ((void)0) -#endif +#define DEBUG_DIFF(...) qCDebug(logApp, __VA_ARGS__) #define DIFFERENCE_INFO_TEXT \ "Please drop two video item's onto this difference item to calculate the difference." diff --git a/YUViewLib/src/playlistitem/playlistItemOverlay.cpp b/YUViewLib/src/playlistitem/playlistItemOverlay.cpp index 271948c22..9efaa9737 100644 --- a/YUViewLib/src/playlistitem/playlistItemOverlay.cpp +++ b/YUViewLib/src/playlistitem/playlistItemOverlay.cpp @@ -31,6 +31,7 @@ */ #include "playlistItemOverlay.h" +#include #include "playlistItemStatisticsFile.h" @@ -42,13 +43,7 @@ #include #include -#define PLAYLISTITEMOVERLAY_DEBUG 0 -#if PLAYLISTITEMOVERLAY_DEBUG && !NDEBUG -#include -#define DEBUG_OVERLAY qDebug -#else -#define DEBUG_OVERLAY(fmt, ...) ((void)0) -#endif +#define DEBUG_OVERLAY(...) qCDebug(logApp, __VA_ARGS__) #define CUSTOM_POS_MAX 100000 @@ -144,8 +139,7 @@ ItemLoadingState playlistItemOverlay::needsLoading(int frameIdx, bool loadRawdat if (this->getChildPlaylistItem(i)->needsLoading(frameIdx, loadRawdata) == ItemLoadingState::LoadingNeeded) { - DEBUG_OVERLAY("playlistItemOverlay::needsLoading LoadingNeeded child %s", - this->getChildPlaylistItem(i)->getName().toLatin1().data()); + DEBUG_OVERLAY("playlistItemOverlay::needsLoading LoadingNeeded child %d", i); return ItemLoadingState::LoadingNeeded; } } @@ -154,8 +148,7 @@ ItemLoadingState playlistItemOverlay::needsLoading(int frameIdx, bool loadRawdat if (this->getChildPlaylistItem(i)->needsLoading(frameIdx, loadRawdata) == ItemLoadingState::LoadingNeededDoubleBuffer) { - DEBUG_OVERLAY("playlistItemOverlay::needsLoading LoadingNeededDoubleBuffer child %s", - this->getChildPlaylistItem(i)->getName().toLatin1().data()); + DEBUG_OVERLAY("playlistItemOverlay::needsLoading LoadingNeededDoubleBuffer child %d", i); return ItemLoadingState::LoadingNeededDoubleBuffer; } } @@ -243,7 +236,7 @@ void playlistItemOverlay::updateLayout(bool onlyIfItemsChanged) return; DEBUG_OVERLAY("playlistItemOverlay::updateLayout%s", - onlyIfNrItemsChanged ? " onlyIfNrItemsChanged" : ""); + onlyIfItemsChanged ? " onlyIfItemsChanged" : ""); if (nrItemsChanged || itemOrderChanged) { @@ -393,12 +386,12 @@ void playlistItemOverlay::updateLayout(bool onlyIfItemsChanged) // Set item bounding rectangle this->childItemRects[i] = targetRect; - DEBUG_OVERLAY("playlistItemOverlay::updateLayout item %d size (%d,%d) alignmentMode %d " + DEBUG_OVERLAY("playlistItemOverlay::updateLayout item %d size (%d,%d) arangementMode %d " "targetRect (%d,%d)", i, childSize.width(), childSize.height(), - alignmentMode, + this->arangementMode, targetRect.left(), targetRect.top()); @@ -527,7 +520,7 @@ playlistItemOverlay *playlistItemOverlay::newPlaylistItemOverlay(const YUViewDom DEBUG_OVERLAY( "playlistItemOverlay::newPlaylistItemOverlay alignmentMode %d manualAlignment (%d,%d)", - alignment, + alignmentMode, manualAlignmentX, manualAlignmentY); } diff --git a/YUViewLib/src/playlistitem/playlistItemRawFile.cpp b/YUViewLib/src/playlistitem/playlistItemRawFile.cpp index d3d2054fb..71b4aba8e 100644 --- a/YUViewLib/src/playlistitem/playlistItemRawFile.cpp +++ b/YUViewLib/src/playlistitem/playlistItemRawFile.cpp @@ -31,6 +31,9 @@ */ #include "playlistItemRawFile.h" +#include + +#include #include #include @@ -41,13 +44,7 @@ #include #include -// Activate this if you want to know when which buffer is loaded/converted to image and so on. -#define PLAYLISTITEMRAWFILE_DEBUG_LOADING 0 -#if PLAYLISTITEMRAWFILE_DEBUG_LOADING && !NDEBUG -#define DEBUG_RAWFILE(f) qDebug() << f -#else -#define DEBUG_RAWFILE(f) ((void)0) -#endif +#define DEBUG_RAWFILE(msg) LOG_DEBUG(logApp) << msg using namespace std::string_view_literals; @@ -83,7 +80,7 @@ playlistItemRawFile::playlistItemRawFile(const QString &rawFilePath, this->prop.isFileSource = true; this->prop.propertiesWidgetTitle = "Raw File Properties"; - this->dataSource.openFile(std::filesystem::path(rawFilePath.toStdString())); + this->dataSource.openFile(functionsGui::toFileSystemPath(rawFilePath)); if (!this->dataSource.isOk()) { @@ -135,9 +132,17 @@ playlistItemRawFile::playlistItemRawFile(const QString &rawFilePath, if (!this->video->isFormatValid()) { // Load 24883200 bytes from the input and try to get the format from the correlation. - QByteArray rawData; - this->dataSource.readBytes(rawData, 0, 24883200); - this->video->setFormatFromCorrelation(rawData, this->dataSource.getFileSize().value_or(-1)); + try + { + QByteArray rawData; + this->dataSource.readBytes(rawData, 0, 24883200); + this->video->setFormatFromCorrelation(rawData, + this->dataSource.getFileSize().value_or(-1)); + } + catch (const std::bad_alloc &) + { + qWarning() << "Out of memory while guessing format from correlation."; + } } } else @@ -226,6 +231,13 @@ InfoData playlistItemRawFile::getInfo() const else info.items.append(InfoItem("Warning"sv, "Could not obtain file size from input.")); } + else if (this->dataSource.isOk() && !this->video->isFormatValid()) + { + info.items.append(InfoItem( + "Warning"sv, + "Could not determine the video format. Please set the width, height and pixel format " + "manually in the properties panel.")); + } return info; } @@ -320,50 +332,121 @@ bool playlistItemRawFile::parseY4MFile() } else if (parameterIndicator == 'C') { - // Get 3 bytes and check them - auto formatName = rawData.mid(offset, 3); - offset += 3; - - // The YUV format. By default, YUV420 is setup. - // TDOO: What is the difference between these two formats? - // 'C420' = 4:2:0 with coincident chroma planes - // 'C420jpeg' = 4:2 : 0 with biaxially - displaced chroma planes - // 'C420paldv' = 4 : 2 : 0 with vertically - displaced chroma planes - auto subsampling = video::yuv::Subsampling::YUV_420; - if (formatName == "422") - subsampling = video::yuv::Subsampling::YUV_422; - else if (formatName == "444") - subsampling = video::yuv::Subsampling::YUV_444; + // Get the full color space word (up to the next space) + int space_index = rawData.indexOf(' ', offset); + auto formatName = rawData.mid(offset, space_index - offset); + offset = space_index; unsigned bitsPerSample = 8; + auto subsampling = video::yuv::Subsampling::YUV_420; - if (rawData.at(offset) == 'p') + if (formatName == "420" || formatName == "420jpeg" || formatName == "420mpeg2" || + formatName == "420paldv") + { + // 'C420' = 4:2:0 with coincident chroma planes + // 'C420jpeg' = 4:2:0 with biaxially-displaced chroma planes + // 'C420paldv' = 4:2:0 with vertically-displaced chroma planes + bitsPerSample = 8; + subsampling = video::yuv::Subsampling::YUV_420; + } + else if (formatName == "422") + { + bitsPerSample = 8; + subsampling = video::yuv::Subsampling::YUV_422; + } + else if (formatName == "444") + { + bitsPerSample = 8; + subsampling = video::yuv::Subsampling::YUV_444; + } + else if (formatName == "mono") { - offset++; - if (rawData.at(offset) == '1') - { - offset++; - if (rawData.at(offset) == '0') - { - bitsPerSample = 10; - offset++; - } - else if (rawData.at(offset) == '2') - { - bitsPerSample = 12; - offset++; - } - else if (rawData.at(offset) == '4') - { - bitsPerSample = 14; - offset++; - } - else if (rawData.at(offset) == '6') - { - bitsPerSample = 16; - offset++; - } - } + bitsPerSample = 8; + subsampling = video::yuv::Subsampling::YUV_400; + } + else if (formatName == "411") + { + bitsPerSample = 8; + subsampling = video::yuv::Subsampling::YUV_411; + } + // 10-bit components + else if (formatName == "420p10") + { + bitsPerSample = 10; + subsampling = video::yuv::Subsampling::YUV_420; + } + else if (formatName == "422p10") + { + bitsPerSample = 10; + subsampling = video::yuv::Subsampling::YUV_422; + } + else if (formatName == "444p10") + { + bitsPerSample = 10; + subsampling = video::yuv::Subsampling::YUV_444; + } + else if (formatName == "mono10") + { + bitsPerSample = 10; + subsampling = video::yuv::Subsampling::YUV_400; + } + // 12-bit components + else if (formatName == "420p12") + { + bitsPerSample = 12; + subsampling = video::yuv::Subsampling::YUV_420; + } + else if (formatName == "422p12") + { + bitsPerSample = 12; + subsampling = video::yuv::Subsampling::YUV_422; + } + else if (formatName == "444p12") + { + bitsPerSample = 12; + subsampling = video::yuv::Subsampling::YUV_444; + } + else if (formatName == "mono12") + { + bitsPerSample = 12; + subsampling = video::yuv::Subsampling::YUV_400; + } + // 14-bit components + else if (formatName == "420p14") + { + bitsPerSample = 14; + subsampling = video::yuv::Subsampling::YUV_420; + } + else if (formatName == "422p14") + { + bitsPerSample = 14; + subsampling = video::yuv::Subsampling::YUV_422; + } + else if (formatName == "444p14") + { + bitsPerSample = 14; + subsampling = video::yuv::Subsampling::YUV_444; + } + // 16-bit components + else if (formatName == "420p16") + { + bitsPerSample = 16; + subsampling = video::yuv::Subsampling::YUV_420; + } + else if (formatName == "422p16") + { + bitsPerSample = 16; + subsampling = video::yuv::Subsampling::YUV_422; + } + else if (formatName == "444p16") + { + bitsPerSample = 16; + subsampling = video::yuv::Subsampling::YUV_444; + } + else if (formatName == "mono16") + { + bitsPerSample = 16; + subsampling = video::yuv::Subsampling::YUV_400; } format = video::yuv::PixelFormatYUV(subsampling, bitsPerSample); @@ -396,11 +479,13 @@ bool playlistItemRawFile::parseY4MFile() // parameters is also terminated by 0x0A. // The offset in bytes to the next frame - auto stride = width * height * 3 / 2; + auto ypixels = width * height; + auto cpixels = ((width + 1) / 2) * ((height + 1) / 2); if (format.getSubsampling() == video::yuv::Subsampling::YUV_422) - stride = width * height * 2; + cpixels = ((width + 1) / 2) * height; else if (format.getSubsampling() == video::yuv::Subsampling::YUV_444) - stride = width * height * 3; + cpixels = width * height; + auto stride = ypixels + 2 * cpixels; if (format.getBitsPerSample() > 8) stride *= 2; @@ -542,6 +627,8 @@ void playlistItemRawFile::loadRawData(int frameIdx) return; auto nrBytes = this->video->getBytesPerFrame(); + if (nrBytes <= 0) + return; // Load the raw data for the given frameIdx from file and set it in the video int64_t fileStartPos; @@ -550,10 +637,21 @@ void playlistItemRawFile::loadRawData(int frameIdx) else fileStartPos = frameIdx * nrBytes; + if (fileStartPos < 0) + return; + DEBUG_RAWFILE("playlistItemRawFile::loadRawData Start loading frame " << frameIdx << " bytes " << int(nrBytes)); - if (this->dataSource.readBytes(this->video->rawData, fileStartPos, nrBytes) < nrBytes) - return; // Error + try + { + if (this->dataSource.readBytes(this->video->rawData, fileStartPos, nrBytes) < nrBytes) + return; // Error + } + catch (const std::bad_alloc &) + { + qWarning() << "Out of memory while loading RAW frame data."; + return; + } this->video->rawData_frameIndex = frameIdx; DEBUG_RAWFILE("playlistItemRawFile::loadRawData Frame " << frameIdx << " loaded"); @@ -594,7 +692,7 @@ void playlistItemRawFile::getSupportedFileExtensions(QStringList &allExtensions, void playlistItemRawFile::reloadItemSource() { // Reopen the file - this->dataSource.openFile(this->properties().name.toStdString()); + this->dataSource.openFile(functionsGui::toFileSystemPath(this->properties().name)); if (!this->dataSource.isOk()) // Opening the file failed. return; diff --git a/YUViewLib/src/playlistitem/playlistItemResample.cpp b/YUViewLib/src/playlistitem/playlistItemResample.cpp index 250ea2391..ec2e25a2c 100644 --- a/YUViewLib/src/playlistitem/playlistItemResample.cpp +++ b/YUViewLib/src/playlistitem/playlistItemResample.cpp @@ -31,18 +31,13 @@ */ #include "playlistItemResample.h" +#include #include #include -// Activate this if you want to know when which difference is loaded -#define PLAYLISTITEMRESAMPLE_DEBUG_LOADING 0 -#if PLAYLISTITEMRESAMPLE_DEBUG_LOADING && !NDEBUG -#define DEBUG_RESAMPLE qDebug -#else -#define DEBUG_RESAMPLE(fmt, ...) ((void)0) -#endif +#define DEBUG_RESAMPLE(...) qCDebug(logApp, __VA_ARGS__) #define RESAMPLE_INFO_TEXT "Please drop an item onto this item to show a resampled version of it." diff --git a/YUViewLib/src/playlistitem/playlistItemStatisticsFile.cpp b/YUViewLib/src/playlistitem/playlistItemStatisticsFile.cpp index 6b3931c3e..a45ddc368 100644 --- a/YUViewLib/src/playlistitem/playlistItemStatisticsFile.cpp +++ b/YUViewLib/src/playlistitem/playlistItemStatisticsFile.cpp @@ -31,6 +31,7 @@ */ #include "playlistItemStatisticsFile.h" +#include #include #include @@ -45,12 +46,7 @@ #include #include -#define PLAYLISTITEMSTATISTICS_DEBUG 0 -#if PLAYLISTITEMSTATISTICS_DEBUG && !NDEBUG -#define DEBUG_STAT qDebug -#else -#define DEBUG_STAT(fmt, ...) ((void)0) -#endif +#define DEBUG_STAT(...) qCDebug(logStats, __VA_ARGS__) // The internal buffer for parsing the starting positions. The buffer must not be larger than 2GB // so that we can address all the positions in it with int (using such a large buffer is not a good diff --git a/YUViewLib/src/playlistitem/playlistItemText.cpp b/YUViewLib/src/playlistitem/playlistItemText.cpp index 2d319aa0e..7cf846efa 100644 --- a/YUViewLib/src/playlistitem/playlistItemText.cpp +++ b/YUViewLib/src/playlistitem/playlistItemText.cpp @@ -31,6 +31,7 @@ */ #include "playlistItemText.h" +#include #include #include @@ -39,13 +40,7 @@ #include -// Activate this if you want to know when which buffer is loaded/converted to image and so on. -#define PLAYLISTITEMTEXT_DEBUG 0 -#if PLAYLISTITEMTEXT_DEBUG && !NDEBUG -#define DEBUG_TEXT qDebug -#else -#define DEBUG_TEXT(fmt, ...) ((void)0) -#endif +#define DEBUG_TEXT(...) qCDebug(logApp, __VA_ARGS__) playlistItemText::playlistItemText(const QString &initialText) : playlistItem(QString("Text: \"%1\"").arg(initialText), Type::Static) diff --git a/YUViewLib/src/playlistitem/playlistItemWithVideo.cpp b/YUViewLib/src/playlistitem/playlistItemWithVideo.cpp index b88e134ef..2ac603fa5 100644 --- a/YUViewLib/src/playlistitem/playlistItemWithVideo.cpp +++ b/YUViewLib/src/playlistitem/playlistItemWithVideo.cpp @@ -31,14 +31,9 @@ */ #include "playlistItemWithVideo.h" +#include -// Activate this if you want to know when which buffer is loaded/converted to image and so on. -#define PLAYLISTITEMWITHVIDEO_DEBUG_LOADING 0 -#if PLAYLISTITEMWITHVIDEO_DEBUG_LOADING && !NDEBUG -#define DEBUG_PLVIDEO qDebug -#else -#define DEBUG_PLVIDEO(fmt, ...) ((void)0) -#endif +#define DEBUG_PLVIDEO(...) qCDebug(logApp, __VA_ARGS__) playlistItemWithVideo::playlistItemWithVideo(const QString &itemNameOrFileName) : playlistItem(itemNameOrFileName, Type::Indexed) diff --git a/YUViewLib/src/statistics/StatisticUIHandler.cpp b/YUViewLib/src/statistics/StatisticUIHandler.cpp index 25567f537..351b3246c 100644 --- a/YUViewLib/src/statistics/StatisticUIHandler.cpp +++ b/YUViewLib/src/statistics/StatisticUIHandler.cpp @@ -31,6 +31,7 @@ */ #include "StatisticUIHandler.h" +#include #include #include @@ -41,20 +42,13 @@ #include #include -#include #include #include namespace stats { -// Activate this if you want to know when what is loaded. -#define STATISTICS_DEBUG_LOADING 0 -#if STATISTICS_DEBUG_LOADING && !NDEBUG -#define DEBUG_STATUI qDebug -#else -#define DEBUG_STATUI(fmt, ...) ((void)0) -#endif +#define DEBUG_STATUI(...) qCDebug(logStats, __VA_ARGS__) StatisticUIHandler::StatisticUIHandler() { @@ -97,8 +91,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 +156,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); diff --git a/YUViewLib/src/statistics/StatisticsData.cpp b/YUViewLib/src/statistics/StatisticsData.cpp index aa93a2bd7..95a4b9efc 100644 --- a/YUViewLib/src/statistics/StatisticsData.cpp +++ b/YUViewLib/src/statistics/StatisticsData.cpp @@ -31,17 +31,11 @@ */ #include "StatisticsData.h" +#include #include -// Activate this if you want to know when what is loaded. -#define STATISTICS_DEBUG_LOADING 0 -#if STATISTICS_DEBUG_LOADING && !NDEBUG -#include -#define DEBUG_STATDATA(fmt) qDebug() << fmt -#else -#define DEBUG_STATDATA(fmt) ((void)0) -#endif +#define DEBUG_STATDATA(msg) LOG_DEBUG(logStats) << msg namespace stats { @@ -186,8 +180,16 @@ std::vector StatisticsData::getTypesThatNeedLoading(int frameIndex) const typesToLoad.push_back(statsType.typeID); } - DEBUG_STATDATA("StatisticsData::getTypesThatNeedLoading " - << QString::fromStdString(to_string(typesToLoad))); + { + QString typesStr; + for (size_t i = 0; i < typesToLoad.size(); ++i) + { + if (i > 0) + typesStr += ", "; + typesStr += QString::number(typesToLoad[i]); + } + DEBUG_STATDATA("StatisticsData::getTypesThatNeedLoading [" << typesStr << "]"); + } return typesToLoad; } diff --git a/YUViewLib/src/statistics/StatisticsDataPainting.cpp b/YUViewLib/src/statistics/StatisticsDataPainting.cpp index f421055a8..8a51dc610 100644 --- a/YUViewLib/src/statistics/StatisticsDataPainting.cpp +++ b/YUViewLib/src/statistics/StatisticsDataPainting.cpp @@ -31,6 +31,7 @@ */ #include "StatisticsDataPainting.h" +#include #include #include @@ -44,14 +45,7 @@ namespace { -// Activate this if you want to know when what is loaded. -#define STATISTICS_DEBUG_PAINTING 0 -#if STATISTICS_DEBUG_PAINTING && !NDEBUG -#define DEBUG_PAINT qDebug -#else -#define DEBUG_PAINT(fmt, ...) ((void)0) -#endif - +#define DEBUG_PAINT(...) qCDebug(logStats, __VA_ARGS__) QPolygon convertToQPolygon(const stats::Polygon &poly) { if (poly.empty()) diff --git a/YUViewLib/src/statistics/StatisticsFileBase.cpp b/YUViewLib/src/statistics/StatisticsFileBase.cpp index 99b24c8c3..e186ebd87 100644 --- a/YUViewLib/src/statistics/StatisticsFileBase.cpp +++ b/YUViewLib/src/statistics/StatisticsFileBase.cpp @@ -32,6 +32,8 @@ #include "StatisticsFileBase.h" +#include + using namespace std::string_view_literals; namespace stats @@ -39,7 +41,7 @@ namespace stats StatisticsFileBase::StatisticsFileBase(const QString &filename) { - this->file.openFile(filename.toStdString()); + this->file.openFile(functionsGui::toFileSystemPath(filename)); if (!this->file.isOk()) { this->errorMessage = "Error opening file " + filename; diff --git a/YUViewLib/src/statistics/StatisticsFileCSV.cpp b/YUViewLib/src/statistics/StatisticsFileCSV.cpp index 7eeeb2272..68cd31a73 100644 --- a/YUViewLib/src/statistics/StatisticsFileCSV.cpp +++ b/YUViewLib/src/statistics/StatisticsFileCSV.cpp @@ -32,6 +32,8 @@ #include "StatisticsFileCSV.h" +#include + #include #include @@ -218,13 +220,13 @@ void StatisticsFileCSV::readFrameAndTypePositionsFromFile(std::atomic_bool &brea } catch (const char *str) { - std::cerr << "Error while parsing meta data: " << str << "\n"; + qWarning() << "Error while parsing meta data:" << str; this->errorMessage = QString("Error while parsing meta data: ") + QString(str); this->error = true; } catch (const std::exception &ex) { - std::cerr << "Error while parsing:" << ex.what() << "\n"; + qWarning() << "Error while parsing:" << ex.what(); this->errorMessage = QString("Error while parsing: ") + QString(ex.what()); this->error = true; } @@ -331,13 +333,13 @@ void StatisticsFileCSV::loadStatisticData(StatisticsData &statisticsData, int po } catch (const char *str) { - std::cerr << "Error while parsing: " << str << '\n'; + qWarning() << "Error while parsing:" << str; this->errorMessage = QString("Error while parsing meta data: ") + QString(str); this->error = true; } catch (...) { - std::cerr << "Error while parsing."; + qWarning() << "Error while parsing."; this->errorMessage = QString("Error while parsing meta data."); this->error = true; } @@ -494,13 +496,13 @@ void StatisticsFileCSV::readHeaderFromFile(StatisticsData &statisticsData) } catch (const char *str) { - std::cerr << "Error while parsing meta data: " << str << '\n'; + qWarning() << "Error while parsing meta data:" << str; this->errorMessage = QString("Error while parsing meta data: ") + QString(str); this->error = true; } catch (...) { - std::cerr << "Error while parsing meta data."; + qWarning() << "Error while parsing meta data."; this->errorMessage = QString("Error while parsing meta data."); this->error = true; } diff --git a/YUViewLib/src/statistics/StatisticsFileVTMBMS.cpp b/YUViewLib/src/statistics/StatisticsFileVTMBMS.cpp index 5f01aaf27..4757a4af0 100644 --- a/YUViewLib/src/statistics/StatisticsFileVTMBMS.cpp +++ b/YUViewLib/src/statistics/StatisticsFileVTMBMS.cpp @@ -32,6 +32,8 @@ #include "StatisticsFileVTMBMS.h" +#include + #include #include @@ -167,14 +169,14 @@ void StatisticsFileVTMBMS::readFrameAndTypePositionsFromFile(std::atomic_bool &b } catch (const char *str) { - std::cerr << "Error while parsing meta data: " << str << "\n"; + qWarning() << "Error while parsing meta data:" << str; this->errorMessage = QString("Error while parsing meta data: ") + QString(str); this->error = true; return; } catch (const std::exception &ex) { - std::cerr << "Error while parsing:" << ex.what() << "\n"; + qWarning() << "Error while parsing:" << ex.what(); this->errorMessage = QString("Error while parsing: ") + QString(ex.what()); this->error = true; return; @@ -400,13 +402,13 @@ void StatisticsFileVTMBMS::loadStatisticData(StatisticsData &statisticsData, int } // try catch (const char *str) { - std::cerr << "Error while parsing: " << str << '\n'; + qWarning() << "Error while parsing:" << str; this->errorMessage = QString("Error while parsing meta data: ") + QString(str); return; } catch (...) { - std::cerr << "Error while parsing."; + qWarning() << "Error while parsing."; this->errorMessage = QString("Error while parsing meta data."); return; } @@ -544,12 +546,12 @@ void StatisticsFileVTMBMS::readHeaderFromFile(StatisticsData &statisticsData) } // try catch (const char *str) { - std::cerr << "Error while parsing meta data: " << str << '\n'; + qWarning() << "Error while parsing meta data:" << str; this->errorMessage = QString("Error while parsing meta data: ") + QString(str); } catch (...) { - std::cerr << "Error while parsing meta data."; + qWarning() << "Error while parsing meta data."; this->errorMessage = QString("Error while parsing meta data."); } } diff --git a/YUViewLib/src/statistics/StatisticsType.cpp b/YUViewLib/src/statistics/StatisticsType.cpp index bc68925f0..c83c93203 100644 --- a/YUViewLib/src/statistics/StatisticsType.cpp +++ b/YUViewLib/src/statistics/StatisticsType.cpp @@ -76,6 +76,7 @@ std::vector AllArrowHeads = {StatisticsType::ArrowHea StatisticsType::StatisticsType(int typeID, const QString &typeName) : typeID(typeID), typeName(typeName) { + this->gridStyle.color = Color(255, 255, 255); } StatisticsType::StatisticsType(int typeID, const QString &typeName, int vectorScaling) diff --git a/YUViewLib/src/ui/Mainwindow.cpp b/YUViewLib/src/ui/Mainwindow.cpp index e0c6852b0..b3e2a739e 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(); @@ -402,13 +425,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, @@ -473,6 +494,37 @@ 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(); + }); + // "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(); } @@ -813,12 +865,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/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