Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ffmpeg): implement new features of ffmpeg 7 #1040

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ require (
github.com/pion/transport/v2 v2.2.5 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/unascribed/FlexVer/go/flexver v1.0.0
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/unascribed/FlexVer/go/flexver v1.0.0 h1:eaAAWwaT8TiGK75wfEgQRPRVJc1ZIiLTLGUKXxpcs0c=
github.com/unascribed/FlexVer/go/flexver v1.0.0/go.mod h1:OkWZGfmV3DV2ADlgoS7W1+dD1OOci4mEracZCi3ulBk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Expand Down
1 change: 1 addition & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ var mu sync.Mutex
func apiHandler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
app.Info["host"] = r.Host
app.Info["ffmpeg"] = app.FFmpegVersion
mu.Unlock()

ResponseJSON(w, app.Info)
Expand Down
1 change: 1 addition & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

var Version = "1.9.2"
var UserAgent = "go2rtc/" + Version
var FFmpegVersion = ""

var ConfigPath string
var Info = map[string]any{
Expand Down
27 changes: 25 additions & 2 deletions internal/ffmpeg/ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ffmpeg

import (
"net/url"
"strconv"
"strings"

"github.com/AlexxIT/go2rtc/internal/app"
Expand All @@ -10,7 +11,9 @@ import (
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
"github.com/rs/zerolog/log"
)

func Init() {
Expand All @@ -31,13 +34,33 @@ func Init() {
return "exec:" + args.String(), nil
})

if cfg.Mod["version"] == "6.0-default" {
app.FFmpegVersion, _ = parseArgs("").GetFFmpegVersion()
} else {
app.FFmpegVersion = cfg.Mod["version"]
}
Comment on lines +37 to +41
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩼

log.Info().Str("version", app.FFmpegVersion).Msg("[ffmpeg] found")

if core.CompareVersions(app.FFmpegVersion, "7.0") >= 0 {
maxThreadsStr := strconv.Itoa(core.MaxCPUThreads(1))
filterThreadsParam := " -filter_threads " + maxThreadsStr

keysToUpdate := []string{"h264", "h265", "mjpeg"}

for _, key := range keysToUpdate {
defaults[key] += filterThreadsParam
log.Trace().Str("filter_thread", maxThreadsStr).Str("preset", key).Msg("[ffmpeg] modify defaults")
}
}

device.Init(defaults["bin"])
hardware.Init(defaults["bin"])
}

var defaults = map[string]string{
"bin": "ffmpeg",
"global": "-hide_banner",
"bin": "ffmpeg",
"version": "6.0-default",
"global": "-hide_banner",

// inputs
"file": "-re -i {input}",
Expand Down
80 changes: 80 additions & 0 deletions pkg/core/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package core

import (
"crypto/rand"
"unicode"

"runtime"
"strconv"
"strings"
"time"

"github.com/unascribed/FlexVer/go/flexver"
)

const (
Expand Down Expand Up @@ -70,3 +74,79 @@ func Caller() string {
_, file, line, _ := runtime.Caller(1)
return file + ":" + strconv.Itoa(line)
}

// MaxCPUThreads calculates the maximum number of CPU threads available for use,
// taking into account a specified number of cores to reserve.
//
// The function determines the total number of CPU cores available on the system using runtime.NumCPU()
// and subtracts the number of reservedCores from this total. This calculation is intended to allow
// applications to reserve a certain number of cores for critical tasks, while using the remaining
// cores for other operations.
//
// Parameters:
// - reservedCores: An int specifying the number of CPU cores to reserve.
//
// Returns:
// - An int representing the maximum number of CPU threads that can be used after reserving the specified
// number of cores. This function ensures that at least one thread is always available, so it returns
// a minimum of 1, even if the number of reservedCores equals or exceeds the total number of CPU cores.
//
// Example usage:
//
// maxThreads := MaxCPUThreads(2)
// fmt.Printf("Maximum available CPU threads: %d\n", maxThreads)
//
// Note: It's important to consider the workload and performance characteristics of your application
// when deciding how many cores to reserve. Reserving too many cores could lead to underutilization
// of system resources, while reserving too few could impact the performance of critical tasks.
func MaxCPUThreads(reservedCores int) int {
numCPU := runtime.NumCPU()
maxThreads := numCPU - reservedCores
if maxThreads < 1 {
return 1 // Ensure at least one thread is always available
}
return maxThreads
}

// CompareVersions compares two version strings, v1 and v2, after optionally removing a leading letter from each.
// The comparison is performed using the flexver.Compare function. If the first character of either version string
// is a letter, that character is removed before comparison. This function is useful for comparing version strings
// where a leading character might indicate a special version type or pre-release status that should not affect
// the numerical version comparison.
//
// The function returns an integer indicating the relationship between the two versions:
// - 0 if v1 == v2,
// - -1 if v1 < v2,
// - 1 if v1 > v2.
//
// Parameters:
//
// v1 (string): The first version string to compare.
// v2 (string): The second version string to compare.
//
// Returns:
//
// int: An integer indicating the result of the comparison (-1, 0, 1).
//
// Example:
//
// result := CompareVersions("a1.0", "1.2")
// // result will be -1 since "1.0" is considered less than "1.2"
func CompareVersions(v1, v2 string) int {
if len(v1) > 0 && unicode.IsLetter(rune(v1[0])) {
v1 = v1[1:]
}
if len(v2) > 0 && unicode.IsLetter(rune(v2[0])) {
v2 = v2[1:]
}
result, err := flexver.CompareError(v1, v2)
if err != nil {
return -1
}
if result < 0 {
return -1
} else if result > 0 {
return 1
}
return 0
}
90 changes: 90 additions & 0 deletions pkg/core/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package core

import (
"math"
"runtime"
"testing"
)

func TestMaxCPUThreads(t *testing.T) {
tests := []struct {
name string
want int
}{
{
name: "ExpectPositive",
want: int(math.Round(math.Abs(float64(runtime.NumCPU())))) - 1,
},
{
name: "CompareWithGOMAXPROCS",
want: runtime.GOMAXPROCS(0) - 1, // This may not always equal NumCPU() if GOMAXPROCS has been set to a specific value.
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := MaxCPUThreads(1); got != tt.want {
t.Errorf("NumCPU() = %v, want %v", got, tt.want)
}
})
}
}

func TestCompareVersions(t *testing.T) {
type args struct {
v1 string
v2 string
}
tests := []struct {
name string
args args
want int
}{
{
name: "equal versions",
args: args{v1: "1.0.0", v2: "1.0.0"},
want: 0,
},
{
name: "v1 greater than v2",
args: args{v1: "1.0.1", v2: "1.0.0"},
want: 1,
},
{
name: "v1 less than v2",
args: args{v1: "1.0.0", v2: "1.0.1"},
want: -1,
},
{
name: "v1 greater with pre-release",
args: args{v1: "1.0.1-alpha", v2: "1.0.1-beta"},
want: -1,
},
{
name: "v1 less with different major",
args: args{v1: "1.2.3", v2: "2.1.1"},
want: -1,
},
{
name: "v1 greater with different minor",
args: args{v1: "1.3.0", v2: "1.2.9"},
want: 1,
},
{
name: "btbn-ffmpeg ebobo version format",
args: args{v1: "n7.0-7-gd38bf5e08e-20240411", v2: "6.1.1"},
want: 1,
},
{
name: "btbn-ffmpeg ebobo version format 2",
args: args{v1: "n7.0-7-gd38bf5e08e-20240411", v2: "7.1"},
want: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CompareVersions(tt.args.v1, tt.args.v2); got != tt.want {
t.Errorf("CompareVersions() = %v, want %v", got, tt.want)
}
})
}
}
22 changes: 22 additions & 0 deletions pkg/ffmpeg/ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package ffmpeg

import (
"bytes"
"fmt"
"os/exec"
"strconv"
"strings"

"github.com/AlexxIT/go2rtc/pkg/core"
)

type Args struct {
Expand Down Expand Up @@ -84,10 +88,28 @@ func (a *Args) String() string {
b.WriteString(filter)
}
b.WriteByte('"')
ffmpegVersion, _ := a.GetFFmpegVersion()
if core.CompareVersions(ffmpegVersion, "7.0") >= 0 {

b.WriteString(fmt.Sprintf(` -filter_complex_threads %d`, core.MaxCPUThreads(1)))
}
}

b.WriteByte(' ')
b.WriteString(a.Output)

return b.String()
}

func (a *Args) GetFFmpegVersion() (string, error) {
cmd := exec.Command(a.Bin, "-version")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}
firstLine := strings.Split(out.String(), "\n")[0]
versionInfo := strings.Fields(firstLine)[2]
return versionInfo, nil
}
63 changes: 63 additions & 0 deletions pkg/ffmpeg/ffmpeg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ffmpeg

