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"
tag_format = "$version"
version_scheme = "semver2"
version = "0.5.1"
version = "0.2.0"
update_changelog_on_bump = true
major_version_zero = true

View file

@ -21,26 +21,21 @@ jobs:
with:
go-version: '1.21'
- name: Install gox and UPX
- name: Install 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 cross-platform binaries
- name: Build normal binary
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: |
for file in build/*linux*; do
if [ -f "$file" ]; then
upx --brute "$file"
fi
done
./build-compact.sh
- name: Get version
id: version
@ -51,56 +46,23 @@ jobs:
echo "version=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
fi
- name: Create platform bundles
- name: Create release directory
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 }}
# 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
- name: Upload normal binary
uses: forgejo/upload-artifact@v4
with:
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: fps-go-brr-normal-${{ steps.version.outputs.version }}
path: release/fps-go-brr-${{ steps.version.outputs.version }}
- name: Upload Linux bundle
- name: Upload compact binary
uses: forgejo/upload-artifact@v4
with:
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: fps-go-brr-compact-${{ steps.version.outputs.version }}
path: release/fps-go-brr-compact-${{ steps.version.outputs.version }}
- name: Create Release
if: startsWith(github.ref, 'refs/tags/')
@ -113,8 +75,7 @@ jobs:
## fps-go-brr ${{ steps.version.outputs.version }}
### Downloads
- `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)
- `fps-go-brr-${{ steps.version.outputs.version }}` - Normal build
- `fps-go-brr-compact-${{ steps.version.outputs.version }}` - Compact build (optimized with UPX compression)
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-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)
### Feat

View file

@ -43,10 +43,6 @@ 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
@ -61,32 +57,14 @@ go mod download
```
### 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`
- 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
- Normal and compact builds are uploaded as separate artifacts
- UPX compression applied to compact builds for size optimization
## 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,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
- **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
@ -107,7 +106,6 @@ 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
@ -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.
## 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

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 (
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,35 +1,8 @@
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,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
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"
)
@ -29,7 +22,7 @@ func main() {
Usage: "Count frames",
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 {
firstFrame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
secondFrame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
firstRGBA := imageToRGBA(firstFrame)
secondRGBA := imageToRGBA(secondFrame)
return compareFrames(firstRGBA, secondRGBA)
first_rgba := imageToRGBA(first_frame)
second_rgba := imageToRGBA(second_frame)
return compare_frames(first_rgba, second_rgba)
},
},
{
@ -68,12 +61,12 @@ func main() {
Action: func(ctx context.Context, cmd *cli.Command) error {
firstFrame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
secondFrame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
first_frame, _ := getImageFromFilePath(cmd.StringArg("frame1"))
second_frame, _ := getImageFromFilePath(cmd.StringArg("frame2"))
firstRGBA := imageToRGBA(firstFrame)
secondRGBA := imageToRGBA(secondFrame)
return compareFramesAlt(firstRGBA, secondRGBA)
first_rgba := imageToRGBA(first_frame)
second_rgba := imageToRGBA(second_frame)
return compare_frames_alt(first_rgba, second_rgba)
},
},
{
@ -101,9 +94,9 @@ func main() {
},
},
Flags: []cli.Flag{
&cli.Uint64Flag{
&cli.Float64Flag{
Name: "tolerance",
Usage: "Pixel difference tolerance (0-255?)",
Usage: "Pixel difference tolerance (0-255)",
Value: 0,
},
&cli.StringFlag{
@ -111,46 +104,11 @@ 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 {
videoPath := cmd.StringArg("video")
if videoPath == "" {
return fmt.Errorf("Must provide video path")
}
tolerance := cmd.Uint64("tolerance")
tolerance := uint64(cmd.Float64("tolerance"))
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`
//
// Parameters:
@ -169,18 +127,18 @@ func main() {
//
// Returns:
// - error
func countVideoFrames(video string) error {
func count_video_frames(video string) error {
log.Default().Print("Trying to open video at: " + video)
videoFile, _ := vidio.NewVideo(video)
video_file, _ := vidio.NewVideo(video)
count := 0
for videoFile.Read() {
for video_file.Read() {
count++
}
log.Default().Println("Video total frames: " + strconv.Itoa(count))
return nil
}
func compareFrames(frame1 *image.RGBA, frame2 *image.RGBA) error {
func compare_frames(frame1 *image.RGBA, frame2 *image.RGBA) error {
accumError := int64(0)
for i := 0; i < len(frame1.Pix); i++ {
@ -192,7 +150,7 @@ func compareFrames(frame1 *image.RGBA, frame2 *image.RGBA) error {
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)
accumError := int64(0)
for i := 0; i < len(frame1.Pix); i++ {
@ -214,10 +172,9 @@ 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 {
@ -225,46 +182,45 @@ func isDiffUInt8WithTolerance(x, y uint8, tolerance uint64) bool {
sq := d * d
if sq > tolerance {
return true
} else {
return false
}
return false
}
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
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
for video1.Read() {
totalFrames++
total_frames++
video2.Read()
accumError := uint64(0)
for i := 0; i < len(video1Frame.Pix); i++ {
if useSqDiff {
if isDiffUInt8WithTolerance(video1Frame.Pix[i], video2Frame.Pix[i], minDiff) {
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) {
accumError++
}
} else {
if isDiffUInt8(video1Frame.Pix[i], video2Frame.Pix[i]) {
if isDiffUInt8(video1_frame.Pix[i], video2_frame.Pix[i]) {
accumError++
}
}
}
if minDiff <= accumError {
uniqueFrames++
log.Default().Println("[" + strconv.Itoa(totalFrames) + "]Unique frame")
if min_diff <= accumError {
unique_frames++
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Unique frame")
} else {
log.Default().Println("[" + strconv.Itoa(totalFrames) + "]Non-unique frame")
log.Default().Println("[" + strconv.Itoa(total_frames) + "]Non-unique frame")
}
}
video1.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
}
@ -292,7 +248,7 @@ func getImageFromFilePath(filePath string) (image.Image, error) {
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)
if err != nil {
return err
@ -312,11 +268,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", "frame_width", "frame_height"})
err = csvWriter.Write([]string{"frame", "average_fps", "frame_time", "unique_frame_count", "real_frame_time"})
if err != nil {
return fmt.Errorf("failed to write CSV header: %v", err)
}
@ -329,13 +285,11 @@ 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)
@ -344,8 +298,6 @@ 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
@ -355,40 +307,16 @@ 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
@ -399,8 +327,6 @@ 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
}
@ -430,7 +356,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)
@ -441,7 +367,7 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
uniqueFramesInCurrentSecond++
totalUniqueFrames++
copy(previousFrame.Pix, currentFrame.Pix)
// Start tracking new unique frame
currentUniqueFrameDuration = 1
}
@ -456,52 +382,17 @@ func analyzeFramePersistence(videoPath string, tolerance uint64, csvOutput strin
effectiveFPS: effectiveFPS,
currentFrameTime: actualFrameTimeMs,
realFrameTime: 0, // Will be calculated in second pass
frameWidth: frameWidth,
frameHeight: frameHeight,
})
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
}
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 {
@ -521,8 +412,6 @@ 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)
@ -569,62 +458,5 @@ 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
}