diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 064ed009..7b2268d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [ master ] pull_request: - branches: [ main ] + branches: [ master ] workflow_dispatch: jobs: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index dae5f269..28f6e656 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -2,9 +2,9 @@ name: lint on: push: - branches: [ main ] + branches: [ master ] pull_request: - branches: [ main ] + branches: [ master ] workflow_dispatch: jobs: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..f98c7b25 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,77 @@ +# OpenConverter - Project Knowledge + +## Overview + +OpenConverter is a Qt + FFmpeg-based media converter with GUI and CLI modes. It supports multiple transcoding backends: FFmpeg API, FFmpeg CLI tool (FFTool), and BMF framework. + +## Build (macOS) + +```bash +# Prerequisites: FFmpeg installed via Homebrew (pkg-config), Qt 6.5.1 at /Users/jacklau/Qt +mkdir build && cd build +cmake ../src -DCMAKE_PREFIX_PATH=/Users/jacklau/Qt/6.5.1/macos -DBMF_TRANSCODER=OFF -DCMAKE_BUILD_TYPE=Debug +cmake --build . -j$(sysctl -n hw.ncpu) +# Output: build/OpenConverter.app +``` + +### CMake Options + +| Option | Default | Description | +|--------|---------|-------------| +| `ENABLE_GUI` | ON | Build with Qt GUI | +| `ENABLE_TESTS` | OFF | Build unit tests (uses GTest) | +| `BMF_TRANSCODER` | ON | Enable BMF backend (requires BMF SDK) | +| `FFTOOL_TRANSCODER` | ON | Enable FFmpeg CLI tool backend | +| `FFMPEG_TRANSCODER` | ON | Enable FFmpeg API backend | +| `FFMPEG_ROOT_PATH` | (auto) | Custom FFmpeg install path | + +### Dependencies + +- **Qt**: 6.5.1 at `/Users/jacklau/Qt/6.5.1/macos` (Components: Core, Gui, Widgets, Network) +- **FFmpeg**: 5.1.x from Homebrew (libavcodec, libavformat, libavfilter, libavutil, libswresample, libswscale) +- **BMF** (optional): at `/Users/jacklau/Documents/Programs/Git/Github/bmf/output/bmf` + +## Project Structure + +``` +src/ +├── CMakeLists.txt # Main build file +├── main.cpp # Entry point (GUI or CLI based on ENABLE_GUI) +├── common/ # Shared data structures (encode_parameter, info, stream_context) +├── engine/ # Converter engine orchestration +├── transcoder/ # Backend implementations (ffmpeg, fftool, bmf) +├── builder/ # Qt GUI pages and logic +├── component/ # Reusable Qt widgets +├── resources/ # UI files, translations, icons +└── tests/ # GTest-based unit tests +``` + +## Key Patterns + +- GUI pages inherit from `base_page` and live in `builder/` +- Reusable widgets live in `component/` +- Transcoder backends implement the `transcoder` interface in `transcoder/include/transcoder.h` +- FFmpeg version detection is automatic (supports v4.x through v7.x) +- macOS release builds use `tool/fix_macos_libs.sh` to bundle libraries + +## Testing + +```bash +cmake ../src -DENABLE_TESTS=ON -DENABLE_GUI=OFF -DBMF_TRANSCODER=OFF +cmake --build . +ctest +``` + +## Logger + +`common/include/logger.h` / `common/src/logger.cpp` + +- Singleton: `Logger::Instance()` +- `SetLogPath(std::string)` — set before enabling; called from `open_converter.cpp` using Qt-computed path +- `SetEnabled(bool)` — installs/restores `av_log` callback, opens/closes file in append mode +- Log path: `QStandardPaths::GenericDataLocation + "/OpenConverter/openconverter.log"` + - macOS: `~/Library/Application Support/OpenConverter/openconverter.log` +- Persisted via `QSettings` key `logging/fileLoggingEnabled` (default: false) +- Toggle UI: **Settings → Enable Log File** in the menu bar +- Future log level filter: add `SetMinLevel(int avLogLevel)`; check `level <= m_minLevel` in callback +- Note: `av_log_get_default_callback()` not available in FFmpeg 5.1 — use `av_log_default_callback` directly diff --git a/doc/CompileGuide.md b/doc/CompileGuide.md index f1c47953..48d5ff68 100644 --- a/doc/CompileGuide.md +++ b/doc/CompileGuide.md @@ -1,4 +1,59 @@ -# OC Project Compilation Guide for Windows +# OC Project Compilation Guide + +## macOS + +### Prerequisites + +1. **Qt 6.5+** - Install from [qt.io](https://www.qt.io/download) (e.g., to `~/Qt`) +2. **FFmpeg 5.x-7.x** - Install via Homebrew: `brew install ffmpeg` +3. **CMake 3.10+** - Install via Homebrew: `brew install cmake` + +### Build Steps + +```bash +cd OpenConverter +mkdir build && cd build + +# Configure (adjust Qt path to your installation) +cmake ../src \ + -DCMAKE_PREFIX_PATH=~/Qt/6.5.1/macos \ + -DBMF_TRANSCODER=OFF \ + -DCMAKE_BUILD_TYPE=Debug + +# Build +cmake --build . -j$(sysctl -n hw.ncpu) +``` + +The output is `OpenConverter.app` in the build directory. + +### CMake Options + +| Option | Default | Description | +|--------|---------|-------------| +| `ENABLE_GUI` | ON | Build with Qt GUI | +| `ENABLE_TESTS` | OFF | Build unit tests | +| `BMF_TRANSCODER` | ON | Enable BMF backend (requires BMF SDK) | +| `FFTOOL_TRANSCODER` | ON | Enable FFmpeg CLI tool backend | +| `FFMPEG_TRANSCODER` | ON | Enable FFmpeg API backend | +| `FFMPEG_ROOT_PATH` | (auto via pkg-config) | Custom FFmpeg install path | + +### Release Build + +For release builds, after building run the library bundling script: + +```bash +cmake ../src \ + -DCMAKE_PREFIX_PATH=~/Qt/6.5.1/macos \ + -DBMF_TRANSCODER=OFF \ + -DCMAKE_BUILD_TYPE=Release + +cmake --build . -j$(sysctl -n hw.ncpu) +../tool/fix_macos_libs.sh +``` + +--- + +## Windows ## 1. Install Qt 5 or Qt 6 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f6ceb64..30a7a9fa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.10...3.24) -project(OpenConverter VERSION 1.5.4 LANGUAGES CXX) +project(OpenConverter VERSION 1.5.5 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -25,6 +25,7 @@ set(COMMON_SOURCES ${CMAKE_SOURCE_DIR}/main.cpp ${CMAKE_SOURCE_DIR}/common/src/encode_parameter.cpp ${CMAKE_SOURCE_DIR}/common/src/info.cpp + ${CMAKE_SOURCE_DIR}/common/src/logger.cpp ${CMAKE_SOURCE_DIR}/common/src/process_parameter.cpp ${CMAKE_SOURCE_DIR}/common/src/stream_context.cpp ${CMAKE_SOURCE_DIR}/engine/src/converter.cpp @@ -34,6 +35,7 @@ set(COMMON_SOURCES set(COMMON_HEADERS ${CMAKE_SOURCE_DIR}/common/include/encode_parameter.h ${CMAKE_SOURCE_DIR}/common/include/info.h + ${CMAKE_SOURCE_DIR}/common/include/logger.h ${CMAKE_SOURCE_DIR}/common/include/process_parameter.h ${CMAKE_SOURCE_DIR}/common/include/process_observer.h ${CMAKE_SOURCE_DIR}/common/include/stream_context.h @@ -189,6 +191,20 @@ list(GET FFMPEG_LIBRARY_DIRS 0 FFMPEG_LIBRARY_DIRS_FIRST) find_ffmpeg_version(${FFMPEG_INCLUDE_DIRS}/libavutil ${FFMPEG_LIBRARY_DIRS_FIRST} OC_FFMPEG_VERSION) add_definitions(-DOC_FFMPEG_VERSION=${OC_FFMPEG_VERSION}) +# Git commit hash +execute_process( + COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/.. + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET +) +if(NOT GIT_COMMIT_HASH) + set(GIT_COMMIT_HASH "unknown") +endif() +add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") +add_definitions(-DOC_VERSION="${PROJECT_VERSION}") + # Add include directories include_directories( ${CMAKE_SOURCE_DIR} diff --git a/src/builder/include/open_converter.h b/src/builder/include/open_converter.h index 0177a928..01857a05 100644 --- a/src/builder/include/open_converter.h +++ b/src/builder/include/open_converter.h @@ -49,6 +49,7 @@ #include "../../common/include/encode_parameter.h" #include "../../common/include/info.h" +#include "../../common/include/logger.h" #include "../../common/include/process_observer.h" #include "../../common/include/process_parameter.h" @@ -91,11 +92,14 @@ private slots: void SlotTranscoderChanged(QAction *action); void OnNavigationButtonClicked(int pageIndex); void OnQueueButtonClicked(); + void SlotLogToggled(bool checked); + void SlotAbout(); private: Ui::OpenConverter *ui; QTranslator m_translator; QString m_currLang; + QString m_settingsPath; QString m_langPath; QString currentInputPath; QString currentOutputPath; diff --git a/src/builder/include/shared_data.h b/src/builder/include/shared_data.h index 6cfb1007..180f067f 100644 --- a/src/builder/include/shared_data.h +++ b/src/builder/include/shared_data.h @@ -47,9 +47,14 @@ class SharedData { // Clear all data void Clear(); + // Last browsed directory (for file dialog starting path only) + QString GetLastDirectory() const; + void SetLastDirectory(const QString &dir); + private: QString inputFilePath; QString outputFilePath; + QString lastDirectory; bool outputPathManuallySet; }; diff --git a/src/builder/src/info_view_page.cpp b/src/builder/src/info_view_page.cpp index f5d49a48..3944f861 100644 --- a/src/builder/src/info_view_page.cpp +++ b/src/builder/src/info_view_page.cpp @@ -155,10 +155,18 @@ void InfoViewPage::SetupUI() { } void InfoViewPage::OnBrowseButtonClicked() { + QString startDir; + if (!filePathLineEdit->text().isEmpty()) { + startDir = filePathLineEdit->text(); + } else { + OpenConverter *mainWindow = qobject_cast(window()); + if (mainWindow && mainWindow->GetSharedData()) + startDir = mainWindow->GetSharedData()->GetLastDirectory(); + } QString fileName = QFileDialog::getOpenFileName( this, "Select Media File", - "", + startDir, "Media Files (*.mp4 *.mkv *.avi *.mov *.flv *.wmv *.mp3 *.wav *.aac *.flac *.jpg *.jpeg *.png *.bmp *.tiff *.webp *.gif);;All Files (*.*)" ); diff --git a/src/builder/src/open_converter.cpp b/src/builder/src/open_converter.cpp index f993c0d6..6ff6b020 100644 --- a/src/builder/src/open_converter.cpp +++ b/src/builder/src/open_converter.cpp @@ -18,7 +18,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -34,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +45,7 @@ #include #include #include +#include #include "../../common/include/encode_parameter.h" #include "../../common/include/info.h" @@ -79,6 +83,77 @@ OpenConverter::OpenConverter(QWidget *parent) ui->setupUi(this); setAcceptDrops(true); + + // ── File logging ────────────────────────────────────────────────────── + QString appDataDir = QStandardPaths::writableLocation( + QStandardPaths::GenericDataLocation) + + "/OpenConverter"; + QDir().mkpath(appDataDir); + m_settingsPath = appDataDir + "/settings.ini"; + Logger::Instance().SetLogPath((appDataDir + "/openconverter.log").toStdString()); + + // ── Restore user settings ──────────────────────────────────────────── + QSettings settings(m_settingsPath, QSettings::IniFormat); + bool loggingEnabled = settings.value("logging/fileLoggingEnabled", false).toBool(); + ui->action_enableLog->setChecked(loggingEnabled); + Logger::Instance().SetEnabled(loggingEnabled); + + connect(ui->action_enableLog, &QAction::toggled, + this, &OpenConverter::SlotLogToggled); + + connect(ui->action_about, &QAction::triggered, + this, &OpenConverter::SlotAbout); + + // macOS: move Settings and About into the application menu + ui->action_enableLog->setMenuRole(QAction::NoRole); + ui->action_about->setMenuRole(QAction::AboutRole); + + // Create Preferences action for macOS app menu + QAction *prefAction = new QAction(tr("Settings"), this); + prefAction->setMenuRole(QAction::PreferencesRole); + connect(prefAction, &QAction::triggered, this, [this]() { + QDialog *settingsDialog = new QDialog(this); + settingsDialog->setWindowTitle(tr("Settings")); + settingsDialog->setAttribute(Qt::WA_DeleteOnClose); + + QVBoxLayout *layout = new QVBoxLayout(settingsDialog); + QCheckBox *logCheckBox = new QCheckBox(tr("Enable Log File"), settingsDialog); + logCheckBox->setChecked(Logger::Instance().IsEnabled()); + layout->addWidget(logCheckBox); + + connect(logCheckBox, &QCheckBox::toggled, this, [this](bool checked) { + ui->action_enableLog->setChecked(checked); + }); + + layout->addStretch(); + + QPushButton *clearButton = new QPushButton(tr("Reset All Settings"), settingsDialog); + layout->addWidget(clearButton); + + connect(clearButton, &QPushButton::clicked, this, [this, settingsDialog]() { + QMessageBox::StandardButton reply = QMessageBox::question( + settingsDialog, tr("Reset Settings"), + tr("Reset all settings to defaults? The application will restart."), + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) { + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.clear(); + Logger::Instance().SetEnabled(false); + ui->action_enableLog->setChecked(false); + settingsDialog->close(); + } + }); + + settingsDialog->setLayout(layout); + settingsDialog->exec(); + }); + // Add actions to a menu so macOS can move them to the app menu via roles. + // On Windows/Linux this menu stays visible as "Help" (placed before Language). + QMenu *helpMenu = new QMenu(tr("Help"), this); + helpMenu->addAction(prefAction); + helpMenu->addAction(ui->action_about); + menuBar()->insertMenu(ui->menuLanguage->menuAction(), helpMenu); + setWindowTitle("OpenConverter"); setWindowIcon(QIcon(":/OpenConverter-logo.png")); @@ -88,6 +163,11 @@ OpenConverter::OpenConverter(QWidget *parent) // Initialize shared data sharedData = new SharedData(); + // Restore last browsed directory from settings + QString lastDir = settings.value("app/lastFilePath").toString(); + if (!lastDir.isEmpty()) + sharedData->SetLastDirectory(lastDir); + // Initialize batch queue dialog batchQueueDialog = nullptr; @@ -122,8 +202,22 @@ OpenConverter::OpenConverter(QWidget *parent) } if (!transcoderActions.isEmpty()) { - transcoderActions.first()->setChecked(true); - converter->set_transcoder(transcoderActions.first()->objectName().toStdString()); + QString savedTranscoder = settings.value("app/transcoder").toString(); + bool restored = false; + if (!savedTranscoder.isEmpty()) { + for (QAction* action : transcoderActions) { + if (action->objectName() == savedTranscoder) { + action->setChecked(true); + converter->set_transcoder(savedTranscoder.toStdString()); + restored = true; + break; + } + } + } + if (!restored) { + transcoderActions.first()->setChecked(true); + converter->set_transcoder(transcoderActions.first()->objectName().toStdString()); + } } languageGroup->setExclusive(true); @@ -133,14 +227,17 @@ OpenConverter::OpenConverter(QWidget *parent) languageGroup->addAction(action); } - // Initialize language - default to English (no translation file needed) - m_currLang = "english"; + // Initialize language - restore from settings or default to English m_langPath = ":/"; + QString savedLang = settings.value("app/language", "english").toString(); - // Set the English menu item as checked by default for (QAction* action : languageActions) { - if (action->objectName() == "english") { + if (action->objectName() == savedLang) { action->setChecked(true); + if (savedLang != "english") + LoadLanguage(savedLang); + else + m_currLang = savedLang; break; } } @@ -221,6 +318,8 @@ void OpenConverter::SlotTranscoderChanged(QAction *action) { #endif // If the transcoder name is not valid, log an error if (isValid) { + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.setValue("app/transcoder", action->objectName()); ui->statusBar->showMessage( tr("Current Transcoder changed to %1") .arg(QString::fromStdString(transcoderName))); @@ -234,9 +333,10 @@ void OpenConverter::SlotTranscoderChanged(QAction *action) { // Called every time, when a menu entry of the language menu is called void OpenConverter::SlotLanguageChanged(QAction *action) { if (0 != action) { - // load the language dependent on the action content LoadLanguage(action->objectName()); setWindowIcon(action->icon()); + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.setValue("app/language", action->objectName()); } } @@ -456,6 +556,12 @@ void OpenConverter::OnNavigationButtonClicked(int pageIndex) { } OpenConverter::~OpenConverter() { + // Save last browsed directory + QSettings settings(m_settingsPath, QSettings::IniFormat); + QString lastDir = sharedData->GetLastDirectory(); + if (!lastDir.isEmpty()) + settings.setValue("app/lastFilePath", lastDir); + // Remove observer before deleting processParameter if (processParameter) { processParameter->remove_observer(this); @@ -501,4 +607,52 @@ QString OpenConverter::GetCurrentTranscoderName() const { return "FFMPEG"; } +void OpenConverter::SlotLogToggled(bool checked) { + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.setValue("logging/fileLoggingEnabled", checked); + Logger::Instance().SetEnabled(checked); +} + +void OpenConverter::SlotAbout() { + QString ffmpegVer = QString("%1.%2") + .arg(OC_FFMPEG_VERSION / 10) + .arg(OC_FFMPEG_VERSION % 10); + + QDialog *aboutDialog = new QDialog(this); + aboutDialog->setWindowTitle(tr("About OpenConverter")); + aboutDialog->setAttribute(Qt::WA_DeleteOnClose); + + QVBoxLayout *layout = new QVBoxLayout(aboutDialog); + + QLabel *iconLabel = new QLabel; + QPixmap logo(":/OpenConverter-logo.png"); + if (!logo.isNull()) + iconLabel->setPixmap(logo.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + iconLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(iconLabel); + + QLabel *textLabel = new QLabel(QString( + "

