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): enhance codec support and hardware encoder handling across platforms #1154

Open
wants to merge 2 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
7 changes: 7 additions & 0 deletions internal/ffmpeg/ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package ffmpeg

import (
"net/url"
"os/exec"
"slices"
"strings"

"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/helpers"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
Expand Down Expand Up @@ -47,6 +49,11 @@ func Init() {
return "exec:" + args.String(), nil
})

cmd := exec.Command(defaults["bin"], "-encoders")

b, _ := cmd.Output()
helpers.Encoders = helpers.ParseFFmpegEncoders(string(b))

streams.HandleFunc("ffmpeg", NewProducer)

api.HandleFunc("api/ffmpeg", apiFFmpeg)
Expand Down
6 changes: 6 additions & 0 deletions internal/ffmpeg/hardware/hardware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/ffmpeg/helpers"
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"

"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -156,6 +157,11 @@ func run(bin string, args string) bool {
return err == nil
}

// checkAndRun checks if the encoder is supported and runs the probe command.
func checkAndRun(bin, encoder, probeCmd string) bool {
return helpers.IsEncoderSupported(encoder) && run(bin, probeCmd)
}

func runToString(bin string, args string) string {
if run(bin, args) {
return "OK"
Expand Down
37 changes: 22 additions & 15 deletions internal/ffmpeg/hardware/hardware_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,41 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
)

const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
const (
ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
)

func ProbeAll(bin string) []*api.Source {
return []*api.Source{
{
Name: runToString(bin, ProbeVideoToolboxH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
},
{
Name: runToString(bin, ProbeVideoToolboxH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineVideoToolbox,
},
probes := []struct {
encoder string
cmd string
video string
}{
{"h264_videotoolbox", ProbeVideoToolboxH264, "h264"},
{"hevc_videotoolbox", ProbeVideoToolboxH265, "h265"},
}

var sources []*api.Source
for _, probe := range probes {
sources = append(sources, &api.Source{
Name: runToString(bin, probe.cmd),
URL: "ffmpeg:...#video=" + probe.video + "#hardware=" + EngineVideoToolbox,
})
}
return sources
}

func ProbeHardware(bin, name string) string {
switch name {
case "h264":
if run(bin, ProbeVideoToolboxH264) {
if checkAndRun(bin, "h264_videotoolbox", ProbeVideoToolboxH264) {
return EngineVideoToolbox
}

case "h265":
if run(bin, ProbeVideoToolboxH265) {
if checkAndRun(bin, "h265_videotoolbox", ProbeVideoToolboxH265) {
return EngineVideoToolbox
}
}

return EngineSoftware
}
155 changes: 87 additions & 68 deletions internal/ffmpeg/hardware/hardware_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,93 +21,112 @@ const (
)

func ProbeAll(bin string) []*api.Source {
var probes []struct {
encoder string
cmd string
video string
engine string
}

if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
return []*api.Source{
{
Name: runToString(bin, ProbeV4L2M2MH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeV4L2M2MH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
},
{
Name: runToString(bin, ProbeRKMPPH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
},
{
Name: runToString(bin, ProbeRKMPPH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
},
probes = []struct {
encoder string
cmd string
video string
engine string
}{
{"h264_v4l2m2m", ProbeV4L2M2MH264, "h264", EngineV4L2M2M},
{"hevc_v4l2m2m", ProbeV4L2M2MH265, "h265", EngineV4L2M2M},
{"h264_rkmpp_encoder", ProbeRKMPPH264, "h264", EngineRKMPP},
{"hevc_rkmpp_encoder", ProbeRKMPPH265, "h265", EngineRKMPP},
}
} else {
probes = []struct {
encoder string
cmd string
video string
engine string
}{
{"h264_vaapi", ProbeVAAPIH264, "h264", EngineVAAPI},
{"hevc_vaapi", ProbeVAAPIH265, "h265", EngineVAAPI},
{"mjpeg_vaapi", ProbeVAAPIJPEG, "mjpeg", EngineVAAPI},
{"h264_nvenc", ProbeCUDAH264, "h264", EngineCUDA},
{"hevc_nvenc", ProbeCUDAH265, "h265", EngineCUDA},
}
}

return []*api.Source{
{
Name: runToString(bin, ProbeVAAPIH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeVAAPIH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeVAAPIJPEG),
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineVAAPI,
},
{
Name: runToString(bin, ProbeCUDAH264),
URL: "ffmpeg:...#video=h264#hardware=" + EngineCUDA,
},
{
Name: runToString(bin, ProbeCUDAH265),
URL: "ffmpeg:...#video=h265#hardware=" + EngineCUDA,
},
var sources []*api.Source
for _, probe := range probes {
sources = append(sources, &api.Source{
Name: runToString(bin, probe.cmd),
URL: "ffmpeg:...#video=" + probe.video + "#hardware=" + probe.engine,
})
}
return sources
}

func ProbeHardware(bin, name string) string {
var probes []struct {
encoder string
cmd string
engine string
}

if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
switch name {
case "h264":
if run(bin, ProbeV4L2M2MH264) {
return EngineV4L2M2M
probes = []struct {
encoder string
cmd string
engine string
}{
{"h264_v4l2m2m", ProbeV4L2M2MH264, EngineV4L2M2M},
{"h264_rkmpp_encoder", ProbeRKMPPH264, EngineRKMPP},
}
if run(bin, ProbeRKMPPH264) {
return EngineRKMPP
case "h265":
probes = []struct {
encoder string
cmd string
engine string
}{
{"hevc_v4l2m2m", ProbeV4L2M2MH265, EngineV4L2M2M},
{"hevc_rkmpp_encoder", ProbeRKMPPH265, EngineRKMPP},
}
}
} else {
switch name {
case "h264":
probes = []struct {
encoder string
cmd string
engine string
}{
{"h264_nvenc", ProbeCUDAH264, EngineCUDA},
{"h264_vaapi", ProbeVAAPIH264, EngineVAAPI},
}
case "h265":
if run(bin, ProbeV4L2M2MH265) {
return EngineV4L2M2M
probes = []struct {
encoder string
cmd string
engine string
}{
{"hevc_nvenc", ProbeCUDAH265, EngineCUDA},
{"hevc_vaapi", ProbeVAAPIH265, EngineVAAPI},
}
if run(bin, ProbeRKMPPH265) {
return EngineRKMPP
case "mjpeg":
probes = []struct {
encoder string
cmd string
engine string
}{
{"mjpeg_vaapi", ProbeVAAPIJPEG, EngineVAAPI},
}
}

return EngineSoftware
}

switch name {
case "h264":
if run(bin, ProbeCUDAH264) {
return EngineCUDA
}
if run(bin, ProbeVAAPIH264) {
return EngineVAAPI
}

case "h265":
if run(bin, ProbeCUDAH265) {
return EngineCUDA
}
if run(bin, ProbeVAAPIH265) {
return EngineVAAPI
}

case "mjpeg":
if run(bin, ProbeVAAPIJPEG) {
return EngineVAAPI
for _, probe := range probes {
if checkAndRun(bin, probe.encoder, probe.cmd) {
return probe.engine
}
}

Expand Down
71 changes: 71 additions & 0 deletions internal/ffmpeg/helpers/encoders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package helpers

import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)

// Encoder represents an FFmpeg encoder with its attributes
type Encoder struct {
Type string
FrameLevelMT bool
SliceLevelMT bool
Experimental bool
DrawHorizBand bool
DirectRender bool
Name string
Description string
}

var Encoders []Encoder

// ParseFFmpegEncoders parses the given FFmpeg encoder output and returns a slice of Encoders
func ParseFFmpegEncoders(input string) []Encoder {
var encoders []Encoder
scanner := bufio.NewScanner(strings.NewReader(input))

// Define the regular expression to match encoder lines
re := regexp.MustCompile(`^\s?([VAS])([F\.])([S\.])([X\.])([B\.])([D\.])\s+([\w-_]+)\s+(.*)$`)

for scanner.Scan() {
line := scanner.Text()

// Skip lines that don't match the encoder format
if !re.MatchString(line) {
continue
}

// Extract data using the regular expression
matches := re.FindStringSubmatch(line)
if len(matches) == 9 {
encoders = append(encoders, Encoder{
Type: string(matches[1][0]),
FrameLevelMT: matches[2] == "F",
SliceLevelMT: matches[3] == "S",
Experimental: matches[4] == "X",
DrawHorizBand: matches[5] == "B",
DirectRender: matches[6] == "D",
Name: matches[7],
Description: matches[8],
})
}
}

if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading input:", err)
}

return encoders
}

func IsEncoderSupported(codec string) bool {
for _, encoder := range Encoders {
if encoder.Name == codec {
return true
}
}
return false
}
Loading