import (
"testing"
)

func TestArgs_GetFFmpegVersion(t *testing.T) {
type fields struct {
Bin string
Global string
Input string
Codecs []string
Filters []string
Output string
Video int
Audio int
}
tests := []struct {
name string
fields fields
want string
wantErr bool
}{
{
name: "Default FFmpeg Path",
fields: fields{
Bin: "ffmpeg",
},
want: "*",
wantErr: false,
},
{
name: "Invalid FFmpeg Path",
fields: fields{
Bin: "/invalid/path/to/ffmpeg",
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Args{
Bin: tt.fields.Bin,
Global: tt.fields.Global,
Input: tt.fields.Input,
Codecs: tt.fields.Codecs,
Filters: tt.fields.Filters,
Output: tt.fields.Output,
Video: tt.fields.Video,
Audio: tt.fields.Audio,
}
got, err := a.GetFFmpegVersion()
if (err != nil) != tt.wantErr {
t.Errorf("Args.GetFFmpegVersion() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want && tt.want != "*" {
t.Errorf("Args.GetFFmpegVersion() = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
const url = new URL('api', location.href);
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
const info = document.querySelector('.info');
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
info.innerText = `Version: ${data.version}, FFmpeg: ${data.ffmpeg}, Config: ${data.config_path}`;
});

reload();
Expand Down