Compare commits

..

19 commits
0.2.0 ... main

Author SHA1 Message Date
bdfd6cba2e
fix(msin): actually fix the issue... 2025-06-26 18:57:20 +10:00
001c747103
fix(main): index out of range [-1] 2025-06-26 01:18:12 +10:00
55236c0a24
feat(analyze-frame-persistence): add flags for defining min res
This should allow for improved results, at the very least filtering out junk data
2025-06-26 00:12:17 +10:00
fa9aa2e174
bump: version 0.5.0 → 0.5.1
All checks were successful
Build and Release / build (push) Successful in 3m44s
2025-06-17 00:40:42 +10:00
50131d949b
fix(csv output): add missing frame width and height headers 2025-06-17 00:40:24 +10:00
a31c976f38
bump: version 0.4.0 → 0.5.0
All checks were successful
Build and Release / build (push) Successful in 3m37s
2025-06-16 02:20:21 +10:00
08317bd3df
refactor(README): add resdet info 2025-06-16 02:20:04 +10:00
e8742aba21
feat(main): add a ton of features
- progress bar
- frame res measurements
- verbosity flag
2025-06-16 02:17:49 +10:00
801abef51b
refactor(README): add new lines before lists to fit markdown standard 2025-06-16 02:06:59 +10:00
b04cbf006a
feat: add gox build script
All checks were successful
Build and Release / build (push) Successful in 2m50s
2025-06-15 04:39:06 +10:00
7c14b59bb6
bump: version 0.3.0 → 0.4.0
All checks were successful
Build and Release / build (push) Successful in 3m35s
2025-06-15 03:45:36 +10:00
51144a9911
ci: cross compile automatically 2025-06-15 03:45:12 +10:00
c6755cb585
bump: version 0.2.0 → 0.3.0
All checks were successful
Build and Release / build (push) Successful in 1m38s
2025-06-15 03:00:23 +10:00
aec0f14c68
refactor: change variable and function names to fit with golang conventions 2025-06-15 03:00:09 +10:00
3529f7e415
refactor: remove pointless else statments 2025-06-15 02:57:54 +10:00
f9575c92a9
refactor: add package string 2025-06-15 02:57:21 +10:00
1b4cd880ef
chore(gitignore): ignore .vscode settings 2025-06-15 02:54:45 +10:00
8ba0319c6e
refactor(gitignore): ignore compact build binary
All checks were successful
Build and Release / build (push) Successful in 1m22s
2025-06-14 04:12:17 +10:00
db12b8a732
docs(README): add small note about AI use
Some checks failed
Build and Release / build (push) Has been cancelled
2025-06-14 04:11:18 +10:00
10 changed files with 390 additions and 80 deletions

View file

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

View file

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

2
.gitignore vendored
View file

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

View file

@ -1,3 +1,32 @@
## 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)
### Feat

View file

