Add remote transcoding support

master
Jonas Letzbor 2024-03-01 23:27:03 +01:00
parent 3944499365
commit 4ddcf2c143
Signed by: RPJosh
GPG Key ID: 43ACB900522EA740
9 changed files with 248 additions and 4 deletions

5
go-vod/.gitignore vendored
View File

@ -20,3 +20,8 @@ go-vod
# Go workspace file # Go workspace file
go.work go.work
# Binary folder
ffmpeg/
# RPdb token folder
token.txt

View File

@ -0,0 +1,78 @@
## Dependencies
Für die Ausführung von *go-vod* benötigen wir die [Jellyfin-Version](https://github.com/jellyfin/jellyfin-ffmpeg/releases) von FFmpeg. Die von Arch standardmäßig zur Verfügung gestellte Binary von FFmpeg funktioniert for *VAAPI* **nicht**.
## Beispiele
In dieser Anwendung passieren keine speziellen oder komplexen Sachen! Im Grunde wird das Video nur herunterskaliert und dann in "Stücken" dem Client zurückgegeben. Mehr passiert nicht!
Ein Beispiel hierfür kann folgendermaßen aussehen.
```sh
ffmpeg/ffmpeg -loglevel warning \
# Zu welcher Sekunde des Videos die Segmente starten sollen
-ss 16.000000 -hwaccel \
# VAAPI spezifische sachen
vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i ffmpeg/sample.mp4 -copyts \
# Flags zum herunterskalieren
-fflags +genpts -vf \ "format=nv12|vaapi,hwupload,scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080" \
# Video und audio mappen
-map "0:v:0" "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
# Teile das Video in 4 Sekunden lange segmente
-f hls -hls_flags split_by_time -hls_time 4 -hls_segment_type mpegts \
# Und schreibe diese als Segmente weg
-force_key_frames "expr:gte(t,n_forced*2)" -hls_segment_filename /home/ubuntugui/videos/1080p-%06d.ts -
# Zum kopieren
ffmpeg/ffmpeg -loglevel warning \
-ss 16.000000 -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i ffmpeg/sample.mp4 -copyts \
-fflags +genpts -vf \ "format=nv12|vaapi,hwupload,scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080" \
-map "0:v:0" -b:v 5M "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
-f hls -hls_flags split_by_time -hls_time 4 -hls_segment_type mpegts \
-force_key_frames "expr:gte(t,n_forced*2)" -hls_segment_filename /media/ubuntugui/videos/1080p-%06d.ts -
# Um die performance zu testen (ohne Segemente)
ffmpeg/ffmpeg -loglevel info \
-ss 16.000000 -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i ffmpeg/sample.mp4 -copyts \
-fflags +genpts -vf \ "format=nv12|vaapi,hwupload,scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080" \
-map "0:v:0" "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
/media/ubuntugui/videos/1080p.ts -y
# Optimierte Variante
ffmpeg/ffmpeg -loglevel info \
-ss 16.000000 -init_hw_device vaapi=dr:/dev/dri/renderD128 -hwaccel vaapi -hwaccel_device dr -hwaccel_output_format vaapi -filter_hw_device dr \
-i ffmpeg/sample.mp4 -copyts \
-fflags +genpts -vf 'scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080' \
-rc_mode VBR -qmax 30 -g 30 -b:v 5000k \
-map "0:v:0" -b:v 5M "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
/media/ubuntugui/videos/1080p.ts -y
# Quick Sync (Intel)
/media/ubuntugui/videos/ffmpeg -loglevel info \
-ss 16.000000 -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i /media/ubuntugui/videos/PXL_20231230_122849503.mp4 -copyts \
-fflags +genpts -vf "format=nv12|vaapi,hwupload,scale_vaapi=force_original_aspect_ratio=decrease:format=nv12:w=1920:h=1080" \
-map "0:v:0" -b:v 5M "-c:v" h264_vaapi -global_quality 23 -map "0:a:0?" "-c:a" aac -ac 1 -start_number 1 -avoid_negative_ts disabled \
/media/ubuntugui/videos/1080p.ts -y
# Vulkan Tests (haben nicht so ganz funktoniert....) -> nur nützlich, wenn man Farbkonvertierungen macht (vaapi -> vulkan -> konvertierungen -> vaapi)
ffmpeg/ffmpeg -loglevel info \
-ss 16.000000 -init_hw_device drm=dr:/dev/dri/renderD128 -init_hw_device vulkan@dr -hwaccel vaapi -filter_hw_device dr -hwaccel_output_format vaapi \
-i ffmpeg/sample.mp4 \
-vf "hwupload=derive_device=vulkan,scale_vulkan=w=1920:h=1080,hwmap=derive_device=vaapi,hwupload_vaapi" -autoscale 0 \
"-c:v" h264_vaapi -b:v 5M "-c:a" aac -ac 1 \
/media/ubuntugui/videos/1080p.ts -y
```
### Ergebnis
Eine Intel IGPU ist ungefähr *1.8x* so schnell wie eine dedizierte AMD Grafikkarte. Warum ist dies der Fall?
## Ausführen
Folgende Befehle für das Ausführen / Bauen der Binary.
```sh
go run ./
```

View File

@ -1,3 +1,8 @@
module github.com/pulsejet/memories/go-vod module github.com/pulsejet/memories/go-vod
go 1.16 go 1.16
require (
git.rpjosh.de/RPJosh/RPdb/v4 v4.3.0 // indirect
git.rpjosh.de/RPJosh/go-logger v1.3.2
)

46
go-vod/go.sum 100644
View File

@ -0,0 +1,46 @@
git.rpjosh.de/RPJosh/RPdb/v4 v4.3.0 h1:7jh6MTOvDhieb5NGm6NMwAIz0aZWwmZiw0EghDwkCz0=
git.rpjosh.de/RPJosh/RPdb/v4 v4.3.0/go.mod h1:y++epwPmZfn+IG3dYYhFE6gXoTJRqLoiwV4QOIEv4XA=
git.rpjosh.de/RPJosh/go-logger v1.2.0/go.mod h1:iD3KaRyOIkYMj7E+xFMn5uDVCzW1lSJQopz1Fl1+BSM=
git.rpjosh.de/RPJosh/go-logger v1.3.2 h1:y8qFEBYeJzLLi6H7CpHHGb2pB0IyfHSG6m6o8TxL1uo=
git.rpjosh.de/RPJosh/go-logger v1.3.2/go.mod h1:iD3KaRyOIkYMj7E+xFMn5uDVCzW1lSJQopz1Fl1+BSM=
github.com/lesismal/llib v1.1.10/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg=
github.com/lesismal/nbio v1.3.10/go.mod h1:cBAu/+XwOfgzhuvl0KA953ZgLx9SxBZPLrp2mMX+Yxk=
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-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -5,6 +5,7 @@ import (
"log" "log"
"os" "os"
"git.rpjosh.de/RPJosh/go-logger"
"github.com/pulsejet/memories/go-vod/transcoder" "github.com/pulsejet/memories/go-vod/transcoder"
) )
@ -16,10 +17,10 @@ func main() {
VersionMonitor: false, VersionMonitor: false,
Version: VERSION, Version: VERSION,
Bind: ":47788", Bind: ":47788",
ChunkSize: 3, ChunkSize: 2,
LookBehind: 3, LookBehind: 4,
GoalBufferMin: 1, GoalBufferMin: 1,
GoalBufferMax: 4, GoalBufferMax: 5,
StreamIdleTime: 60, StreamIdleTime: 60,
ManagerIdleTime: 60, ManagerIdleTime: 60,
} }
@ -36,7 +37,18 @@ func main() {
} }
} }
// Auto detect ffmpeg and ffprobe // Configure logger
configureLogger()
// Initialize RPdb API
c.RPdbAPI = transcoder.GetRPdbAPI()
// Set ffmpeg path
c.FFmpeg = "./ffmpeg/ffmpeg"
c.FFprobe = "./ffmpeg/ffprobe"
c.VAAPI = true
// Auto-detect ffmpeg and ffprobe
c.AutoDetect() c.AutoDetect()
// Start server // Start server
@ -46,3 +58,14 @@ func main() {
log.Println("Exiting go-vod with status code", code) log.Println("Exiting go-vod with status code", code)
os.Exit(code) os.Exit(code)
} }
// configureLogger configures the gloal logger with some default values
func configureLogger() {
globalLogger := logger.GetLoggerFromEnv(&logger.Logger{
File: &logger.FileLogger{},
Level: logger.LevelDebug,
ColoredOutput: true,
PrintSource: true,
})
logger.SetGlobalLogger(globalLogger)
}

