Compare commits

..

No commits in common. "main" and "0.2.0" have entirely different histories.
main ... 0.2.0

10 changed files with 80 additions and 390 deletions

View file

@ -2,6 +2,6 @@
name = "cz_conventional_commits" name = "cz_conventional_commits"
tag_format = "$version" tag_format = "$version"
version_scheme = "semver2" version_scheme = "semver2"
version = "0.5.1" version = "0.2.0"
update_changelog_on_bump = true update_changelog_on_bump = true
major_version_zero = true major_version_zero = true

View file

@ -21,26 +21,21 @@ jobs:
with: with:
go-version: '1.21' go-version: '1.21'
- name: Install gox and UPX - name: Install UPX
run: | run: |
go install github.com/mitchellh/gox@latest
wget -O upx.tar.xz https://github.com/upx/upx/releases/download/v5.0.1/upx-5.0.1-amd64_linux.tar.xz wget -O upx.tar.xz https://github.com/upx/upx/releases/download/v5.0.1/upx-5.0.1-amd64_linux.tar.xz
tar -xf upx.tar.xz tar -xf upx.tar.xz
cp upx-5.0.1-amd64_linux/upx /usr/local/bin/ cp upx-5.0.1-amd64_linux/upx /usr/local/bin/
cp upx-5.0.1-amd64_linux/upx.1 /usr/local/share/man/man1/ || true cp upx-5.0.1-amd64_linux/upx.1 /usr/local/share/man/man1/ || true
chmod +x /usr/local/bin/upx chmod +x /usr/local/bin/upx
- name: Build cross-platform binaries - name: Build normal binary
run: | run: |
gox -os="darwin" -os="linux" -os="windows" -arch="amd64" -arch="arm64" -osarch="linux/386" -osarch="windows/386" -output="build/{{.Dir}}-{{.OS}}-{{.Arch}}" go build -o fps-go-brr .
- name: Compress Linux binaries with UPX - name: Build compact binary
run: | run: |
for file in build/*linux*; do ./build-compact.sh
if [ -f "$file" ]; then
upx --brute "$file"
fi
done
- name: Get version - name: Get version
id: version id: version
@ -51,56 +46,23 @@ jobs:
echo "version=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "version=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
fi fi
- name: Create platform bundles - name: Create release directory
run: | run: |
mkdir -p release mkdir -p release
cp fps-go-brr release/fps-go-brr-${{ steps.version.outputs.version }}
cp fps-go-brr-compact release/fps-go-brr-compact-${{ steps.version.outputs.version }}
# Create Darwin (macOS) bundle - name: Upload normal binary
mkdir -p bundle-darwin
cp build/fps-go-brr-darwin-amd64 bundle-darwin/ 2>/dev/null || true
cp build/fps-go-brr-darwin-arm64 bundle-darwin/ 2>/dev/null || true
if [ "$(ls -A bundle-darwin 2>/dev/null)" ]; then
tar -czf release/fps-go-brr-darwin-${{ steps.version.outputs.version }}.tar.gz -C bundle-darwin .
fi
# Create Linux bundle
mkdir -p bundle-linux
cp build/fps-go-brr-linux-amd64 bundle-linux/ 2>/dev/null || true
cp build/fps-go-brr-linux-arm64 bundle-linux/ 2>/dev/null || true
cp build/fps-go-brr-linux-386 bundle-linux/ 2>/dev/null || true
if [ "$(ls -A bundle-linux 2>/dev/null)" ]; then
tar -czf release/fps-go-brr-linux-${{ steps.version.outputs.version }}.tar.gz -C bundle-linux .
fi
# Create Windows bundle
mkdir -p bundle-windows
cp build/fps-go-brr-windows-amd64.exe bundle-windows/ 2>/dev/null || true
cp build/fps-go-brr-windows-arm64.exe bundle-windows/ 2>/dev/null || true
cp build/fps-go-brr-windows-386.exe bundle-windows/ 2>/dev/null || true
if [ "$(ls -A bundle-windows 2>/dev/null)" ]; then
tar -czf release/fps-go-brr-windows-${{ steps.version.outputs.version }}.tar.gz -C bundle-windows .
fi
- name: Upload Darwin bundle
uses: forgejo/upload-artifact@v4 uses: forgejo/upload-artifact@v4
with: with:
name: fps-go-brr-darwin-${{ steps.version.outputs.version }} name: fps-go-brr-normal-${{ steps.version.outputs.version }}
path: release/fps-go-brr-darwin-${{ steps.version.outputs.version }}.tar.gz path: release/fps-go-brr-${{ steps.version.outputs.version }}
if-no-files-found: ignore
- name: Upload Linux bundle - name: Upload compact binary
uses: forgejo/upload-artifact@v4 uses: forgejo/upload-artifact@v4
with: with:
name: fps-go-brr-linux-${{ steps.version.outputs.version }} name: fps-go-brr-compact-${{ steps.version.outputs.version }}
path: release/fps-go-brr-linux-${{ steps.version.outputs.version }}.tar.gz path: release/fps-go-brr-compact-${{ steps.version.outputs.version }}
if-no-files-found: ignore
- name: Upload Windows bundle
uses: forgejo/upload-artifact@v4
with:
name: fps-go-brr-windows-${{ steps.version.outputs.version }}
path: release/fps-go-brr-windows-${{ steps.version.outputs.version }}.tar.gz
if-no-files-found: ignore
- name: Create Release - name: Create Release
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@ -113,8 +75,7 @@ jobs:
## fps-go-brr ${{ steps.version.outputs.version }} ## fps-go-brr ${{ steps.version.outputs.version }}
### Downloads ### Downloads
- `fps-go-brr-darwin-${{ steps.version.outputs.version }}.tar.gz` - macOS builds (amd64, arm64) - `fps-go-brr-${{ steps.version.outputs.version }}` - Normal build
- `fps-go-brr-linux-${{ steps.version.outputs.version }}.tar.gz` - Linux builds (amd64, arm64, 386) - compressed with UPX - `fps-go-brr-compact-${{ steps.version.outputs.version }}` - Compact build (optimized with UPX compression)
- `fps-go-brr-windows-${{ steps.version.outputs.version }}.tar.gz` - Windows builds (amd64, arm64, 386)
Linux binaries are compressed with UPX for smaller size but may have slightly slower startup time due to decompression. The compact build is smaller but may have slightly slower startup time due to decompression.

2
.gitignore vendored
View file

@ -1,3 +1 @@
fps-go-brr fps-go-brr
fps-go-brr-compact
.vscode/settings.json

View file

@ -1,32 +1,3 @@
## 0.5.1 (2025-06-17)
### Fix
- **csv output**: add missing frame width and height headers
## 0.5.0 (2025-06-16)
### Feat
- **main**: add a ton of features - progress bar - frame res measurements - verbosity flag
- add gox build script
### Refactor
- **README**: add resdet info
- **README**: add new lines before lists to fit markdown standard
## 0.4.0 (2025-06-15)
## 0.3.0 (2025-06-15)
### Refactor
- change variable and function names to fit with golang conventions
- remove pointless else statments
- add package string
- **gitignore**: ignore compact build binary
## 0.2.0 (2025-06-14) ## 0.2.0 (2025-06-14)
### Feat ### Feat

View file

@ -43,10 +43,6 @@ go build -o fps-go-brr .
# Optimized compact build (requires UPX) # Optimized compact build (requires UPX)
./build-compact.sh ./build-compact.sh
./fps-go-brr-compact <command> [args] ./fps-go-brr-compact <command> [args]
# Cross-platform build (requires gox)
go install github.com/mitchellh/gox@latest
gox -os="darwin" -os="linux" -os="windows" -arch="amd64" -arch="arm64" -osarch="linux/386" -osarch="windows/386"
``` ```
### Testing ### Testing
@ -61,32 +57,14 @@ go mod download
``` ```
### Release Builds ### Release Builds
- Forgejo Actions automatically build and release cross-platform binaries on tag pushes - Forgejo Actions automatically build and release both normal and compact binaries on tag pushes
- Uses custom runner: `9950x` - Uses custom runner: `9950x`
- Cross-compilation via `gox` for Darwin (macOS), Linux, and Windows - Normal and compact builds are uploaded as separate artifacts
- Multiple architectures: amd64, arm64, and 386 (Linux/Windows only) - UPX compression applied to compact builds for size optimization
- UPX compression applied to Linux builds only using `--brute` flag
- Platform-specific `.tar.gz` bundles for distribution
## Repository Information
- **Main Repository**: https://git.aria.coffee/aria/fps-go-brr (Personal Forgejo instance)
- **Mirror**: https://github.com/BuyMyMojo/fps-go-brr (GitHub - accepts PRs and issues)
- **Dual Licensed**: MIT OR Apache-2.0 (SPDX-License-Identifier: MIT OR Apache-2.0)
- **Copyright**: 2025 Aria, Wicket
### Inspirations
This project draws inspiration from:
- Digital Foundry (YouTube) - Professional video game performance analysis
- Brazil Pixel (YouTube) - Technical video analysis and frame rate studies
- TRDrop (GitHub) - Raw video analysis program for framerate estimation
- Original Python implementation - Early proof-of-concept for frame persistence analysis
## Memories ## Memories
- The forgejo workflow runner is executed as root so it does not need to use root - The forgejo workflow runner is executed as root so it does not need to use root
- Updated workflow to use `gox` for cross-compilation and create platform-specific `.tar.gz` bundles
## CLI Usage ## CLI Usage

View file

@ -12,7 +12,6 @@ A Go CLI tool for video frame analysis and comparison. Analyze frame persistence
- **Configurable Tolerance**: Adjust pixel difference sensitivity for noisy videos - **Configurable Tolerance**: Adjust pixel difference sensitivity for noisy videos
- **Real-time Analysis**: Stream processing for efficient memory usage - **Real-time Analysis**: Stream processing for efficient memory usage
- **Two-pass Architecture**: Accurate frame timing calculations for smooth visualizations - **Two-pass Architecture**: Accurate frame timing calculations for smooth visualizations
- **Resolution Measurements**: Using the [resdet](https://github.com/0x09/resdet) cli to log dynamic resolutions (Requires resdet to be installed seperately)
## Quick Start ## Quick Start
@ -107,7 +106,6 @@ This is an early-stage project. Contributions, bug reports, and feature requests
## Technical Details ## Technical Details
Built with: Built with:
- **CLI Framework**: [urfave/cli/v3](https://github.com/urfave/cli) - **CLI Framework**: [urfave/cli/v3](https://github.com/urfave/cli)
- **Video Processing**: [AlexEidt/Vidio](https://github.com/AlexEidt/Vidio) - **Video Processing**: [AlexEidt/Vidio](https://github.com/AlexEidt/Vidio)
- **Image Processing**: Go standard library - **Image Processing**: Go standard library
@ -126,20 +124,11 @@ This project draws inspiration from:
The goal is to provide similar professional-grade video analysis capabilities for the open-source community. The goal is to provide similar professional-grade video analysis capabilities for the open-source community.
## Note on AI use
The use of AI in this project is minor and just an experiment, all major design decisions and functionality are heavily worked on by humans!
I do hope for future AI tools with ethical models to be avaliable and verifiable in the future!
The testing phase for Claude's coding agent in this repo is finished and it shall not contribute more to the code at the current time.
## License ## License
SPDX-License-Identifier: MIT OR Apache-2.0 SPDX-License-Identifier: MIT OR Apache-2.0
This project is dual-licensed under your choice of: This project is dual-licensed under your choice of:
- MIT License - see [LICENSE.MIT](LICENSE.MIT) file for details - MIT License - see [LICENSE.MIT](LICENSE.MIT) file for details
- Apache License 2.0 - see [LICENSE.Apache-2.0](LICENSE.Apache-2.0) file for details - Apache License 2.0 - see [LICENSE.Apache-2.0](LICENSE.Apache-2.0) file for details

View file

@ -1,3 +0,0 @@
#!/bin/bash
gox -os="darwin" -os="linux" -os="windows" -arch="amd64" -arch="arm64" -osarch="linux/386" -osarch="windows/386"

9
go.mod
View file

@ -4,16 +4,7 @@ go 1.24.3
require ( require (
github.com/AlexEidt/Vidio v1.5.1 // indirect github.com/AlexEidt/Vidio v1.5.1 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/cheggaaa/pb v1.0.29 // indirect
github.com/cheggaaa/pb/v3 v3.1.7 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/urfave/cli/v3 v3.3.3 // indirect github.com/urfave/cli/v3 v3.3.3 // indirect
github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a // indirect github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393 // indirect golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393 // indirect
) )

27
go.sum
View file

@ -1,35 +1,8 @@
github.com/AlexEidt/Vidio v1.5.1 h1:tovwvtgQagUz1vifiL9OeWkg1fP/XUzFazFKh7tFtaE= github.com/AlexEidt/Vidio v1.5.1 h1:tovwvtgQagUz1vifiL9OeWkg1fP/XUzFazFKh7tFtaE=
github.com/AlexEidt/Vidio v1.5.1/go.mod h1:djhIMnWMqPrC3X6nB6ymGX6uWWlgw+VayYGKE1bNwmI= github.com/AlexEidt/Vidio v1.5.1/go.mod h1:djhIMnWMqPrC3X6nB6ymGX6uWWlgw+VayYGKE1bNwmI=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo=
github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30=
github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a h1:00UFliGZl2UciXe8o/2iuEsRQ9u7z0rzDTVzuj6EYY0= github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a h1:00UFliGZl2UciXe8o/2iuEsRQ9u7z0rzDTVzuj6EYY0=
github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY= github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393 h1:0P8IF6+RwCumULxvjp9EtJryUs46MgLIgeHbCt7NU4Q= golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393 h1:0P8IF6+RwCumULxvjp9EtJryUs46MgLIgeHbCt7NU4Q=
golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

286
main.go
View file

@ -1,23 +1,16 @@
// A Go CLI tool for video frame analysis and comparison. Analyze frame persistence, detect dropped frames, and export data for visualization tools like those used by Digital Foundry.
package main package main
import ( import (
"cmp"
"context" "context"
"encoding/csv" "encoding/csv"
"errors"
"fmt" "fmt"
"image" "image"
"image/draw" "image/draw"
"image/png"
"log" "log"
"os" "os"
"os/exec"
"strconv" "strconv"
"strings"
vidio "github.com/AlexEidt/Vidio" vidio "github.com/AlexEidt/Vidio"
"github.com/cheggaaa/pb"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -29,7 +22,7 @@ func main() {
Usage: "Count frames", Usage: "Count frames",
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return countVideoFrames(cmd.Args().First()) return count_video_frames(cmd.Args().First())
}, },
}, },
{ {
@ -46,12 +39,12 @@ func main() {
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
firstFrame, _ := getImageFromFilePath(cmd.StringArg("frame1")) first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
secondFrame, _ := getImageFromFilePath(cmd.StringArg("frame2")) second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
firstRGBA := imageToRGBA(firstFrame) first_rgba := imageToRGBA(first_frame)
secondRGBA := imageToRGBA(secondFrame) second_rgba := imageToRGBA(second_frame)
return compareFrames(firstRGBA, secondRGBA) return compare_frames(first_rgba, second_rgba)
}, },
}, },
{ {
@ -68,12 +61,12 @@ func main() {
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
firstFrame, _ := getImageFromFilePath(cmd.StringArg("frame1")) first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
secondFrame, _ := getImageFromFilePath(cmd.StringArg("frame2")) second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
firstRGBA := imageToRGBA(firstFrame) first_rgba := imageToRGBA(first_frame)
secondRGBA := imageToRGBA(secondFrame) second_rgba := imageToRGBA(second_frame)
return compareFramesAlt(firstRGBA, secondRGBA) return compare_frames_alt(first_rgba, second_rgba)
}, },
}, },
{ {
@ -101,9 +94,9 @@ func main() {
}, },
}, },
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.Uint64Flag{ &cli.Float64Flag{
Name: "tolerance", Name: "tolerance",
Usage: "Pixel difference tolerance (0-255?)", Usage: "Pixel difference tolerance (0-255)",
Value: 0, Value: 0,
}, },
&cli.StringFlag{ &cli.StringFlag{
@ -111,46 +104,11 @@ func main() {
Usage: "Path to CSV file for frame data output", Usage: "Path to CSV file for frame data output",
Value: "", Value: "",
}, },
&cli.BoolFlag{
Name: "resdet",
Usage: "use the resdet cli to measure each frame's resoltion\nWARNING: This will slow the process down by a LOT",
Value: false,
},
&cli.BoolFlag{
Name: "verbose",
Usage: "print out total unique frames for every second of measurements",
Value: false,
},
&cli.BoolFlag{
Name: "testing-log",
Usage: "make a seperate output csv in the same folder that live updates",
Value: false,
},
&cli.IntFlag{
Name: "resdet-minimum-height",
Usage: "minimum possible height of detected image",
Value: 0,
},
&cli.IntFlag{
Name: "resdet-minimum-width",
Usage: "minimum possible width of detected image",
Value: 0,
},
}, },
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
videoPath := cmd.StringArg("video") tolerance := uint64(cmd.Float64("tolerance"))
if videoPath == "" {
return fmt.Errorf("Must provide video path")
}
tolerance := cmd.Uint64("tolerance")
csvOutput := cmd.String("csv-output") csvOutput := cmd.String("csv-output")
return analyzeFramePersistence(cmd.StringArg("video"), tolerance, csvOutput, cmd.Bool("resdet"), cmd.Bool("verbose"), cmd.Int("resdet-minimum-height"), cmd.Int("resdet-minimum-width"), cmd.Bool("testing-log")) return analyzeFramePersistence(cmd.StringArg("video"), tolerance, csvOutput)
}, },
}, },
}, },
@ -161,7 +119,7 @@ func main() {
} }
} }
// countVideoFrames // count_video_frames
// Prints out the total ammount of frames within `video` // Prints out the total ammount of frames within `video`
// //
// Parameters: // Parameters:
@ -169,18 +127,18 @@ func main() {
// //
// Returns: // Returns:
// - error // - error
func countVideoFrames(video string) error { func count_video_frames(video string) error {
log.Default().Print("Trying to open video at: " + video) log.Default().Print("Trying to open video at: " + video)
videoFile, _ := vidio.NewVideo(video) video_file, _ := vidio.NewVideo(video)
count := 0 count := 0
for videoFile.Read() { for video_file.Read() {
count++ count++
} }
log.Default().Println("Video total frames: " + strconv.Itoa(count)) log.Default().Println("Video total frames: " + strconv.Itoa(count))
return nil return nil
} }
func compareFrames(frame1 *image.RGBA, frame2 *image.RGBA) error { func compare_frames(frame1 *image.RGBA, frame2 *image.RGBA) error {
accumError := int64(0) accumError := int64(0)
for i := 0; i < len(frame1.Pix); i++ { for i := 0; i < len(frame1.Pix); i++ {
@ -192,7 +150,7 @@ func compareFrames(frame1 *image.RGBA, frame2 *image.RGBA) error {
return nil return nil
} }
func compareFramesAlt(frame1 *image.RGBA, frame2 *image.RGBA) error { func compare_frames_alt(frame1 *image.RGBA, frame2 *image.RGBA) error {
// diff_frame := image.NewRGBA(frame1.Rect) // diff_frame := image.NewRGBA(frame1.Rect)
accumError := int64(0) accumError := int64(0)
for i := 0; i < len(frame1.Pix); i++ { for i := 0; i < len(frame1.Pix); i++ {
@ -214,10 +172,9 @@ func isDiffUInt8(x, y uint8) bool {
sq := d * d sq := d * d
if sq > 0 { if sq > 0 {
return true return true
} else {
return false
} }
return false
} }
func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool { func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
@ -225,46 +182,45 @@ func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
sq := d * d sq := d * d
if sq > tolerance { if sq > tolerance {
return true return true
} else {
return false
} }
return false
} }
func countUniqueVideoFrames(videoPath1 string, videoPath2 string, minDiff uint64, useSqDiff bool) error { func countUniqueVideoFrames(video_path1 string, video_path2 string, min_diff uint64, use_sq_diff bool) error {
video1, _ := vidio.NewVideo(videoPath1) video1, _ := vidio.NewVideo(video_path1)
video2, _ := vidio.NewVideo(videoPath2) video2, _ := vidio.NewVideo(video_path2)
video1Frame := image.NewRGBA(image.Rect(0, 0, video1.Width(), video1.Height())) video1_frame := image.NewRGBA(image.Rect(0, 0, video1.Width(), video1.Height()))
video2Frame := image.NewRGBA(image.Rect(0, 0, video2.Width(), video2.Height())) video2_frame := image.NewRGBA(image.Rect(0, 0, video2.Width(), video2.Height()))
video1.SetFrameBuffer(video1Frame.Pix) video1.SetFrameBuffer(video1_frame.Pix)
video2.SetFrameBuffer(video2Frame.Pix) video2.SetFrameBuffer(video2_frame.Pix)
totalFrames := 0 total_frames := 0
uniqueFrames := 0 unique_frames := 0
for video1.Read() { for video1.Read() {
totalFrames++ total_frames++
video2.Read() video2.Read()
accumError := uint64(0) accumError := uint64(0)
for i := 0; i < len(video1Frame.Pix); i++ { for i := 0; i < len(video1_frame.Pix); i++ {
if useSqDiff { if use_sq_diff {
if isDiffUInt8WithTolerance(video1Frame.Pix[i], video2Frame.Pix[i], minDiff) { if isDiffUInt8WithTolerance(video1_frame.Pix[i], video2_frame.Pix[i], min_diff) {
accumError++ accumError++
} }
} else { } else {
if isDiffUInt8(video1Frame.Pix[i], video2Frame.Pix[i]) { if isDiffUInt8(video1_frame.Pix[i], video2_frame.Pix[i]) {
accumError++ accumError++
} }
} }
} }
if minDiff <= accumError { if min_diff <= accumError {
uniqueFrames++ unique_frames++
log.Default().Println("[" + strconv.Itoa(totalFrames) + "]Unique frame") log.Default().Println("[" + strconv.Itoa(total_frames) + "]Unique frame")
} else { } else {
log.Default().Println("[" + strconv.Itoa(totalFrames) + "]Non-unique frame") log.Default().Println("[" + strconv.Itoa(total_frames) + "]Non-unique frame")
} }
} }
video1.Close() video1.Close()
video2.Close() video2.Close()
log.Default().Println(strconv.Itoa(uniqueFrames) + "/" + strconv.Itoa(totalFrames) + " are unique!") log.Default().Println(strconv.Itoa(unique_frames) + "/" + strconv.Itoa(total_frames) + " are unique!")
return nil return nil
} }
@ -292,7 +248,7 @@ func getImageFromFilePath(filePath string) (image.Image, error) {
return image, err return image, err
} }
func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput string, toggleResdet bool, verbose bool, minResdetHeight int, minResdetWidth int, liveCSV bool) error { func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput string) error {
video, err := vidio.NewVideo(videoPath) video, err := vidio.NewVideo(videoPath)
if err != nil { if err != nil {
return err return err
@ -312,11 +268,11 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
return fmt.Errorf("failed to create CSV file: %v", err) return fmt.Errorf("failed to create CSV file: %v", err)
} }
defer csvFile.Close() defer csvFile.Close()
csvWriter = csv.NewWriter(csvFile) csvWriter = csv.NewWriter(csvFile)
defer csvWriter.Flush() defer csvWriter.Flush()
err = csvWriter.Write([]string{"frame", "average_fps", "frame_time", "unique_frame_count", "real_frame_time", "frame_width", "frame_height"}) err = csvWriter.Write([]string{"frame", "average_fps", "frame_time", "unique_frame_count", "real_frame_time"})
if err != nil { if err != nil {
return fmt.Errorf("failed to write CSV header: %v", err) return fmt.Errorf("failed to write CSV header: %v", err)
} }
@ -329,13 +285,11 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
effectiveFPS float64 effectiveFPS float64
currentFrameTime float64 currentFrameTime float64
realFrameTime float64 realFrameTime float64
frameWidth int
frameHeight int
} }
var frameAnalysisData []FrameData var frameAnalysisData []FrameData
var uniqueFrameDurations []int // Duration of each unique frame var uniqueFrameDurations []int // Duration of each unique frame
currentFrame := image.NewRGBA(image.Rect(0, 0, video.Width(), video.Height())) currentFrame := image.NewRGBA(image.Rect(0, 0, video.Width(), video.Height()))
previousFrame := image.NewRGBA(image.Rect(0, 0, video.Width(), video.Height())) previousFrame := image.NewRGBA(image.Rect(0, 0, video.Width(), video.Height()))
video.SetFrameBuffer(currentFrame.Pix) video.SetFrameBuffer(currentFrame.Pix)
@ -344,8 +298,6 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
var frameNumber int var frameNumber int
var uniqueFramesPerSecond []int var uniqueFramesPerSecond []int
var framePersistenceDurations []float64 var framePersistenceDurations []float64
var frameWidthMeasurements []int
var frameHeightMeasurements []int
currentSecond := 0 currentSecond := 0
uniqueFramesInCurrentSecond := 0 uniqueFramesInCurrentSecond := 0
@ -355,40 +307,16 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
hasFirstFrame := false hasFirstFrame := false
bar := pb.StartNew(video.Frames())
for video.Read() { for video.Read() {
frameNumber++ frameNumber++
// frame colum will be full of 0s normally, not the worst compromise
frameWidth := 0
frameHeight := 0
// mesure resoltion
if toggleResdet {
var lastFrameWidth int
var lastFrameHeight int
if len(frameWidthMeasurements) != 0 {
lastFrameWidth = frameWidthMeasurements[len(frameWidthMeasurements)-1]
lastFrameHeight = frameHeightMeasurements[len(frameHeightMeasurements)-1]
} else {
lastFrameWidth = currentFrame.Bounds().Max.X
lastFrameHeight = currentFrame.Bounds().Max.X
}
frameWidth, frameHeight = resdet(verbose, currentFrame, frameWidth, frameHeight, minResdetHeight, minResdetHeight, lastFrameWidth, lastFrameHeight)
}
frameWidthMeasurements = append(frameWidthMeasurements, frameWidth)
frameHeightMeasurements = append(frameHeightMeasurements, frameHeight)
if !hasFirstFrame { if !hasFirstFrame {
copy(previousFrame.Pix, currentFrame.Pix) copy(previousFrame.Pix, currentFrame.Pix)
hasFirstFrame = true hasFirstFrame = true
uniqueFramesInCurrentSecond = 1 uniqueFramesInCurrentSecond = 1
totalUniqueFrames = 1 totalUniqueFrames = 1
currentUniqueFrameDuration = 1 currentUniqueFrameDuration = 1
// Store data for first frame // Store data for first frame
currentTime := float64(frameNumber) / fps currentTime := float64(frameNumber) / fps
effectiveFPS := float64(totalUniqueFrames) / currentTime effectiveFPS := float64(totalUniqueFrames) / currentTime
@ -399,8 +327,6 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
effectiveFPS: effectiveFPS, effectiveFPS: effectiveFPS,
currentFrameTime: actualFrameTimeMs, currentFrameTime: actualFrameTimeMs,
realFrameTime: 0, // Will be calculated in second pass realFrameTime: 0, // Will be calculated in second pass
frameWidth: frameWidth,
frameHeight: frameHeight,
}) })
continue continue
} }
@ -430,7 +356,7 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
uniqueFrameDurations[totalUniqueFrames-1] = currentUniqueFrameDuration uniqueFrameDurations[totalUniqueFrames-1] = currentUniqueFrameDuration
} }
} }
if consecutiveDuplicateCount > 1 { if consecutiveDuplicateCount > 1 {
persistenceMs := float64(consecutiveDuplicateCount+1) * frameTimeMs persistenceMs := float64(consecutiveDuplicateCount+1) * frameTimeMs
framePersistenceDurations = append(framePersistenceDurations, persistenceMs) framePersistenceDurations = append(framePersistenceDurations, persistenceMs)
@ -441,7 +367,7 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
uniqueFramesInCurrentSecond++ uniqueFramesInCurrentSecond++
totalUniqueFrames++ totalUniqueFrames++
copy(previousFrame.Pix, currentFrame.Pix) copy(previousFrame.Pix, currentFrame.Pix)
// Start tracking new unique frame // Start tracking new unique frame
currentUniqueFrameDuration = 1 currentUniqueFrameDuration = 1
} }
@ -456,52 +382,17 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
effectiveFPS: effectiveFPS, effectiveFPS: effectiveFPS,
currentFrameTime: actualFrameTimeMs, currentFrameTime: actualFrameTimeMs,
realFrameTime: 0, // Will be calculated in second pass realFrameTime: 0, // Will be calculated in second pass
frameWidth: frameWidth,
frameHeight: frameHeight,
}) })
if verbose { newSecond := int(float64(frameNumber-1) / fps)
newSecond := int(float64(frameNumber-1) / fps) if newSecond > currentSecond {
if newSecond > currentSecond { uniqueFramesPerSecond = append(uniqueFramesPerSecond, uniqueFramesInCurrentSecond)
uniqueFramesPerSecond = append(uniqueFramesPerSecond, uniqueFramesInCurrentSecond) log.Default().Printf("Second %d: %d unique frames", currentSecond+1, uniqueFramesInCurrentSecond)
log.Default().Printf("Second %d: %d unique frames", currentSecond+1, uniqueFramesInCurrentSecond) currentSecond = newSecond
currentSecond = newSecond uniqueFramesInCurrentSecond = 0
uniqueFramesInCurrentSecond = 0
}
} }
if liveCSV {
if _, err := os.Stat("live.csv"); errors.Is(err, os.ErrNotExist) {
f, err := os.Create("live.csv")
if err != nil {
return err
}
_, err = f.WriteString("frame, average_fps, frame_time, unique_frame_count, real_frame_time, frame_width, frame_height\n")
if err != nil {
return err
}
f.Close()
}
f, err := os.OpenFile("live.csv", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
currentFrameData := frameAnalysisData[len(frameAnalysisData)-1]
fmt.Fprintf(f, "%v, %.2f, %.2f, %v, %.2f, %v, %v\n", currentFrameData.frameNumber, currentFrameData.effectiveFPS, currentFrameData.currentFrameTime, currentFrameData.uniqueFrameCount, currentFrameData.realFrameTime, currentFrameData.frameWidth, currentFrameData.frameHeight)
f.Close()
}
bar.Increment()
} }
bar.Finish()
// Record the final unique frame duration // Record the final unique frame duration
if totalUniqueFrames > 0 { if totalUniqueFrames > 0 {
if len(uniqueFrameDurations) < totalUniqueFrames { if len(uniqueFrameDurations) < totalUniqueFrames {
@ -521,8 +412,6 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
fmt.Sprintf("%.2f", frameData.currentFrameTime), fmt.Sprintf("%.2f", frameData.currentFrameTime),
strconv.Itoa(frameData.uniqueFrameCount), strconv.Itoa(frameData.uniqueFrameCount),
fmt.Sprintf("%.2f", realFrameTimeMs), fmt.Sprintf("%.2f", realFrameTimeMs),
strconv.Itoa(frameData.frameWidth),
strconv.Itoa(frameData.frameHeight),
}) })
if err != nil { if err != nil {
log.Default().Printf("Warning: failed to write CSV row %d: %v", i+1, err) log.Default().Printf("Warning: failed to write CSV row %d: %v", i+1, err)
@ -569,62 +458,5 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
log.Default().Printf("No frame persistence detected (all frames are unique)") log.Default().Printf("No frame persistence detected (all frames are unique)")
} }
if len(frameWidthMeasurements) > 0 && len(frameHeightMeasurements) > 0 {
sumWidth := 0
sumHeight := 0
for _, width := range frameWidthMeasurements {
sumWidth += width
}
if sumWidth != 0 {
for _, height := range frameHeightMeasurements {
sumHeight += height
}
avgWidth := float64(sumWidth) / float64(len(frameWidthMeasurements))
avgHeight := float64(sumHeight) / float64(len(frameHeightMeasurements))
log.Default().Printf("Average Width: %.2f", avgWidth)
log.Default().Printf("Average Height: %.2f", avgHeight)
}
}
return nil return nil
} }
func resdet(verbose bool, currentFrame *image.RGBA, frameWidth int, frameHeight int, minHeight int, minWidth int, prevFrameWidth int, prevFrameHeight int) (int, int) {
frameFile, err0 := os.Create("/tmp/frame.png")
err1 := png.Encode(frameFile, currentFrame)
out, err2 := exec.Command("resdet", "-v", "1", frameFile.Name()).Output()
err3 := frameFile.Close()
err4 := os.Remove(frameFile.Name())
formattedOutput := strings.Split(string(out), " ")
frameWidthOut, err5 := strconv.Atoi(formattedOutput[0])
frameHeightOut, err6 := strconv.Atoi(strings.TrimSuffix(formattedOutput[1], "\n"))
if err := cmp.Or(err0, err1, err2, err3, err4, err5, err6); err != nil {
log.Fatal(err)
}
if frameHeightOut > minHeight {
frameHeight = frameHeightOut
} else {
frameHeight = prevFrameHeight
}
if frameWidthOut > minWidth {
frameWidth = frameWidthOut
} else {
frameWidth = prevFrameWidth
}
return frameWidth, frameHeight
}