mirror of
				https://github.com/AlexxIT/go2rtc.git
				synced 2025-10-31 11:46:26 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			303 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			303 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package ffmpeg
 | |
| 
 | |
| import (
 | |
| 	"net/url"
 | |
| 	"strings"
 | |
| 
 | |
| 	"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/rtsp"
 | |
| 	"github.com/AlexxIT/go2rtc/internal/streams"
 | |
| 	"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
 | |
| )
 | |
| 
 | |
| func Init() {
 | |
| 	var cfg struct {
 | |
| 		Mod map[string]string `yaml:"ffmpeg"`
 | |
| 	}
 | |
| 
 | |
| 	cfg.Mod = defaults // will be overriden from yaml
 | |
| 
 | |
| 	app.LoadConfig(&cfg)
 | |
| 
 | |
| 	if app.GetLogger("exec").GetLevel() >= 0 {
 | |
| 		defaults["global"] += " -v error"
 | |
| 	}
 | |
| 
 | |
| 	streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
 | |
| 		args := parseArgs(url[7:])
 | |
| 		return "exec:" + args.String(), nil
 | |
| 	})
 | |
| 
 | |
| 	device.Init(defaults["bin"])
 | |
| 	hardware.Init(defaults["bin"])
 | |
| }
 | |
| 
 | |
| var defaults = map[string]string{
 | |
| 	"bin":    "ffmpeg",
 | |
| 	"global": "-hide_banner",
 | |
| 
 | |
| 	// inputs
 | |
| 	"file": "-re -i {input}",
 | |
| 	"http": "-fflags nobuffer -flags low_delay -i {input}",
 | |
| 	"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
 | |
| 
 | |
| 	"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
 | |
| 
 | |
| 	// output
 | |
| 	"output":       "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
 | |
| 	"output/mjpeg": "-f mjpeg -",
 | |
| 
 | |
| 	// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
 | |
| 	// `-tune zerolatency` - for minimal latency
 | |
| 	// `-profile high -level 4.1` - most used streaming profile
 | |
| 	"h264":  "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
 | |
| 	"h265":  "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
 | |
| 	"mjpeg": "-c:v mjpeg",
 | |
| 	//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
 | |
| 
 | |
| 	// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
 | |
| 	// https://github.com/pion/webrtc/issues/1514
 | |
| 	// https://ffmpeg.org/ffmpeg-resampler.html
 | |
| 	// `-async 1` or `-min_comp 0` - force frame_size=960, important for WebRTC audio quality
 | |
| 	"opus":       "-c:a libopus -application:a lowdelay -frame_duration 20 -min_comp 0",
 | |
| 	"pcmu":       "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
 | |
| 	"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
 | |
| 	"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
 | |
| 	"pcma":       "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
 | |
| 	"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
 | |
| 	"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
 | |
| 	"aac":        "-c:a aac", // keep sample rate and channels
 | |
| 	"aac/16000":  "-c:a aac -ar:a 16000 -ac:a 1",
 | |
| 	"mp3":        "-c:a libmp3lame -q:a 8",
 | |
| 	"pcm":        "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
 | |
| 	"pcm/16000":  "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
 | |
| 	"pcm/48000":  "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
 | |
| 	"pcml":       "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
 | |
| 	"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
 | |
| 
 | |
| 	// hardware Intel and AMD on Linux
 | |
| 	// better not to set `-async_depth:v 1` like for QSV, because framedrops
 | |
| 	// `-bf 0` - disable B-frames is very important
 | |
| 	"h264/vaapi":  "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
 | |
| 	"h265/vaapi":  "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
 | |
| 	"mjpeg/vaapi": "-c:v mjpeg_vaapi",
 | |
| 
 | |
| 	// hardware Raspberry
 | |
| 	"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
 | |
| 	"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
 | |
| 
 | |
| 	// hardware NVidia on Linux and Windows
 | |
| 	// preset=p2 - faster, tune=ll - low latency
 | |
| 	"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
 | |
| 	"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto",
 | |
| 
 | |
| 	// hardware Intel on Windows
 | |
| 	"h264/dxva2":  "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
 | |
| 	"h265/dxva2":  "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
 | |
| 	"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
 | |
| 
 | |
| 	// hardware macOS
 | |
| 	"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
 | |
| 	"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
 | |
| }
 | |