View File

@ -6,6 +6,8 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"git.rpjosh.de/RPJosh/RPdb/v4/go/api"
) )
type Config struct { type Config struct {
@ -61,6 +63,9 @@ type Config struct {
// Use GOP size workaround for streaming (NVENC) // Use GOP size workaround for streaming (NVENC)
UseGopSize bool `json:"useGopSize"` UseGopSize bool `json:"useGopSize"`
// RPdbAPI provides access to the RPdb API
RPdbAPI *api.Api
} }
func (c *Config) FromFile(path string) { func (c *Config) FromFile(path string) {

View File

@ -131,9 +131,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
// Map server path to locale path
path = getClientPath(path)
// Get existing manager or create new one // Get existing manager or create new one
manager := h.getManager(path, streamid) manager := h.getManager(path, streamid)
if manager == nil { if manager == nil {
// We don't have the file on the share already
h.c.getFileFromServer(path)
manager = h.createManager(path, streamid) manager = h.createManager(path, streamid)
} }

View File

@ -0,0 +1,73 @@
package transcoder
import (
"os"
"strings"
"git.rpjosh.de/RPJosh/RPdb/v4/go/api"
"git.rpjosh.de/RPJosh/RPdb/v4/go/models"
"git.rpjosh.de/RPJosh/go-logger"
)
// SERVER_PREFIX is the nextcloud data directory of the server
const SERVER_PREFIX = "/media/nextclouddata"
// CLIENT_PREFIX is the nextcloud data directory of the client
const CLIENT_PREFIX = "/media/ubuntugui/NextcloudCache"
// getClientPath returns the absolute path of the movie
// on the client.
// The server and client paths are different!
func getClientPath(path string) string {
if strings.HasPrefix(path, SERVER_PREFIX) {
return CLIENT_PREFIX + strings.TrimPrefix(path, SERVER_PREFIX)
} else {
logger.Warning("Server directory configuration is probably incorrect. Got directory %q", path)
return path
}
}
// getFileFromServer sends a request to the nextcloud server to
// copy the file to the shared volume both server and client can
// access
func (c *Config) getFileFromServer(path string) {
serverPath := strings.TrimPrefix(path, CLIENT_PREFIX)
// Creat entry
rsp, err := c.RPdbAPI.CreateEntry(models.Entry{
Attribute: &models.Attribute{ID: 315},
Offset: "now",
Parameters: []models.EntryParameter{{Value: SERVER_PREFIX + serverPath}},
})
if err != nil {
logger.Warning("Failed to create RPdb entry: %s", err)
return
}
if rsp.ResponseCode != 0 {
logger.Warning("Failed to copy file: %s", rsp.Response)
}
}
func GetRPdbAPI() *api.Api {
apiOptions := api.ApiOptions{BaseUrl: "https://rp.rpjosh.de/api/v1", MultiInstance: true}
return api.NewApi(readRPdbToken(), apiOptions)
}
// readRPdbToken reads the API-Key to use for RPdb.
// It panics if no environment variable was set or the specified
// file with the token does not exist
func readRPdbToken() string {
path := os.Getenv("RPDB_TOKEN")
if path == "" {
path = "./token.txt"
}
// Try to read the file
cnt, err := os.ReadFile(path)
if err != nil {
logger.Fatal("Failed to read RPdb API key from file %q: %s", path, err)
}
return string(cnt)
}

View File

@ -448,11 +448,14 @@ func (s *Stream) transcodeArgs(startAt float64, isHls bool) []string {
} }
} }
//filter = filter + ",fps=60"
args = append(args, []string{"-vf", filter}...) args = append(args, []string{"-vf", filter}...)
} }
// Output specs for video // Output specs for video
args = append(args, []string{ args = append(args, []string{
// "-b:v", "5000k",
// "-rc_mode", "VBR",
"-map", "0:v:0", "-map", "0:v:0",
"-c:v", CV, "-c:v", CV,
}...) }...)