diff --git a/cmd/olcrtc/main.go b/cmd/olcrtc/main.go index 3ce6bc2..1e14779 100644 --- a/cmd/olcrtc/main.go +++ b/cmd/olcrtc/main.go @@ -18,6 +18,7 @@ import ( "github.com/openlibrecommunity/olcrtc/internal/app/session" "github.com/openlibrecommunity/olcrtc/internal/logger" "github.com/openlibrecommunity/olcrtc/internal/names" + "github.com/openlibrecommunity/olcrtc/internal/transport/videochannel" ) const modeGen = "gen" @@ -25,8 +26,10 @@ const modeGen = "gen" // ErrDataDirRequired is returned when no data directory is specified. var ErrDataDirRequired = errors.New("data directory required (use -data data)") +//nolint:gochecknoglobals // Tests replace the long-running session runner with a bounded function. var runSession = session.Run +//nolint:gochecknoglobals // Tests replace gen runner with a stub. var runGen = execGen type config struct { @@ -63,6 +66,7 @@ type config struct { seiFragmentSize int seiAckTimeoutMS int amount int + ffmpegPath string } func main() { @@ -89,6 +93,10 @@ func runWithArgs(args []string) error { func runWithConfig(cfg config) error { configureLogging(cfg.debug) + if cfg.ffmpegPath != "ffmpeg" && cfg.ffmpegPath != "" { + videochannel.FFmpegPath = cfg.ffmpegPath + } + if cfg.mode == modeGen { return runGen(cfg) } @@ -200,6 +208,7 @@ func parseFlagsFrom(args []string, errorHandling flag.ErrorHandling) (config, er fs.IntVar(&cfg.seiFragmentSize, "frag", 0, "Fragment size in bytes for fragmented transports (seichannel)") fs.IntVar(&cfg.seiAckTimeoutMS, "ack-ms", 0, "ACK timeout in milliseconds for reliable visual transports (seichannel)") fs.IntVar(&cfg.amount, "amount", 0, "Number of rooms to generate (gen mode only)") + fs.StringVar(&cfg.ffmpegPath, "ffmpeg", "ffmpeg", "Path to ffmpeg executable") if err := fs.Parse(args); err != nil { return cfg, fmt.Errorf("parse flags: %w", err) diff --git a/docs/settings.md b/docs/settings.md index a7db07e..75d943f 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -147,6 +147,7 @@ | `-video-qr-size` | Размер фрагмента QR в байтах, `0` = авто | `0` | | `-video-tile-module` | Размер тайла в пикселях 1..270 (только `tile`) | `4` | | `-video-tile-rs` | Reed-Solomon паритет % 0..200 (только `tile`) | `20` | +| `-ffmpeg` | Путь к исполняемому файлу ffmpeg | `ffmpeg` | Для codec `tile` нужно точно `1080x1080`. diff --git a/internal/transport/videochannel/ffmpeg.go b/internal/transport/videochannel/ffmpeg.go index 53b13dc..69e14a0 100644 --- a/internal/transport/videochannel/ffmpeg.go +++ b/internal/transport/videochannel/ffmpeg.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "os" "os/exec" "strconv" "strings" @@ -42,6 +43,9 @@ var ( ErrUnexpectedFrameSize = errors.New("unexpected encoder frame size") ) +// FFmpegPath defines the path to the ffmpeg executable. +var FFmpegPath = "ffmpeg" + type codecSpec struct { mimeType string fourCC string @@ -195,14 +199,25 @@ func newFFmpegEncoder( width, height, fps int, bitrate, hw string, ) (*ffmpegEncoder, error) { - if _, err := exec.LookPath("ffmpeg"); err != nil { - return nil, ErrFFmpegUnavailable + ffmpegBin := FFmpegPath + if envBin := os.Getenv("FFMPEG_BIN"); envBin != "" { + ffmpegBin = envBin + } + + if ffmpegBin == "ffmpeg" { + if _, err := exec.LookPath("ffmpeg"); err != nil { + return nil, ErrFFmpegUnavailable + } + } else { + if _, err := os.Stat(ffmpegBin); err != nil { + return nil, fmt.Errorf("%w: %v", ErrFFmpegUnavailable, err) + } } vcodec := resolveEncoderCodec(spec, hw) args := buildEncoderArgs(spec, vcodec, width, height, fps, bitrate) - cmd := exec.CommandContext(ctx, "ffmpeg", args...) + cmd := exec.CommandContext(ctx, ffmpegBin, args...) //nolint:gosec stdin, err := cmd.StdinPipe() if err != nil { return nil, fmt.Errorf("encoder stdin: %w", err) @@ -397,14 +412,25 @@ func newFFmpegDecoder( width, height, fps int, hw string, ) (*ffmpegDecoder, error) { - if _, err := exec.LookPath("ffmpeg"); err != nil { - return nil, ErrFFmpegUnavailable + ffmpegBin := FFmpegPath + if envBin := os.Getenv("FFMPEG_BIN"); envBin != "" { + ffmpegBin = envBin + } + + if ffmpegBin == "ffmpeg" { + if _, err := exec.LookPath("ffmpeg"); err != nil { + return nil, ErrFFmpegUnavailable + } + } else { + if _, err := os.Stat(ffmpegBin); err != nil { + return nil, fmt.Errorf("%w: %v", ErrFFmpegUnavailable, err) + } } decoderName := resolveDecoderName(spec, hw) args := buildDecoderArgs(spec, decoderName, width, height, "gray") - cmd := exec.CommandContext(ctx, "ffmpeg", args...) + cmd := exec.CommandContext(ctx, ffmpegBin, args...) //nolint:gosec stdin, err := cmd.StdinPipe() if err != nil { return nil, fmt.Errorf("decoder stdin: %w", err) @@ -539,9 +565,9 @@ func writeIVFHeader(w io.Writer, fourCC string, width, height, frameRate int) er binary.LittleEndian.PutUint16(header[4:6], 0) binary.LittleEndian.PutUint16(header[6:8], 32) copy(header[8:12], []byte(fourCC)) - binary.LittleEndian.PutUint16(header[12:14], uint16(width)) - binary.LittleEndian.PutUint16(header[14:16], uint16(height)) - binary.LittleEndian.PutUint32(header[16:20], uint32(frameRate)) + binary.LittleEndian.PutUint16(header[12:14], uint16(width)) //nolint:gosec + binary.LittleEndian.PutUint16(header[14:16], uint16(height)) //nolint:gosec + binary.LittleEndian.PutUint32(header[16:20], uint32(frameRate)) //nolint:gosec binary.LittleEndian.PutUint32(header[20:24], 1) binary.LittleEndian.PutUint32(header[24:28], 0) binary.LittleEndian.PutUint32(header[28:32], 0) @@ -550,7 +576,7 @@ func writeIVFHeader(w io.Writer, fourCC string, width, height, frameRate int) er func writeIVFFrame(w io.Writer, pts uint64, frame []byte) error { header := make([]byte, 12) - binary.LittleEndian.PutUint32(header[0:4], uint32(len(frame))) + binary.LittleEndian.PutUint32(header[0:4], uint32(len(frame))) //nolint:gosec binary.LittleEndian.PutUint64(header[4:12], pts) if err := writeAll(w, header); err != nil { return err