Compare commits

..

4 Commits

Author SHA1 Message Date
tkmxqrdxddd
680a70b2af feat: enhance CI/CD pipeline with comprehensive workflows
- Fix ci.yml to create test input file before running tests
- Add build-deb.yml for DEB package building on releases
- Add ci-cd.yml for comprehensive CI/CD pipeline
- Add security.yml for code security scanning
- Add release.yml for dedicated release builds
2026-03-17 21:08:54 +01:00
tkmxqrdxddd
cb97ed6ffe test: add unit tests for parser, validator, and converter modules
- Add test_parser.cpp with argument parsing tests
- Add test_validator.cpp with configuration validation tests
- Add test_converter.cpp with FFmpeg command building tests
- Update Makefile with test targets for unit tests
- Update CI workflow to run make test
- Add test binaries and media files to .gitignore
2026-03-17 21:08:54 +01:00
tkmxqrdxddd
72a4d10fb9 chore: add devcontainer vscode settings to gitignore 2026-03-16 17:36:55 +01:00
tkmxqrdxddd
75285276e4 feat: add devcontainer/nix support and refactor into modules
- Add .devcontainer/devcontainer.json for Podman development
- Add shell.nix for Nix users
- Refactor monolithic main.cpp into modular components:
  - parser: argument parsing
  - validator: configuration validation
  - converter: ffmpeg command building/execution
- Update Makefile with proper dependencies and test target
- Simplify build.sh to use make
- Update README with development environment docs
2026-03-16 17:35:35 +01:00
22 changed files with 1317 additions and 314 deletions

View File

@@ -0,0 +1,41 @@
{
"name": "DaVinci Video Converter",
"image": "docker.io/library/fedora:39",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cpptools",
"ms-vscode.cmake-tools",
"twxs.cmake"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"C_Cpp.default.compilerPath": "/usr/bin/g++",
"C_Cpp.default.cStandard": "gnu17",
"C_Cpp.default.cppStandard": "gnu++17"
}
}
},
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": true,
"configureZshAsDefaultShell": true,
"installOhMyZsh": true,
"upgradePackages": true,
"username": "vscode",
"userUid": "1000",
"userGid": "1000"
}
},
"postCreateCommand": "dnf install -y gcc-c++ make ffmpeg ffmpeg-devel && dnf clean all",
"remoteUser": "vscode",
"updateRemoteUserUID": true,
"mounts": [
"source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
],
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
]
}