OpenConverter

" + "" + "" + "" + "" + "" + "
Version: %1
Commit: %2
Qt: %3
FFmpeg: %4
" + "
" + "

A media converter built on FFmpeg, Qt, and BMF.

" + "

" + "github.com/OpenConverterLab/OpenConverter

") + .arg(OC_VERSION) + .arg(GIT_COMMIT_HASH) + .arg(QT_VERSION_STR) + .arg(ffmpegVer)); + textLabel->setOpenExternalLinks(true); + textLabel->setTextFormat(Qt::RichText); + layout->addWidget(textLabel); + + aboutDialog->setLayout(layout); + aboutDialog->exec(); +} + #include "open_converter.moc" diff --git a/src/builder/src/open_converter.ui b/src/builder/src/open_converter.ui index 1756e9af..48aa51b2 100644 --- a/src/builder/src/open_converter.ui +++ b/src/builder/src/open_converter.ui @@ -166,6 +166,19 @@ QLabel { Chinese + + + true + + + Enable Log File + + + + + About OpenConverter + + diff --git a/src/builder/src/shared_data.cpp b/src/builder/src/shared_data.cpp index 977cda88..4c24d362 100644 --- a/src/builder/src/shared_data.cpp +++ b/src/builder/src/shared_data.cpp @@ -33,6 +33,9 @@ QString SharedData::GetInputFilePath() const { void SharedData::SetInputFilePath(const QString &filePath) { inputFilePath = filePath; + // Track last directory for file dialogs + if (!filePath.isEmpty()) + lastDirectory = QFileInfo(filePath).absolutePath(); // When input path changes, reset output path to auto-generate mode if (!outputPathManuallySet) { outputFilePath = ""; @@ -84,5 +87,14 @@ void SharedData::ResetOutputPath() { void SharedData::Clear() { inputFilePath = ""; outputFilePath = ""; + lastDirectory = ""; outputPathManuallySet = false; } + +QString SharedData::GetLastDirectory() const { + return lastDirectory; +} + +void SharedData::SetLastDirectory(const QString &dir) { + lastDirectory = dir; +} diff --git a/src/common/include/logger.h b/src/common/include/logger.h new file mode 100644 index 00000000..bece2715 --- /dev/null +++ b/src/common/include/logger.h @@ -0,0 +1,58 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include + +extern "C" { +#include +} + +class Logger { +public: + static Logger& Instance(); + + // Set the full path for the log file (must be called before SetEnabled(true)) + void SetLogPath(const std::string& path); + std::string GetLogPath() const; + + // Enable or disable file logging + void SetEnabled(bool enabled); + bool IsEnabled() const; + + // Non-copyable singleton + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + +private: + Logger(); + ~Logger(); + + static void AvLogCallback(void* avcl, int level, const char* fmt, va_list vl); + static const char* LevelToString(int level); + + bool m_enabled = false; + int m_minLevel = AV_LOG_DEBUG; // reserved: future per-level filtering + std::string m_logPath; + std::ofstream m_logFile; + void (*m_originalCallback)(void*, int, const char*, va_list) = nullptr; +}; + +#endif // LOGGER_H diff --git a/src/common/src/logger.cpp b/src/common/src/logger.cpp new file mode 100644 index 00000000..c67624f4 --- /dev/null +++ b/src/common/src/logger.cpp @@ -0,0 +1,106 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "logger.h" + +#include +#include +#include + +extern "C" { +#include +} + +Logger& Logger::Instance() { + static Logger instance; + return instance; +} + +Logger::Logger() { + m_originalCallback = av_log_default_callback; +} + +Logger::~Logger() { + SetEnabled(false); +} + +void Logger::SetLogPath(const std::string& path) { + m_logPath = path; +} + +std::string Logger::GetLogPath() const { + return m_logPath; +} + +bool Logger::IsEnabled() const { + return m_enabled; +} + +void Logger::SetEnabled(bool enabled) { + if (enabled == m_enabled) + return; + + if (enabled) { + m_logFile.open(m_logPath, std::ios::app); + if (!m_logFile.is_open()) + return; // path not set or unwritable — stay disabled + m_enabled = true; + av_log_set_callback(&Logger::AvLogCallback); + } else { + m_enabled = false; + av_log_set_callback(m_originalCallback); + if (m_logFile.is_open()) + m_logFile.close(); + } +} + +const char* Logger::LevelToString(int level) { + if (level <= AV_LOG_FATAL) return "FATAL"; + if (level <= AV_LOG_ERROR) return "ERROR"; + if (level <= AV_LOG_WARNING) return "WARNING"; + if (level <= AV_LOG_INFO) return "INFO"; + return "DEBUG"; +} + +void Logger::AvLogCallback(void* avcl, int level, const char* fmt, va_list vl) { + Logger& logger = Instance(); + + // Copy va_list for our use; forward the original to the saved callback + va_list vl_copy; + va_copy(vl_copy, vl); + + char buf[1024]; + vsnprintf(buf, sizeof(buf), fmt, vl_copy); + va_end(vl_copy); + + // Timestamp + auto now = std::chrono::system_clock::now(); + std::time_t t = std::chrono::system_clock::to_time_t(now); + std::tm* tm_info = std::localtime(&t); + char timeBuf[32]; + std::strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", tm_info); + + if (logger.m_logFile.is_open()) { + logger.m_logFile << "[" << timeBuf << "] [" + << LevelToString(level) << "] " << buf; + logger.m_logFile.flush(); + } + + // Forward to original callback so console output still works + if (logger.m_originalCallback) + logger.m_originalCallback(avcl, level, fmt, vl); +} diff --git a/src/resources/linglong.yaml b/src/resources/linglong.yaml index 4f92792f..3747ccee 100644 --- a/src/resources/linglong.yaml +++ b/src/resources/linglong.yaml @@ -3,7 +3,7 @@ version: "1" package: id: io.github.openconverterlab name: "OpenConverter" - version: 1.5.4.0 + version: 1.5.5.0 kind: app description: | OpenConverter diff --git a/src/resources/macos_launcher.sh b/src/resources/macos_launcher.sh index d22a8c39..53fe14e0 100644 --- a/src/resources/macos_launcher.sh +++ b/src/resources/macos_launcher.sh @@ -10,4 +10,3 @@ export DYLD_LIBRARY_PATH="$HOME/Library/Application Support/OpenConverter/Python # Execute the real binary exec "$SCRIPT_DIR/OpenConverter.real" "$@" - diff --git a/src/tests/transcoder_test.cpp b/src/tests/transcoder_test.cpp index b297eb8a..52950c48 100644 --- a/src/tests/transcoder_test.cpp +++ b/src/tests/transcoder_test.cpp @@ -1,4 +1,5 @@ #include "../common/include/encode_parameter.h" +#include "../common/include/logger.h" #include "../engine/include/converter.h" #include #include @@ -7,6 +8,10 @@ #include #include +extern "C" { +#include +} + // Test fixture for transcoder tests class TranscoderTest : public ::testing::Test { protected: @@ -400,3 +405,55 @@ TEST_F(TranscoderTest, AudioVideoStreamOrder) { EXPECT_TRUE(std::filesystem::exists(outputFile)); EXPECT_GT(std::filesystem::file_size(outputFile), 0); } + +// ── Logger Tests ────────────────────────────────────────────────────────────── + +class LoggerTest : public ::testing::Test { +protected: + std::string logPath; + + void SetUp() override { + logPath = (std::filesystem::temp_directory_path() + / "oc_logger_test.log").string(); + std::filesystem::remove(logPath); + Logger::Instance().SetLogPath(logPath); + Logger::Instance().SetEnabled(false); + } + + void TearDown() override { + Logger::Instance().SetEnabled(false); + std::filesystem::remove(logPath); + } +}; + +TEST_F(LoggerTest, DisabledByDefault) { + EXPECT_FALSE(Logger::Instance().IsEnabled()); +} + +TEST_F(LoggerTest, EnableCreatesLogFile) { + Logger::Instance().SetEnabled(true); + EXPECT_TRUE(Logger::Instance().IsEnabled()); + EXPECT_TRUE(std::filesystem::exists(logPath)); +} + +TEST_F(LoggerTest, DisableClosesFile) { + Logger::Instance().SetEnabled(true); + Logger::Instance().SetEnabled(false); + EXPECT_FALSE(Logger::Instance().IsEnabled()); +} + +TEST_F(LoggerTest, AvLogWritesToFile) { + Logger::Instance().SetEnabled(true); + av_log(nullptr, AV_LOG_ERROR, "unit test error line\n"); + Logger::Instance().SetEnabled(false); + + std::ifstream in(logPath); + std::string contents((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + EXPECT_NE(contents.find("unit test error line"), std::string::npos); + EXPECT_NE(contents.find("[ERROR]"), std::string::npos); +} + +TEST_F(LoggerTest, GetLogPathReturnsSetPath) { + EXPECT_EQ(Logger::Instance().GetLogPath(), logPath); +}