@ -43,6 +43,10 @@ go build -o fps-go-brr .
# Optimized compact build (requires UPX)
./build-compact.sh
./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
@ -57,14 +61,32 @@ go mod download
```
### Release Builds
- Forgejo Actions automatically build and release both normal and compact binaries on tag pushes
- Forgejo Actions automatically build and release cross-platform binaries on tag pushes
- Uses custom runner: `9950x`
- Normal and compact builds are uploaded as separate artifacts
- UPX compression applied to compact builds for size optimization
- Cross-compilation via `gox` for Darwin (macOS), Linux, and Windows
- Multiple architectures: amd64, arm64, and 386 (Linux/Windows only)
- 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
- 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

View file

@ -12,6 +12,7 @@ A Go CLI tool for video frame analysis and comparison. Analyze frame persistence
- **Configurable Tolerance**: Adjust pixel difference sensitivity for noisy videos
- **Real-time Analysis**: Stream processing for efficient memory usage
- **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
@ -106,6 +107,7 @@ This is an early-stage project. Contributions, bug reports, and feature requests
## Technical Details
Built with:
- **CLI Framework**: [urfave/cli/v3](https://github.com/urfave/cli)
- **Video Processing**: [AlexEidt/Vidio](https://github.com/AlexEidt/Vidio)
- **Image Processing**: Go standard library
@ -124,11 +126,20 @@ This project draws inspiration from:
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
SPDX-License-Identifier: MIT OR Apache-2.0
This project is dual-licensed under your choice of:
- 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

3
build-all.sh Executable file
View file

@ -0,0 +1,3 @@
#!/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,7 +4,16 @@ go 1.24.3
require (
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/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
)

27
go.sum
View file

@ -1,8 +1,35 @@
github.com/AlexEidt/Vidio v1.5.1 h1:tovwvtgQagUz1vifiL9OeWkg1fP/XUzFazFKh7tFtaE=
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/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/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/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

286
main.go
View file

@ -1,16 +1,23 @@
// 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
import (
"cmp"
"context"
"encoding/csv"
"errors"
"fmt"
"image"
"image/draw"
"image/png"
"log"
"os"
"os/exec"
"strconv"
"strings"
vidio "github.com/AlexEidt/Vidio"
"github.com/cheggaaa/pb"
"github.com/urfave/cli/v3"
)
@ -22,7 +29,7 @@ func main() {
Usage: "Count frames",
Action: func(ctx context.Context, cmd *cli.Command) error {
return count_video_frames(cmd.Args().First())
return countVideoFrames(cmd.Args().First())
},
},
{
@ -39,12 +46,12 @@ func main() {
Action: func(ctx context.Context, cmd *cli.Command) error {
first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
firstFrame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
secondFrame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
first_rgba := imageToRGBA(first_frame)
second_rgba := imageToRGBA(second_frame)
return compare_frames(first_rgba, second_rgba)
firstRGBA := imageToRGBA(firstFrame)
secondRGBA := imageToRGBA(secondFrame)
return compareFrames(firstRGBA, secondRGBA)
},
},
{
@ -61,12 +68,12 @@ func main() {
Action: func(ctx context.Context, cmd *cli.Command) error {
first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
firstFrame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
secondFrame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
first_rgba := imageToRGBA(first_frame)
second_rgba := imageToRGBA(second_frame)
return compare_frames_alt(first_rgba, second_rgba)
firstRGBA := imageToRGBA(firstFrame)
secondRGBA := imageToRGBA(secondFrame)
return compareFramesAlt(firstRGBA, secondRGBA)
},
},
{
@ -94,9 +101,9 @@ func main() {
},
},
Flags: []cli.Flag{
&cli.Float64Flag{
&cli.Uint64Flag{
Name: "tolerance",
Usage: "Pixel difference tolerance (0-255)",
Usage: "Pixel difference tolerance (0-255?)",
Value: 0,
},
&cli.StringFlag{
@ -104,11 +111,46 @@ func main() {
Usage: "Path to CSV file for frame data output",
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 {
tolerance := uint64(cmd.Float64("tolerance"))
videoPath := cmd.StringArg("video")
if videoPath == "" {
return fmt.Errorf("Must provide video path")
}
tolerance := cmd.Uint64("tolerance")
csvOutput := cmd.String("csv-output")
return analyzeFramePersistence(cmd.StringArg("video"), tolerance, csvOutput)
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"))
},
},
},
@ -119,7 +161,7 @@ func main() {
}
}
// count_video_frames
// countVideoFrames
// Prints out the total ammount of frames within `video`
//
// Parameters:
@ -127,18 +169,18 @@ func main() {
//
// Returns:
// - error
func count_video_frames(video string) error {
func countVideoFrames(video string) error {
log.Default().Print("Trying to open video at: " + video)
video_file, _ := vidio.NewVideo(video)
videoFile, _ := vidio.NewVideo(video)
count := 0
for video_file.Read() {
for videoFile.Read() {
count++
}
log.Default().Println("Video total frames: " + strconv.Itoa(count))
return nil
}
func compare_frames(frame1 *image.RGBA, frame2 *image.RGBA) error {
func compareFrames(frame1 *image.RGBA, frame2 *image.RGBA) error {
accumError := int64(0)
for i := 0; i < len(frame1.Pix); i++ {
@ -150,7 +192,7 @@ func compare_frames(frame1 *image.RGBA, frame2 *image.RGBA) error {
return nil
}
func compare_frames_alt(frame1 *image.RGBA, frame2 *image.RGBA) error {
func compareFramesAlt(frame1 *image.RGBA, frame2 *image.RGBA) error {
// diff_frame := image.NewRGBA(frame1.Rect)
accumError := int64(0)
for i := 0; i < len(frame1.Pix); i++ {
@ -172,9 +214,10 @@ func isDiffUInt8(x, y uint8) bool {
sq := d * d
if sq > 0 {
return true
} else {
return false
}
return false
}
func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
@ -182,45 +225,46 @@ func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
sq := d * d
if sq > tolerance {
return true
} else {
return false
}
return false
}
func countUniqueVideoFrames(video_path1 string, video_path2 string, min_diff uint64, use_sq_diff bool) error {
video1, _ := vidio.NewVideo(video_path1)
video2, _ := vidio.NewVideo(video_path2)
video1_frame := image.NewRGBA(image.Rect(0, 0, video1.Width(), video1.Height()))
video2_frame := image.NewRGBA(image.Rect(0, 0, video2.Width(), video2.Height()))
video1.SetFrameBuffer(video1_frame.Pix)
video2.SetFrameBuffer(video2_frame.Pix)
total_frames := 0
unique_frames := 0
func countUniqueVideoFrames(videoPath1 string, videoPath2 string, minDiff uint64, useSqDiff bool) error {
video1, _ := vidio.NewVideo(videoPath1)
video2, _ := vidio.NewVideo(videoPath2)
video1Frame := image.NewRGBA(image.Rect(0, 0, video1.Width(), video1.Height()))
video2Frame := image.NewRGBA(image.Rect(0, 0, video2.Width(), video2.Height()))
video1.SetFrameBuffer(video1Frame.Pix)
video2.SetFrameBuffer(video2Frame.Pix)
totalFrames := 0
uniqueFrames := 0
for video1.Read() {
total_frames++
totalFrames++
video2.Read()
accumError := uint64(0)
for i := 0; i < len(video1_frame.Pix); i++ {
if use_sq_diff {
if isDiffUInt8WithTolerance(video1_frame.Pix[i], video2_frame.Pix[i], min_diff) {
for i := 0; i < len(video1Frame.Pix); i++ {
if useSqDiff {
if isDiffUInt8WithTolerance(video1Frame.Pix[i], video2Frame.Pix[i], minDiff) {
accumError++
}
} else {
if isDiffUInt8(video1_frame.Pix[i], video2_frame.Pix[i]) {
if isDiffUInt8(video1Frame.Pix[i], video2Frame.Pix[i]) {
accumError++
}
}
}
if min_diff <= accumError {
unique_frames++
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Unique frame")
if minDiff <= accumError {
uniqueFrames++
log.Default().Println("[" + strconv.Itoa(totalFrames) + "]Unique frame")
} else {
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Non-unique frame")
log.Default().Println("[" + strconv.Itoa(totalFrames) + "]Non-unique frame")
}
}
video1.Close()
video2.Close()
log.Default().Println(strconv.Itoa(unique_frames) + "/" + strconv.Itoa(total_frames) + " are unique!")
log.Default().Println(strconv.Itoa(uniqueFrames) + "/" + strconv.Itoa(totalFrames) + " are unique!")
return nil
}
@ -248,7 +292,7 @@ func getImageFromFilePath(filePath string) (image.Image, error) {
return image, err
}
func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput string) error {
func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput string, toggleResdet bool, verbose bool, minResdetHeight int, minResdetWidth int, liveCSV bool) error {
video, err := vidio.NewVideo(videoPath)
if err != nil {
return err
@ -268,11 +312,11 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
return fmt.Errorf("failed to create CSV file: %v", err)
}
defer csvFile.Close()
csvWriter = csv.NewWriter(csvFile)
defer csvWriter.Flush()
err = csvWriter.Write([]string{"frame", "average_fps", "frame_time", "unique_frame_count", "real_frame_time"})
err = csvWriter.Write([]string{"frame", "average_fps", "frame_time", "unique_frame_count", "real_frame_time", "frame_width", "frame_height"})
if err != nil {
return fmt.Errorf("failed to write CSV header: %v", err)
}
@ -285,11 +329,13 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
effectiveFPS float64
currentFrameTime float64
realFrameTime float64
frameWidth int
frameHeight int
}
var frameAnalysisData []FrameData
var uniqueFrameDurations []int // Duration of each unique frame
currentFrame := 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)
@ -298,6 +344,8 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
var frameNumber int
var uniqueFramesPerSecond []int
var framePersistenceDurations []float64
var frameWidthMeasurements []int
var frameHeightMeasurements []int
currentSecond := 0
uniqueFramesInCurrentSecond := 0
@ -307,16 +355,40 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
hasFirstFrame := false
bar := pb.StartNew(video.Frames())
for video.Read() {
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 {
copy(previousFrame.Pix, currentFrame.Pix)
hasFirstFrame = true
uniqueFramesInCurrentSecond = 1
totalUniqueFrames = 1
currentUniqueFrameDuration = 1
// Store data for first frame
currentTime := float64(frameNumber) / fps
effectiveFPS := float64(totalUniqueFrames) / currentTime
@ -327,6 +399,8 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
effectiveFPS: effectiveFPS,
currentFrameTime: actualFrameTimeMs,
realFrameTime: 0, // Will be calculated in second pass
frameWidth: frameWidth,
frameHeight: frameHeight,
})
continue
}
@ -356,7 +430,7 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
uniqueFrameDurations[totalUniqueFrames-1] = currentUniqueFrameDuration
}
}
if consecutiveDuplicateCount > 1 {
persistenceMs := float64(consecutiveDuplicateCount+1) * frameTimeMs
framePersistenceDurations = append(framePersistenceDurations, persistenceMs)
@ -367,7 +441,7 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
uniqueFramesInCurrentSecond++
totalUniqueFrames++
copy(previousFrame.Pix, currentFrame.Pix)
// Start tracking new unique frame
currentUniqueFrameDuration = 1
}
@ -382,17 +456,52 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
effectiveFPS: effectiveFPS,
currentFrameTime: actualFrameTimeMs,
realFrameTime: 0, // Will be calculated in second pass
frameWidth: frameWidth,
frameHeight: frameHeight,
})
newSecond := int(float64(frameNumber-1) / fps)
if newSecond > currentSecond {
uniqueFramesPerSecond = append(uniqueFramesPerSecond, uniqueFramesInCurrentSecond)
log.Default().Printf("Second %d: %d unique frames", currentSecond+1, uniqueFramesInCurrentSecond)
currentSecond = newSecond
uniqueFramesInCurrentSecond = 0
if verbose {
newSecond := int(float64(frameNumber-1) / fps)
if newSecond > currentSecond {
uniqueFramesPerSecond = append(uniqueFramesPerSecond, uniqueFramesInCurrentSecond)
log.Default().Printf("Second %d: %d unique frames", currentSecond+1, uniqueFramesInCurrentSecond)
currentSecond = newSecond
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
if totalUniqueFrames > 0 {
if len(uniqueFrameDurations) < totalUniqueFrames {
@ -412,6 +521,8 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
fmt.Sprintf("%.2f", frameData.currentFrameTime),
strconv.Itoa(frameData.uniqueFrameCount),
fmt.Sprintf("%.2f", realFrameTimeMs),
strconv.Itoa(frameData.frameWidth),
strconv.Itoa(frameData.frameHeight),
})
if err != nil {
log.Default().Printf("Warning: failed to write CSV row %d: %v", i+1, err)
@ -458,5 +569,62 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
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
}
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
}