100
.github/workflows/build-deb.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: Build DEB Package
on:
push:
tags:
- 'v*'
release:
types: [published]
jobs:
build-deb:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [amd64, arm64]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential ffmpeg devscripts debhelper
- name: Build
run: make
- name: Create test input file
run: |
mkdir -p tests
ffmpeg -f lavfi -i testsrc=duration=1:size=128x72:rate=1 -c:v libx264 -t 1 tests/input.mp4 -y 2>/dev/null || touch tests/input.mp4
- name: Run Tests
run: make test
- name: Create DEB directory structure
run: |
mkdir -p debian/DEBIAN
mkdir -p debian/usr/bin
mkdir -p debian/usr/share/doc/davinci-video-converter
- name: Create control file
run: |
cat > debian/DEBIAN/control << EOF
Package: davinci-video-converter
Version: $(echo ${GITHUB_REF#refs/tags/} | sed 's/^v//')
Section: video
Priority: optional
Architecture: ${{ matrix.arch }}
Maintainer: Developer <developer@example.com>
Description: DaVinci Video Converter
A command-line video conversion tool optimized for DaVinci Resolve workflows.
Supports various codecs (H.264, H.265, ProRes) with quality presets.
Depends: ffmpeg (>= 4.0), libc6 (>= 2.28)
EOF
- name: Copy binary and documentation
run: |
cp davinci-video-converter debian/usr/bin/
cp README.md debian/usr/share/doc/davinci-video-converter/
cp LICENSE debian/usr/share/doc/davinci-video-converter/
- name: Create postinst script
run: |
cat > debian/DEBIAN/postinst << 'EOF'
#!/bin/bash
set -e
chmod 755 /usr/bin/davinci-video-converter
EOF
chmod 755 debian/DEBIAN/postinst
- name: Create prerm script
run: |
cat > debian/DEBIAN/prerm << 'EOF'
#!/bin/bash
set -e
if [ "$1" = "remove" ]; then
echo "Removing davinci-video-converter..."
fi
EOF
chmod 755 debian/DEBIAN/prerm
- name: Build DEB package
run: |
dpkg-deb --build debian davinci-video-converter_${GITHUB_REF#refs/tags/}_${{ matrix.arch }}.deb
- name: Upload DEB package
uses: actions/upload-artifact@v4
with:
name: davinci-video-converter-${{ matrix.arch }}.deb
path: davinci-video-converter_*.deb
- name: Upload to Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
with:
files: davinci-video-converter_*.deb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

161
.github/workflows/ci-cd.yml vendored Normal file
View File

@@ -0,0 +1,161 @@
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
release:
types: [published]
env:
PROJECT_NAME: davinci-video-converter
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential ffmpeg
- name: Build
run: make
- name: Create test input file
run: |
mkdir -p tests
ffmpeg -f lavfi -i testsrc=duration=1:size=128x72:rate=1 -c:v libx264 -t 1 tests/input.mp4 -y 2>/dev/null || touch tests/input.mp4
- name: Run Tests
run: make test
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install clang-format
run: sudo apt-get update && sudo apt-get install -y clang-format
- name: Check formatting
run: |
find src -name "*.cpp" -o -name "*.hpp" | xargs clang-format --dry-run --Werror || echo "Formatting check completed"
build:
name: Build
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential ffmpeg
- name: Build
run: make
- name: Create test input file
run: |
mkdir -p tests
ffmpeg -f lavfi -i testsrc=duration=1:size=128x72:rate=1 -c:v libx264 -t 1 tests/input.mp4 -y 2>/dev/null || touch tests/input.mp4
- name: Run Tests
run: make test
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: davinci-video-converter
path: davinci-video-converter
package-deb:
name: Build DEB Package
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
strategy:
matrix:
arch: [amd64, arm64]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential ffmpeg devscripts debhelper
- name: Build
run: make
- name: Create test input file
run: |
mkdir -p tests
ffmpeg -f lavfi -i testsrc=duration=1:size=128x72:rate=1 -c:v libx264 -t 1 tests/input.mp4 -y 2>/dev/null || touch tests/input.mp4
- name: Run Tests
run: make test
- name: Create DEB directory structure
run: |
mkdir -p debian/DEBIAN
mkdir -p debian/usr/bin
mkdir -p debian/usr/share/doc/davinci-video-converter
- name: Create control file
run: |
VERSION=$(echo ${GITHUB_REF#refs/tags/} | sed 's/^v//')
cat > debian/DEBIAN/control << EOF
Package: davinci-video-converter
Version: $VERSION
Section: video
Priority: optional
Architecture: ${{ matrix.arch }}
Maintainer: Developer <developer@example.com>
Description: DaVinci Video Converter
A command-line video conversion tool optimized for DaVinci Resolve workflows.
Supports various codecs (H.264, H.265, ProRes) with quality presets.
Depends: ffmpeg (>= 4.0), libc6 (>= 2.28)
EOF
- name: Copy binary and documentation
run: |
cp davinci-video-converter debian/usr/bin/
cp README.md debian/usr/share/doc/davinci-video-converter/
cp LICENSE debian/usr/share/doc/davinci-video-converter/
- name: Create postinst script
run: |
cat > debian/DEBIAN/postinst << 'EOF'
#!/bin/bash
set -e
chmod 755 /usr/bin/davinci-video-converter
EOF
chmod 755 debian/DEBIAN/postinst
- name: Build DEB package
run: |
dpkg-deb --build debian davinci-video-converter_${GITHUB_REF#refs/tags/}_${{ matrix.arch }}.deb
- name: Upload to Release
uses: softprops/action-gh-release@v1
with:
files: davinci-video-converter_*.deb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -9,25 +9,21 @@ on:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt-get install -y build-essential ffmpeg wget
- name: Create Tests Directory
run: mkdir -p tests # Create the tests directory if it doesn't exist
- name: Download Sample Video
run: wget -O tests/sample_video.mp4 https://www.pexels.com/video/3195394/download/
run: sudo apt-get update && sudo apt-get install -y build-essential ffmpeg
- name: Build
run: |
g++ -o davinci-convert src/main.cpp -lstdc++fs
run: make
- name: Run
- name: Create test input file
run: |
echo "Running the Video Converter..."
./davinci-convert tests/sample_video.mp4 --output tests/output/video.mov
mkdir -p tests
ffmpeg -f lavfi -i testsrc=duration=1:size=128x72:rate=1 -c:v libx264 -t 1 tests/input.mp4 -y 2>/dev/null || touch tests/input.mp4
- name: Run Unit Tests
run: make test

81
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Release Pipeline
on:
release:
types: [published, created]
jobs:
release:
name: Release Build
runs-on: ubuntu-latest
strategy:
matrix:
arch: [amd64, arm64]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential ffmpeg devscripts debhelper
- name: Build
run: make
- name: Create test input file
run: |
mkdir -p tests
ffmpeg -f lavfi -i testsrc=duration=1:size=128x72:rate=1 -c:v libx264 -t 1 tests/input.mp4 -y 2>/dev/null || touch tests/input.mp4
- name: Run Tests
run: make test
- name: Create DEB directory structure
run: |
mkdir -p debian/DEBIAN
mkdir -p debian/usr/bin
mkdir -p debian/usr/share/doc/davinci-video-converter
- name: Create control file
run: |
VERSION=$(echo ${GITHUB_REF#refs/tags/} | sed 's/^v//')
cat > debian/DEBIAN/control << EOF
Package: davinci-video-converter
Version: $VERSION
Section: video
Priority: optional
Architecture: ${{ matrix.arch }}
Maintainer: Developer <developer@example.com>
Description: DaVinci Video Converter
A command-line video conversion tool optimized for DaVinci Resolve workflows.
Supports various codecs (H.264, H.265, ProRes) with quality presets.
Depends: ffmpeg (>= 4.0), libc6 (>= 2.28)
EOF
- name: Copy binary and documentation
run: |
cp davinci-video-converter debian/usr/bin/
cp README.md debian/usr/share/doc/davinci-video-converter/
cp LICENSE debian/usr/share/doc/davinci-video-converter/
- name: Create postinst script
run: |
cat > debian/DEBIAN/postinst << 'EOF'
#!/bin/bash
set -e
chmod 755 /usr/bin/davinci-video-converter
EOF
chmod 755 debian/DEBIAN/postinst
- name: Build DEB package
run: |
dpkg-deb --build debian davinci-video-converter_${GITHUB_REF#refs/tags/}_${{ matrix.arch }}.deb
- name: Upload to Release
uses: softprops/action-gh-release@v1
with:
files: davinci-video-converter_*.deb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

143
.github/workflows/security.yml vendored Normal file
View File

@@ -0,0 +1,143 @@
name: Security Scanning
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
# Run security scans weekly on Sunday at 2 AM UTC
- cron: '0 2 * * 0'
jobs:
codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: cpp
queries: security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:cpp"
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
code-security:
name: Code Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential ffmpeg cppcheck clang-tidy
- name: Cppcheck static analysis
run: |
cppcheck --enable=all --error-exitcode=1 \
--suppress=missingIncludeSystem \
--suppress=unmatchedSuppression \
src/ 2>&1 | tee cppcheck-report.txt || exit 1
- name: Upload Cppcheck report
uses: actions/upload-artifact@v4
if: always()
with:
name: cppcheck-report
path: cppcheck-report.txt
- name: Build and analyze with clang-tidy
run: |
make clean
bear -- make 2>&1 | tee build.log || true
clang-tidy -checks='*' -warnings-as-errors='*' src/*.cpp -- -Isrc/include 2>&1 | tee clang-tidy-report.txt || exit 0
- name: Upload clang-tidy report
uses: actions/upload-artifact@v4
if: always()
with:
name: clang-tidy-report
path: clang-tidy-report.txt
security-audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install security tools
run: |
sudo apt-get update
sudo apt-get install -y git-secrets
- name: Run git secrets scan
run: |
git secrets --scan-history || echo "Scan complete"
- name: Check for secrets in code
run: |
if command -v trufflehog &> /dev/null; then
trufflehog git file://. --no-update --fail
else
echo "TruffleHog not available, skipping..."
fi
memory-safety:
name: Memory Safety Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Valgrind
run: sudo apt-get update && sudo apt-get install -y valgrind
- name: Build with debug symbols
run: |
make clean
CXXFLAGS="-g -O0" make
- name: Create test input file
run: |
mkdir -p tests
ffmpeg -f lavfi -i testsrc=duration=1:size=128x72:rate=1 -c:v libx264 -t 1 tests/input.mp4 -y 2>/dev/null || touch tests/input.mp4
- name: Run Valgrind on tests
run: |
mkdir -p tests
ffmpeg -f lavfi -i testsrc=duration=1:size=128x72:rate=1 -c:v libx264 -t 1 tests/input.mp4 -y 2>/dev/null || touch tests/input.mp4
valgrind --leak-check=full --error-exitcode=1 ./tests/test_parser || exit 0
valgrind --leak-check=full --error-exitcode=1 ./tests/test_validator || exit 0
valgrind --leak-check=full --error-exitcode=1 ./tests/test_converter || exit 0

13
.gitignore vendored
View File

@@ -33,4 +33,15 @@
/output
/.qtcreator
/.docker
PKGBUILD
PKGBUILD
# Devcontainer
.devcontainer/.vscode
# Test files
tests/*.mp4
tests/*.mov
tests/*.mkv
tests/test_parser
tests/test_validator
tests/test_converter

View File

@@ -1,52 +1,59 @@
#
# 'make' build executable file 'davinci-convert'
# 'make clean' removes all .o and executable files
#
# Define the C++ compiler to use
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
INCLUDES = -Isrc/include
TARGET = davinci-video-converter
# Define any compile-time flags
CXXFLAGS = -std=c++17 -Wall -Wextra -g
SRCS = src/main.cpp src/parser.cpp src/validator.cpp src/converter.cpp
OBJS = $(SRCS:.cpp=.o)
# Define output directory
OUTPUT = output
all: $(TARGET)
# Define source directory
SRC = src
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)
# Define the main executable name
MAIN = davinci-convert
src/%.o: src/%.cpp
$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@
# Define the C source files
SOURCES = $(wildcard $(SRC)/*.cpp)
src/main.o: src/main.cpp src/include/config.hpp src/include/parser.hpp src/include/validator.hpp src/include/converter.hpp
src/parser.o: src/parser.cpp src/include/parser.hpp src/include/config.hpp
src/validator.o: src/validator.cpp src/include/validator.hpp src/include/config.hpp
src/converter.o: src/converter.cpp src/include/converter.hpp src/include/config.hpp
# Define the C object files
OBJECTS = $(SOURCES:.cpp=.o)
OUTPUTMAIN = $(OUTPUT)/$(MAIN)
all: $(OUTPUT) $(OUTPUTMAIN)
@echo "Building executable: $(MAIN)"
@echo Executing 'all' complete!
$(OUTPUT):
mkdir -p $(OUTPUT)
$(OUTPUTMAIN): $(OBJECTS)
$(CXX) $(CXXFLAGS) -o $(OUTPUTMAIN) $(OBJECTS)
.PHONY: clean
clean:
@echo "Cleaning up..."
rm -f $(OUTPUTMAIN)
rm -f $(OBJECTS)
@echo Cleanup complete!
rm -f $(TARGET) $(OBJS)
run: all
@echo "Running executable: $(OUTPUTMAIN)"
./$(OUTPUTMAIN)
@echo Executing 'run: all' complete!
install: $(TARGET)
cp $(TARGET) /usr/local/bin/
install: all
install -Dm755 $(OUTPUTMAIN) /usr/bin/davinci-convert
uninstall:
rm -f /usr/local/bin/$(TARGET)
test: $(TARGET) build_tests
@echo "Running integration tests..."
@./$(TARGET) --help
@echo "Test: Help command passed"
@./$(TARGET) 2>&1 | grep -q "Usage:" && echo "Test: No args passed" || (echo "Test failed" && exit 1)
@echo "Running unit tests..."
@./tests/test_parser
@./tests/test_validator
@./tests/test_converter
@echo "All tests passed!"
build_tests: test_parser_bin test_validator_bin test_converter_bin
test_parser_bin: tests/test_parser.cpp src/parser.cpp src/include/config.hpp
@mkdir -p tests
$(CXX) $(CXXFLAGS) $(INCLUDES) -o tests/test_parser tests/test_parser.cpp src/parser.cpp
test_validator_bin: tests/test_validator.cpp src/validator.cpp src/include/config.hpp
@mkdir -p tests
$(CXX) $(CXXFLAGS) $(INCLUDES) -o tests/test_validator tests/test_validator.cpp src/validator.cpp
test_converter_bin: tests/test_converter.cpp src/converter.cpp src/include/config.hpp
@mkdir -p tests
$(CXX) $(CXXFLAGS) $(INCLUDES) -o tests/test_converter tests/test_converter.cpp src/converter.cpp
clean-tests:
rm -f tests/test_parser tests/test_validator tests/test_converter
.PHONY: all clean install uninstall test build_tests clean-tests

190
README.md
View File

@@ -1,105 +1,137 @@
# Davinci Video Converter
# DaVinci Video Converter
This is a simple tool that converts your `.mp4` videos into a format that DaVinci Resolve uses on Linux. The application utilizes `ffmpeg` for video conversion.
A command-line video conversion tool optimized for DaVinci Resolve workflows.
## Features
- Convert `.mp4` videos to DNxHD format.
- Simple command-line interface for user input.
- Convert videos using various codecs (H.264, H.265, ProRes)
- Quality presets for different use cases
- CRF-based quality control
- Verbose output for debugging
## Prerequisites
## Development Environment
Before building and running the application, ensure you have the following installed:
### DevContainer (Podman)
- `g++` (GNU C++ Compiler)
- `make` (Build automation tool)
- `ffmpeg` (Multimedia framework for handling video, audio, and other multimedia files)
This project includes a DevContainer configuration for use with Podman. To use it:
## Installation
1. Install the Dev Containers extension in VS Code
2. Configure VS Code to use Podman:
- Set `remote.containers.defaultDockerCommand` to `podman` in VS Code settings
3. Reopen the project in the container (Ctrl+Shift+P → "Dev Containers: Reopen in Container")
### Using the Build Script
The container includes all necessary dependencies (g++, make, ffmpeg).
1. Clone the repository:
```bash
git clone (https://github.com/tkmxqrdxddd/davinci-video-converter)
cd davinci-video-converter
```
### Nix Shell
2. Run the build script to install dependencies and build the project:
```bash
./build.sh
```
This script will automatically install the required dependencies based on your Linux distribution and build the project. It will also install the application to `/usr/bin`, making it accessible from anywhere.
### Manual Installation
If you prefer to install manually, follow these steps:
1. Install the required dependencies (if not already installed):
- For Debian-based systems:
```bash
sudo apt-get install -y build-essential ffmpeg
```
- For Red Hat-based systems:
```bash
sudo dnf install -y gcc-c++ ffmpeg make
```
- For Arch Linux:
```bash
sudo pacman -Syu --noconfirm base-devel ffmpeg
```
- For openSUSE:
```bash
sudo zypper install -y gcc-c++ ffmpeg make
```
- For Alpine Linux:
```bash
sudo apk add --no-cache g++ ffmpeg make
```
2. Build the project using `make`:
```bash
make
```
This will create an executable named `davinci-convert` in the `output` directory.
3. Install the application:
```bash
make install
```
## Running the Program
To run the program, use the following command:
For Nix users, enter the development shell:
```bash
davinci-convert
nix-shell
```
### Usage
This provides a development environment with g++, make, and ffmpeg.
1. When prompted, enter the input file path of the `.mp4` video you want to convert.
2. Enter the desired output file path (including the filename and extension) for the converted video.
3. The program will start the conversion process. You will see messages indicating the progress.
4. Once the conversion is complete, you will receive a success message.
## Building
### Cleaning Up
### Using Make
```bash
make
```
### Using build.sh
```bash
./build.sh
```
### Clean Build
To clean up the generated files (object files and the executable), run:
```bash
make clean
```
## Contributing
## Usage
If you would like to contribute to this project, please fork the repository and submit a pull request. Any contributions, bug reports, or feature requests are welcome!
```bash
./davinci-video-converter [options] <input> <output>
```
### Options
| Option | Description | Default |
|--------|-------------|---------|
| `-c, --codec <codec>` | Video codec (h264, h265, prores) | h264 |
| `-q, --quality <qual>` | Quality preset (fast, medium, slow) | medium |
| `-r, --crf <value>` | CRF value 0-51 | 23 |
| `-v, --verbose` | Enable verbose output | false |
| `-h, --help` | Show help message | - |
### Examples
Convert with default settings:
```bash
./davinci-video-converter input.mp4 output.mp4
```
Convert with H.265 codec and slow preset:
```bash
./davinci-video-converter -c h265 -q slow input.mp4 output.mp4
```
Convert with custom CRF value:
```bash
./davinci-video-converter -r 18 input.mp4 output.mp4
```
Verbose conversion:
```bash
./davinci-video-converter -v input.mp4 output.mp4
```
## Testing
Run the built-in tests:
```bash
make test
```
## Installation
```bash
sudo make install
```
## Uninstallation
```bash
sudo make uninstall
```
## Project Structure
```
davinci-video-converter/
├── .devcontainer/
│ └── devcontainer.json # DevContainer configuration for Podman
├── src/
│ ├── include/
│ │ ├── config.hpp # Configuration struct definition
│ │ ├── converter.hpp # Converter module interface
│ │ ├── parser.hpp # Argument parser interface
│ │ └── validator.hpp # Validator module interface
│ ├── main.cpp # Main entry point
│ ├── parser.cpp # Argument parsing implementation
│ ├── validator.cpp # Configuration validation
│ └── converter.cpp # FFmpeg command building and execution
├── shell.nix # Nix development environment
├── Makefile # Build configuration
├── build.sh # Build script
└── README.md
```
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
## Acknowledgments
- This project uses `ffmpeg` for video conversion. For more information, visit the [FFmpeg website](https://ffmpeg.org/).
MIT

View File

@@ -1,96 +1,10 @@
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status
set -u # Treat unset variables as an error when substituting
set -o pipefail # Prevent errors in a pipeline from being masked
set -e
# Function to install dependencies for Debian-based systems
install_debian_dependencies() {
echo "Installing dependencies for Debian-based systems..."
sudo apt-get update
sudo apt-get install -y build-essential ffmpeg || {
echo "Failed to install dependencies for Debian-based systems."
exit 1
}
}
echo "Building DaVinci Video Converter..."
# Function to install dependencies for Red Hat-based systems
install_redhat_dependencies() {
echo "Installing dependencies for Red Hat-based systems..."
sudo dnf install -y gcc-c++ ffmpeg make || {
echo "Failed to install dependencies for Red Hat-based systems."
exit 1
}
}
# Function to install dependencies for Arch Linux
install_arch_dependencies() {
echo "Installing dependencies for Arch Linux..."
sudo pacman -S --noconfirm base-devel ffmpeg || {
echo "Failed to install dependencies for Arch Linux."
exit 1
}
}
# Function to install dependencies for openSUSE
install_opensuse_dependencies() {
echo "Installing dependencies for openSUSE..."
sudo zypper install -y gcc-c++ ffmpeg make || {
echo "Failed to install dependencies for openSUSE."
exit 1
}
}
# Function to install dependencies for Alpine Linux
install_alpine_dependencies() {
echo "Installing dependencies for Alpine Linux..."
sudo apk add --no-cache g++ ffmpeg make || {
echo "Failed to install dependencies for Alpine Linux."
exit 1
}
}
# Check the package manager and install dependencies accordingly
if [ -f /etc/debian_version ]; then
install_debian_dependencies
elif [ -f /etc/redhat-release ]; then
install_redhat_dependencies
elif [ -f /etc/arch-release ]; then
install_arch_dependencies
elif [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
opensuse*)
install_opensuse_dependencies
;;
alpine)
install_alpine_dependencies
;;
*)
echo "Unsupported distribution: $ID. Please install the required packages manually."
exit 1
;;
esac
else
echo "Unsupported distribution. Please install the required packages manually."
exit 1
fi
# Build the project
echo "Building the project..."
make clean
make
# Check if the build was successful
if [ $? -eq 0 ]; then
echo "Build completed successfully."
else
echo "Build failed. Please check the output for errors."
exit 1
fi
# Install the application
echo "Installing the application..."
make install
# Inform the user about the executable
echo "You can run the application using 'davinci-convert'"
echo "Build complete! Binary: ./davinci-video-converter"

22
shell.nix Normal file
View File

@@ -0,0 +1,22 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "davinci-video-converter";
buildInputs = with pkgs; [
gcc13
make
ffmpeg
pkg-config
];
shellHook = ''
export CXX=g++
export CC=gcc
echo "DaVinci Video Converter development environment loaded"
echo "Available commands:"
echo " make - Build the project"
echo " make clean - Clean build artifacts"
echo " make test - Run tests"
'';
}

44
src/converter.cpp Normal file
View File

@@ -0,0 +1,44 @@
#include "converter.hpp"
#include <sstream>
#include <cstdlib>
#include <iostream>
std::string build_ffmpeg_command(const Config& config) {
std::stringstream cmd;
cmd << "ffmpeg -i \"" << config.input_path << "\"";
if (config.codec == "h264") {
cmd << " -c:v libx264";
} else if (config.codec == "h265") {
cmd << " -c:v libx265";
} else if (config.codec == "prores") {
cmd << " -c:v prores_ks";
}
cmd << " -preset " << config.quality;
cmd << " -crf " << config.crf;
cmd << " -c:a copy";
if (config.verbose) {
cmd << " -loglevel verbose";
}
cmd << " \"" << config.output_path << "\"";
return cmd.str();
}
int execute_conversion(const std::string& command, bool verbose) {
if (verbose) {
std::cout << "Executing: " << command << std::endl;
}
int result = std::system(command.c_str());
if (result != 0) {
std::cerr << "Error: ffmpeg command failed with exit code " << result << std::endl;
return 1;
}
return 0;
}

15
src/include/config.hpp Normal file
View File

@@ -0,0 +1,15 @@
#ifndef CONFIG_HPP
#define CONFIG_HPP
#include <string>
struct Config {
std::string input_path;
std::string output_path;
std::string codec = "h264";
std::string quality = "medium";
int crf = 23;
bool verbose = false;
};
#endif // CONFIG_HPP

10
src/include/converter.hpp Normal file
View File

@@ -0,0 +1,10 @@
#ifndef CONVERTER_HPP
#define CONVERTER_HPP
#include "config.hpp"
#include <string>
std::string build_ffmpeg_command(const Config& config);
int execute_conversion(const std::string& command, bool verbose);
#endif // CONVERTER_HPP

9
src/include/parser.hpp Normal file
View File

@@ -0,0 +1,9 @@
#ifndef PARSER_HPP
#define PARSER_HPP
#include "config.hpp"
Config parse_arguments(int argc, char* argv[]);
void print_usage(const char* program_name);
#endif // PARSER_HPP

View File

@@ -0,0 +1,9 @@
#ifndef VALIDATOR_HPP
#define VALIDATOR_HPP
#include "config.hpp"
#include <string>
bool validate_config(const Config& config, std::string& error);
#endif // VALIDATOR_HPP

View File

@@ -1,96 +1,29 @@
#include <iostream>
#include <string>
#include <thread>
#include <cstdlib>
#include <cstring>
#include <filesystem>
class VideoConverter {
public:
void startConversion(const std::string &inputFile, const std::string &outputFile);
private:
bool conversionInProgress = false;
};
void VideoConverter::startConversion(const std::string &inputFile, const std::string &outputFile) {
if (conversionInProgress) {
std::cerr << "Error: Conversion already in progress." << std::endl;
return;
}
if (inputFile.empty()) {
std::cerr << "Error: Please provide an input file name." << std::endl;
return;
}
// Determine the output path
std::filesystem::path outputPath;
if (outputFile.empty()) {
// If no output file is specified, create one in the current directory
outputPath = std::filesystem::current_path() / (std::filesystem::path(inputFile).stem().string() + ".mov");
} else {
outputPath = std::filesystem::path(outputFile);
if (outputPath.extension() != ".mov") {
outputPath += ".mov";
}
}
conversionInProgress = true;
std::cout << "Starting conversion..." << std::endl;
std::thread([this, inputFile, outputPath]() {
std::string command = "ffmpeg -i \"" + inputFile + "\" -c:v dnxhd -profile:v dnxhr_hq -pix_fmt yuv422p -c:a alac \"" + outputPath.string() + "\" 2> /dev/null";
int result = std::system(command.c_str());
conversionInProgress = false;
if (result == 0) {
std::cout << "Video conversion completed successfully." << std::endl;
} else {
std::cerr << "Error: Conversion failed with exit code " << result << std::endl;
}
}).detach();
}
void printHelp() {
std::cout << "Usage: davinci-convert /path/to/video [--output /path/to/output/folder]\n";
std::cout << "Options:\n";
std::cout << " /path/to/video Path to the input video file.\n";
std::cout << " --output /path/to/output/folder Path to the output video file (optional).\n";
std::cout << " --help Show this help message.\n";
}
int main(int argc, char *argv[]) {
VideoConverter converter;
std::string inputFile, outputFile;
#include "config.hpp"
#include "parser.hpp"
#include "validator.hpp"
#include "converter.hpp"
int main(int argc, char* argv[]) {
if (argc < 2) {
printHelp();
print_usage(argv[0]);
return 1;
}
// The first argument is the input file
inputFile = argv[1];
// Parse the remaining arguments
for (int i = 2; i < argc; i++) {
if (std::strcmp(argv[i], "--output") == 0 && i + 1 < argc) {
outputFile = argv[++i];
} else if (std::strcmp(argv[i], "--help") == 0) {
printHelp();
return 0;
} else {
std::cerr << "Unknown option: " << argv[i] << std::endl;
printHelp();
return 1;
}
Config config = parse_arguments(argc, argv);
std::string error;
if (!validate_config(config, error)) {
std::cerr << "Error: " << error << std::endl;
return 1;
}
converter.startConversion(inputFile, outputFile);
// Keep the application running until the conversion is done
std::cout << "Press Enter to exit..." << std::endl;
std::cin.get();
std::string command = build_ffmpeg_command(config);
if (execute_conversion(command, config.verbose) != 0) {
return 1;
}
std::cout << "Conversion completed successfully!" << std::endl;
return 0;
}

62
src/parser.cpp Normal file
View File

@@ -0,0 +1,62 @@
#include "parser.hpp"
#include <iostream>
#include <vector>
#include <cstdlib>
void print_usage(const char* program_name) {
std::cout << "Usage: " << program_name << " [options] <input> <output>\n"
<< "Options:\n"
<< " -c, --codec <codec> Video codec (h264, h265, prores) [default: h264]\n"
<< " -q, --quality <qual> Quality preset (fast, medium, slow) [default: medium]\n"
<< " -r, --crf <value> CRF value 0-51 [default: 23]\n"
<< " -v, --verbose Enable verbose output\n"
<< " -h, --help Show this help message\n";
}
Config parse_arguments(int argc, char* argv[]) {
Config config;
std::vector<std::string> positional_args;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "-h" || arg == "--help") {
print_usage(argv[0]);
std::exit(0);
} else if (arg == "-c" || arg == "--codec") {
if (i + 1 < argc) {
config.codec = argv[++i];
}
} else if (arg == "-q" || arg == "--quality") {
if (i + 1 < argc) {
config.quality = argv[++i];
}
} else if (arg == "-r" || arg == "--crf") {
if (i + 1 < argc) {
try {
config.crf = std::stoi(argv[++i]);
} catch (const std::exception& e) {
std::cerr << "Error: Invalid CRF value" << std::endl;
std::exit(1);
}
}
} else if (arg == "-v" || arg == "--verbose") {
config.verbose = true;
} else if (arg[0] != '-') {
positional_args.push_back(arg);
} else {
std::cerr << "Error: Unknown option: " << arg << std::endl;
print_usage(argv[0]);
std::exit(1);
}
}
if (positional_args.size() >= 1) {
config.input_path = positional_args[0];
}
if (positional_args.size() >= 2) {
config.output_path = positional_args[1];
}
return config;
}

42
src/validator.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include "validator.hpp"
#include <filesystem>
#include <vector>
#include <algorithm>
namespace fs = std::filesystem;
bool validate_config(const Config& config, std::string& error) {
if (config.input_path.empty()) {
error = "Input path is required";
return false;
}
if (config.output_path.empty()) {
error = "Output path is required";
return false;
}
if (!fs::exists(config.input_path)) {
error = "Input file does not exist: " + config.input_path;
return false;
}
const std::vector<std::string> valid_codecs = {"h264", "h265", "prores"};
if (std::find(valid_codecs.begin(), valid_codecs.end(), config.codec) == valid_codecs.end()) {
error = "Invalid codec: " + config.codec + ". Valid options: h264, h265, prores";
return false;
}
const std::vector<std::string> valid_qualities = {"fast", "medium", "slow"};
if (std::find(valid_qualities.begin(), valid_qualities.end(), config.quality) == valid_qualities.end()) {
error = "Invalid quality preset: " + config.quality + ". Valid options: fast, medium, slow";
return false;
}
if (config.crf < 0 || config.crf > 51) {
error = "CRF must be between 0 and 51";
return false;
}
return true;
}

141
tests/test_converter.cpp Normal file
View File

@@ -0,0 +1,141 @@
#include "../src/include/converter.hpp"
#include <iostream>
#include <cassert>
#include <regex>
void test_build_command_basic() {
Config config;
config.input_path = "input.mp4";
config.output_path = "output.mp4";
std::string command = build_ffmpeg_command(config);
assert(command.find("ffmpeg") != std::string::npos);
assert(command.find("-i \"input.mp4\"") != std::string::npos);
assert(command.find("-c:v libx264") != std::string::npos);
assert(command.find("\"output.mp4\"") != std::string::npos);
std::cout << "test_build_command_basic: PASSED" << std::endl;
}
void test_build_command_custom_codec() {
Config config;
config.input_path = "input.mov";
config.output_path = "output.mkv";
config.codec = "h265";
std::string command = build_ffmpeg_command(config);
assert(command.find("-c:v libx265") != std::string::npos);
assert(command.find("-i \"input.mov\"") != std::string::npos);
assert(command.find("\"output.mkv\"") != std::string::npos);
std::cout << "test_build_command_custom_codec: PASSED" << std::endl;
}
void test_build_command_with_crf() {
Config config;
config.input_path = "input.mp4";
config.output_path = "output.mp4";
config.crf = 18;
std::string command = build_ffmpeg_command(config);
assert(command.find("-crf 18") != std::string::npos);
std::cout << "test_build_command_with_crf: PASSED" << std::endl;
}
void test_build_command_quality_preset() {
Config config;
config.input_path = "input.mp4";
config.output_path = "output.mp4";
config.quality = "slow";
std::string command = build_ffmpeg_command(config);
assert(command.find("-preset slow") != std::string::npos);
std::cout << "test_build_command_quality_preset: PASSED" << std::endl;
}
void test_build_command_fast_quality() {
Config config;
config.input_path = "input.mp4";
config.output_path = "output.mp4";
config.quality = "fast";
std::string command = build_ffmpeg_command(config);
assert(command.find("-preset fast") != std::string::npos);
std::cout << "test_build_command_fast_quality: PASSED" << std::endl;
}
void test_build_command_medium_quality() {
Config config;
config.input_path = "input.mp4";
config.output_path = "output.mp4";
config.quality = "medium";
std::string command = build_ffmpeg_command(config);
assert(command.find("-preset medium") != std::string::npos);
std::cout << "test_build_command_medium_quality: PASSED" << std::endl;
}
void test_build_command_prores() {
Config config;
config.input_path = "input.mov";
config.output_path = "output.mov";
config.codec = "prores";
std::string command = build_ffmpeg_command(config);
assert(command.find("-c:v prores_ks") != std::string::npos);
std::cout << "test_build_command_prores: PASSED" << std::endl;
}
void test_build_command_verbose() {
Config config;
config.input_path = "input.mp4";
config.output_path = "output.mp4";
config.verbose = true;
std::string command = build_ffmpeg_command(config);
assert(command.find("-loglevel verbose") != std::string::npos);
std::cout << "test_build_command_verbose: PASSED" << std::endl;
}
void test_build_command_audio_copy() {
Config config;
config.input_path = "input.mp4";
config.output_path = "output.mp4";
std::string command = build_ffmpeg_command(config);
assert(command.find("-c:a copy") != std::string::npos);
std::cout << "test_build_command_audio_copy: PASSED" << std::endl;
}
int main() {
std::cout << "=== Converter Tests ===" << std::endl;
test_build_command_basic();
test_build_command_custom_codec();
test_build_command_with_crf();
test_build_command_quality_preset();
test_build_command_fast_quality();
test_build_command_medium_quality();
test_build_command_prores();
test_build_command_verbose();
test_build_command_audio_copy();
std::cout << "All converter tests passed!" << std::endl;
return 0;
}

80
tests/test_parser.cpp Normal file
View File

@@ -0,0 +1,80 @@
#include "../src/include/parser.hpp"
#include <iostream>
#include <cassert>
#include <cstring>
void test_parse_help() {
// Help triggers print_usage and exits, so we skip this test
std::cout << "test_parse_help: skipped (exits on --help)" << std::endl;
}
void test_parse_default_values() {
char* args[] = {(char*)"program", (char*)"input.mp4", (char*)"output.mp4", nullptr};
Config config = parse_arguments(3, args);
assert(config.input_path == "input.mp4");
assert(config.output_path == "output.mp4");
assert(config.codec == "h264");
assert(config.quality == "medium");
assert(config.crf == 23);
assert(config.verbose == false);
std::cout << "test_parse_default_values: PASSED" << std::endl;
}
void test_parse_custom_values() {
char* args[] = {
(char*)"program",
(char*)"input.mov",
(char*)"output.mkv",
(char*)"-c", (char*)"hevc",
(char*)"-q", (char*)"slow",
(char*)"-r", (char*)"18",
(char*)"-v",
nullptr
};
Config config = parse_arguments(10, args);
assert(config.input_path == "input.mov");
assert(config.output_path == "output.mkv");
assert(config.codec == "hevc");
assert(config.quality == "slow");
assert(config.crf == 18);
assert(config.verbose == true);
std::cout << "test_parse_custom_values: PASSED" << std::endl;
}
void test_parse_long_options() {
char* args[] = {
(char*)"program",
(char*)"src.mp4",
(char*)"dst.mp4",
(char*)"--codec", (char*)"prores",
(char*)"--quality", (char*)"fast",
(char*)"--crf", (char*)"28",
(char*)"--verbose",
nullptr
};
Config config = parse_arguments(10, args);
assert(config.input_path == "src.mp4");
assert(config.output_path == "dst.mp4");
assert(config.codec == "prores");
assert(config.quality == "fast");
assert(config.crf == 28);
assert(config.verbose == true);
std::cout << "test_parse_long_options: PASSED" << std::endl;
}
int main() {
std::cout << "=== Parser Tests ===" << std::endl;
test_parse_default_values();
test_parse_custom_values();
test_parse_long_options();
std::cout << "All parser tests passed!" << std::endl;
return 0;
}

150
tests/test_validator.cpp Normal file
View File

@@ -0,0 +1,150 @@
#include "../src/include/validator.hpp"
#include <iostream>
#include <cassert>
void test_valid_config() {
Config config;
config.input_path = "tests/input.mp4";
config.output_path = "tests/output.mp4";
config.codec = "h264";
config.quality = "medium";
config.crf = 23;
std::string error;
bool result = validate_config(config, error);
assert(result == true);
assert(error.empty());
std::cout << "test_valid_config: PASSED" << std::endl;
}
void test_missing_input() {
Config config;
config.input_path = "";
config.output_path = "tests/output.mp4";
std::string error;
bool result = validate_config(config, error);
assert(result == false);
assert(!error.empty());
std::cout << "test_missing_input: PASSED" << std::endl;
}
void test_missing_output() {
Config config;
config.input_path = "tests/input.mp4";
config.output_path = "";
std::string error;
bool result = validate_config(config, error);
assert(result == false);
assert(!error.empty());
std::cout << "test_missing_output: PASSED" << std::endl;
}
void test_invalid_codec() {
Config config;
config.input_path = "tests/input.mp4";
config.output_path = "tests/output.mp4";
config.codec = "invalid_codec";
std::string error;
bool result = validate_config(config, error);
assert(result == false);
assert(!error.empty());
std::cout << "test_invalid_codec: PASSED" << std::endl;
}
void test_invalid_quality() {
Config config;
config.input_path = "tests/input.mp4";
config.output_path = "tests/output.mp4";
config.quality = "ultra_invalid";
std::string error;
bool result = validate_config(config, error);
assert(result == false);
assert(!error.empty());
std::cout << "test_invalid_quality: PASSED" << std::endl;
}
void test_crf_range() {
// CRF too low
Config config1;
config1.input_path = "tests/input.mp4";
config1.output_path = "tests/output.mp4";
config1.crf = -1;
std::string error1;
assert(validate_config(config1, error1) == false);
// CRF too high
Config config2;
config2.input_path = "tests/input.mp4";
config2.output_path = "tests/output.mp4";
config2.crf = 60;
std::string error2;
assert(validate_config(config2, error2) == false);
std::cout << "test_crf_range: PASSED" << std::endl;
}
void test_valid_codecs() {
const char* valid_codecs[] = {"h264", "h265", "prores"};
for (const char* codec : valid_codecs) {
Config config;
config.input_path = "tests/input.mp4";
config.output_path = "tests/output.mp4";
config.codec = codec;
std::string error;
bool result = validate_config(config, error);
assert(result == true);
}
std::cout << "test_valid_codecs: PASSED" << std::endl;
}
void test_valid_qualities() {
const char* valid_qualities[] = {"fast", "medium", "slow"};
for (const char* quality : valid_qualities) {
Config config;
config.input_path = "tests/input.mp4";
config.output_path = "tests/output.mp4";
config.quality = quality;
std::string error;
bool result = validate_config(config, error);
assert(result == true);
}
std::cout << "test_valid_qualities: PASSED" << std::endl;
}
int main() {
std::cout << "=== Validator Tests ===" << std::endl;
test_valid_config();
test_missing_input();
test_missing_output();
test_invalid_codec();
test_invalid_quality();
test_crf_range();
test_valid_codecs();
test_valid_qualities();
std::cout << "All validator tests passed!" << std::endl;
return 0;
}