Skip to content
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
21 changes: 10 additions & 11 deletions server/ffprobe/ffprobe.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,19 @@ func Exists() bool {
}

func ProbeUrl(link string) (*ffprobe.ProbeData, error) {
data, err := ffprobe.ProbeURL(getCtx(), link)
data, err := ProbeUrlWithTimeout(link, 5*time.Minute)
return data, err
}

func ProbeReader(reader io.Reader) (*ffprobe.ProbeData, error) {
data, err := ffprobe.ProbeReader(getCtx(), reader)
return data, err
func ProbeUrlWithTimeout(link string, timeout time.Duration) (*ffprobe.ProbeData, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return ffprobe.ProbeURL(ctx, link)
}

func getCtx() context.Context {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(5 * time.Minute)
cancel()
}()
return ctx
func ProbeReader(reader io.Reader) (*ffprobe.ProbeData, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
data, err := ffprobe.ProbeReader(ctx, reader)
return data, err
}
3 changes: 2 additions & 1 deletion server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/anacrolix/publicip v0.3.1
github.com/anacrolix/torrent v1.59.1
github.com/dustin/go-humanize v1.0.1
github.com/ebitengine/purego v0.10.1
github.com/gin-contrib/cors v1.7.6
github.com/gin-contrib/location/v2 v2.0.0
github.com/gin-gonic/gin v1.11.0
Expand All @@ -32,6 +33,7 @@ require (
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
golang.org/x/image v0.33.0
golang.org/x/net v0.55.0
golang.org/x/sys v0.45.0
golang.org/x/time v0.15.0
gopkg.in/telebot.v4 v4.0.0-beta.7
gopkg.in/vansante/go-ffprobe.v2 v2.2.1
Expand Down Expand Up @@ -199,7 +201,6 @@ require (
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.45.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down
197 changes: 197 additions & 0 deletions server/gstreamer/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package gstreamer

import (
"encoding/json"
"runtime"
"strings"
"time"

"server/settings"
)

type Config struct {
GSTVersion float64
GSTPath string
Source string

InactiveMinutes int

AACBitrateKbps int
SegmentSeconds int

TranscodeH264 bool
TranscodeH265 bool
TranscodeAV1 bool
TranscodeVP9 bool
VideoBitrate int

PipelineTimeSeconds int
PipelineAudioQueue int
PipelineVideoQueue int

TempFS bool
TempFSRing int
}

func DefaultConfig() Config {
conf := Config{
GSTVersion: 1.26,
Source: "stream",
InactiveMinutes: 5,
AACBitrateKbps: 256,
SegmentSeconds: 6,
VideoBitrate: 10_000,
PipelineTimeSeconds: 20,
PipelineAudioQueue: 4,
PipelineVideoQueue: 32,
TempFS: true,
}

if runtime.GOOS == "windows" {
conf.GSTVersion = 1.28
conf.GSTPath = `C:\Program Files\gstreamer\1.0\mingw_x86_64`
}

return applySettingsConfig(conf).normalized()
}

func (c Config) normalized() Config {
if c.InactiveMinutes <= 0 {
c.InactiveMinutes = 5
}
if c.AACBitrateKbps <= 0 {
c.AACBitrateKbps = 256
}
if c.SegmentSeconds <= 0 {
c.SegmentSeconds = 6
}
if c.VideoBitrate <= 0 {
c.VideoBitrate = 10_000
}
if c.PipelineTimeSeconds <= 0 {
c.PipelineTimeSeconds = 20
}
if c.PipelineAudioQueue <= 0 {
c.PipelineAudioQueue = 4
}
if c.PipelineVideoQueue <= 0 {
c.PipelineVideoQueue = 32
}
if c.TempFSRing < 0 {
c.TempFSRing = 0
}
if c.GSTVersion <= 0 {
c.GSTVersion = 1.26
}
c.Source = strings.ToLower(strings.TrimSpace(c.Source))
if c.Source != "play" {
c.Source = "stream"
}
return c
}

func (c Config) inactiveDuration() time.Duration {
return time.Duration(c.normalized().InactiveMinutes) * time.Minute
}

type storedConfig struct {
GSTVersion *float64
GSTPath *string
Source *string

InactiveMinutes *int

AACBitrateKbps *int
SegmentSeconds *int

TranscodeH264 *bool
TranscodeH265 *bool
TranscodeAV1 *bool
TranscodeVP9 *bool
VideoBitrate *int

PipelineTimeSeconds *int
PipelineAudioQueue *int
PipelineVideoQueue *int

TempFS *bool `json:"tempfs"`
TempFSRing *int `json:"tempfs_ring"`
}

func applySettingsConfig(conf Config) Config {
if settings.Path == "" {
return conf
}

db := settings.NewJsonDB()
if db == nil {
return conf
}

var data []byte
for _, name := range []string{"gst", "GStreamer"} {
data = db.Get("Settings", name)
if len(data) > 0 {
break
}
}
if len(data) == 0 {
return conf
}

var stored storedConfig
if err := json.Unmarshal(data, &stored); err != nil {
return conf
}

if stored.GSTVersion != nil {
conf.GSTVersion = *stored.GSTVersion
}
if stored.GSTPath != nil {
conf.GSTPath = *stored.GSTPath
}
if stored.Source != nil {
conf.Source = *stored.Source
}
if stored.InactiveMinutes != nil {
conf.InactiveMinutes = *stored.InactiveMinutes
}
if stored.AACBitrateKbps != nil {
conf.AACBitrateKbps = *stored.AACBitrateKbps
}
if stored.SegmentSeconds != nil {
conf.SegmentSeconds = *stored.SegmentSeconds
}
if stored.TranscodeH264 != nil {
conf.TranscodeH264 = *stored.TranscodeH264
}
if stored.TranscodeH265 != nil {
conf.TranscodeH265 = *stored.TranscodeH265
}
if stored.TranscodeAV1 != nil {
conf.TranscodeAV1 = *stored.TranscodeAV1
}
if stored.TranscodeVP9 != nil {
conf.TranscodeVP9 = *stored.TranscodeVP9
}
if stored.VideoBitrate != nil {
conf.VideoBitrate = *stored.VideoBitrate
}
if stored.PipelineTimeSeconds != nil {
conf.PipelineTimeSeconds = *stored.PipelineTimeSeconds
}
if stored.PipelineAudioQueue != nil {
conf.PipelineAudioQueue = *stored.PipelineAudioQueue
}
if stored.PipelineVideoQueue != nil {
conf.PipelineVideoQueue = *stored.PipelineVideoQueue
}
if stored.TempFS != nil {
conf.TempFS = *stored.TempFS
}
if stored.TempFSRing != nil {
conf.TempFSRing = *stored.TempFSRing
}

return conf
}
86 changes: 86 additions & 0 deletions server/gstreamer/echo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package gstreamer

import (
"context"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"

"github.com/gin-gonic/gin"
)

type echoResponse struct {
FFProbe componentStatus `json:"ffprobe"`
GStreamer componentStatus `json:"gstreamer"`
}

type componentStatus struct {
Found bool `json:"found"`
Available bool `json:"available"`
Works bool `json:"works"`
}

func (s *Service) echo(c *gin.Context) {
c.JSON(http.StatusOK, echoResponse{
FFProbe: checkFFProbe(),
GStreamer: checkGStreamer(s.conf),
})
}

func checkFFProbe() componentStatus {
var status componentStatus

path, ok := findFFProbeBinary()
if !ok {
return status
}
status.Found = true

info, err := os.Stat(path)
if err != nil || info.IsDir() {
return status
}
status.Available = true

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

if err := exec.CommandContext(ctx, path, "-version").Run(); err == nil {
status.Works = true
}
return status
}

func findFFProbeBinary() (string, bool) {
if path, err := exec.LookPath("ffprobe"); err == nil {
return path, true
}

dirs := []string{"."}
if exe, err := os.Executable(); err == nil {
dirs = append(dirs, filepath.Dir(exe))
}

seen := make(map[string]struct{}, len(dirs))
for _, dir := range dirs {
absDir, err := filepath.Abs(dir)
if err != nil {
continue
}
if _, ok := seen[absDir]; ok {
continue
}
seen[absDir] = struct{}{}

for _, name := range []string{"ffprobe", "ffprobe.exe"} {
path := filepath.Join(absDir, name)
if _, err := os.Stat(path); err == nil {
return path, true
}
}
}

return "", false
}
48 changes: 48 additions & 0 deletions server/gstreamer/echo_gst.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//go:build (windows && (amd64 || arm64)) || (linux && (amd64 || arm64)) || (darwin && (amd64 || arm64))

package gstreamer

import "time"

func checkGStreamer(conf Config) componentStatus {
gstInitOnce.Do(func() {
initGStreamerRuntime(conf)
})

status := gstInitStatus
if !status.Available || gstRuntime == nil {
return status
}

if checkGStreamerPipeline(gstRuntime) == nil {
status.Works = true
}
return status
}

func checkGStreamerPipeline(api *gstAPI) error {
pipeline, err := api.parseLaunch("fakesrc num-buffers=1 ! fakesink")
if err != nil {
return err
}
defer api.objectUnref(pipeline)

bus := api.pipelineGetBus(pipeline)
if bus != 0 {
defer api.objectUnref(bus)
}

if ret := api.elementSetState(pipeline, gstStatePlaying); ret == gstStateChangeFailure {
_ = api.elementSetState(pipeline, gstStateNull)
return api.popBusError(bus, 0)
}
if ret := api.elementGetState(pipeline, 5*time.Second); ret == gstStateChangeFailure {
_ = api.elementSetState(pipeline, gstStateNull)
return api.popBusError(bus, 0)
}

if ret := api.elementSetState(pipeline, gstStateNull); ret == gstStateChangeFailure {
return api.popBusError(bus, 0)
}
return nil
}
Loading