| 
 | |
| // configTemplate - return template from config (defaults) if exist or return raw template
 | |
| func configTemplate(template string) string {
 | |
| 	if s := defaults[template]; s != "" {
 | |
| 		return s
 | |
| 	}
 | |
| 	return template
 | |
| }
 | |
| 
 | |
| // inputTemplate - select input template from YAML config by template name
 | |
| // if query has input param - select another template by this name
 | |
| // if there is no another template - use input param as template
 | |
| func inputTemplate(name, s string, query url.Values) string {
 | |
| 	var template string
 | |
| 	if input := query.Get("input"); input != "" {
 | |
| 		template = configTemplate(input)
 | |
| 	} else {
 | |
| 		template = defaults[name]
 | |
| 	}
 | |
| 	return strings.Replace(template, "{input}", s, 1)
 | |
| }
 | |
| 
 | |
| func parseArgs(s string) *ffmpeg.Args {
 | |
| 	// init FFmpeg arguments
 | |
| 	args := &ffmpeg.Args{
 | |
| 		Bin:    defaults["bin"],
 | |
| 		Global: defaults["global"],
 | |
| 		Output: defaults["output"],
 | |
| 	}
 | |
| 
 | |
| 	var query url.Values
 | |
| 	if i := strings.IndexByte(s, '#'); i > 0 {
 | |
| 		query = streams.ParseQuery(s[i+1:])
 | |
| 		args.Video = len(query["video"])
 | |
| 		args.Audio = len(query["audio"])
 | |
| 		s = s[:i]
 | |
| 	}
 | |
| 
 | |
| 	// Parse input:
 | |
| 	//   1. Input as xxxx:// link (http or rtsp or any other)
 | |
| 	//   2. Input as stream name
 | |
| 	//   3. Input as FFmpeg device (local USB camera)
 | |
| 	if i := strings.Index(s, "://"); i > 0 {
 | |
| 		switch s[:i] {
 | |
| 		case "http", "https", "rtmp":
 | |
| 			args.Input = inputTemplate("http", s, query)
 | |
| 		case "rtsp", "rtsps":
 | |
| 			// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
 | |
| 			// skip unnecessary input tracks
 | |
| 			switch {
 | |
| 			case (args.Video > 0 && args.Audio > 0) || (args.Video == 0 && args.Audio == 0):
 | |
| 				args.Input = "-allowed_media_types video+audio "
 | |
| 			case args.Video > 0:
 | |
| 				args.Input = "-allowed_media_types video "
 | |
| 			case args.Audio > 0:
 | |
| 				args.Input = "-allowed_media_types audio "
 | |
| 			}
 | |
| 
 | |
| 			args.Input += inputTemplate("rtsp", s, query)
 | |
| 		default:
 | |
| 			args.Input = "-i " + s
 | |
| 		}
 | |
| 	} else if streams.Get(s) != nil {
 | |
| 		s = "rtsp://127.0.0.1:" + rtsp.Port + "/" + s
 | |
| 		switch {
 | |
| 		case args.Video > 0 && args.Audio == 0:
 | |
| 			s += "?video"
 | |
| 		case args.Audio > 0 && args.Video == 0:
 | |
| 			s += "?audio"
 | |
| 		default:
 | |
| 			s += "?video&audio"
 | |
| 		}
 | |
| 		args.Input = inputTemplate("rtsp", s, query)
 | |
| 	} else if strings.HasPrefix(s, "device?") {
 | |
| 		var err error
 | |
| 		args.Input, err = device.GetInput(s)
 | |
| 		if err != nil {
 | |
| 			return nil
 | |
| 		}
 | |
| 	} else {
 | |
| 		args.Input = inputTemplate("file", s, query)
 | |
| 	}
 | |
| 
 | |
| 	if query["async"] != nil {
 | |
| 		args.Input = "-use_wallclock_as_timestamps 1 -async 1 " + args.Input
 | |
| 	}
 | |
| 
 | |
| 	// Parse query params:
 | |
| 	//   1. `width`/`height` params
 | |
| 	//   2. `rotate` param
 | |
| 	//   3. `video` params (support multiple)
 | |
| 	//   4. `audio` params (support multiple)
 | |
| 	//   5. `hardware` param
 | |
| 	if query != nil {
 | |
| 		// 1. Process raw params for FFmpeg
 | |
| 		for _, raw := range query["raw"] {
 | |
| 			// support templates https://github.com/AlexxIT/go2rtc/issues/487
 | |
| 			raw = configTemplate(raw)
 | |
| 			args.AddCodec(raw)
 | |
| 		}
 | |
| 
 | |
| 		// 2. Process video filters (resize and rotation)
 | |
| 		if query["width"] != nil || query["height"] != nil {
 | |
| 			filter := "scale="
 | |
| 			if query["width"] != nil {
 | |
| 				filter += query["width"][0]
 | |
| 			} else {
 | |
| 				filter += "-1"
 | |
| 			}
 | |
| 			filter += ":"
 | |
| 			if query["height"] != nil {
 | |
| 				filter += query["height"][0]
 | |
| 			} else {
 | |
| 				filter += "-1"
 | |
| 			}
 | |
| 			args.AddFilter(filter)
 | |
| 		}
 | |
| 
 | |
| 		if query["rotate"] != nil {
 | |
| 			var filter string
 | |
| 			switch query["rotate"][0] {
 | |
| 			case "90":
 | |
| 				filter = "transpose=1" // 90 degrees clockwise
 | |
| 			case "180":
 | |
| 				filter = "transpose=1,transpose=1"
 | |
| 			case "-90", "270":
 | |
| 				filter = "transpose=2" // 90 degrees counterclockwise
 | |
| 			}
 | |
| 			if filter != "" {
 | |
| 				args.AddFilter(filter)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		for _, drawtext := range query["drawtext"] {
 | |
| 			// support templates https://github.com/AlexxIT/go2rtc/issues/487
 | |
| 			drawtext = configTemplate(drawtext)
 | |
| 
 | |
| 			// support default timestamp format
 | |
| 			if !strings.Contains(drawtext, "text=") {
 | |
| 				drawtext += `:text='%{localtime\:%Y-%m-%d %X}'`
 | |
| 			}
 | |
| 
 | |
| 			args.AddFilter("drawtext=" + drawtext)
 | |
| 		}
 | |
| 
 | |
| 		// 3. Process video codecs
 | |
| 		if args.Video > 0 {
 | |
| 			for _, video := range query["video"] {
 | |
| 				if video != "copy" {
 | |
| 					if codec := defaults[video]; codec != "" {
 | |
| 						args.AddCodec(codec)
 | |
| 					} else {
 | |
| 						args.AddCodec(video)
 | |
| 					}
 | |
| 				} else {
 | |
| 					args.AddCodec("-c:v copy")
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// 4. Process audio codecs
 | |
| 		if args.Audio > 0 {
 | |
| 			for _, audio := range query["audio"] {
 | |
| 				if audio != "copy" {
 | |
| 					if codec := defaults[audio]; codec != "" {
 | |
| 						args.AddCodec(codec)
 | |
| 					} else {
 | |
| 						args.AddCodec(audio)
 | |
| 					}
 | |
| 				} else {
 | |
| 					args.AddCodec("-c:a copy")
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if query["hardware"] != nil {
 | |
| 			hardware.MakeHardware(args, query["hardware"][0], defaults)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	switch {
 | |
| 	case args.Video == 0 && args.Audio == 0:
 | |
| 		args.AddCodec("-c copy")
 | |
| 	case args.Video == 0:
 | |
| 		args.AddCodec("-vn")
 | |
| 	case args.Audio == 0:
 | |
| 		args.AddCodec("-an")
 | |
| 	}
 | |
| 
 | |
| 	// transcoding to only mjpeg
 | |
| 	if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
 | |
| 		// no transcoding from mjpeg input
 | |
| 		(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
 | |
| 		args.Output = defaults["output/mjpeg"]
 | |
| 	}
 | |
| 
 | |
| 	return args
 | |
| }
 | 
