From 99226c74798b5230aeb7e78ba02281fbef9026c7 Mon Sep 17 00:00:00 2001 From: ila Date: Thu, 23 Jul 2026 20:31:00 +0800 Subject: [PATCH] fix(v2): log redacted ffmpeg/ffprobe stderr on stream failure The stream worker discarded ffmpeg stderr, so reconnect loops were opaque. Capture stderr and log it with the RTSP URL/credentials redacted, so the real cause (auth, timeout, option, URL parse) is diagnosable without leaking the source address. Co-Authored-By: Claude Opus 4.8 --- v2/internal/source/stream.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/v2/internal/source/stream.go b/v2/internal/source/stream.go index a4444a9..0961086 100644 --- a/v2/internal/source/stream.go +++ b/v2/internal/source/stream.go @@ -2,18 +2,32 @@ package source import ( + "bytes" "context" "encoding/json" "errors" "fmt" "io" + "log" "os/exec" + "regexp" "strconv" "strings" "sync" "time" ) +var rtspURLPattern = regexp.MustCompile(`rtsp://[^\s'"]+`) + +// redact removes the resolved source URL (which may carry credentials) from any +// FFmpeg/FFprobe diagnostic text before it is logged. +func redact(text, sourceURL string) string { + if sourceURL != "" { + text = strings.ReplaceAll(text, sourceURL, "") + } + return rtspURLPattern.ReplaceAllString(text, "rtsp://") +} + type Status string const ( @@ -195,8 +209,12 @@ func probe(ctx context.Context, config Config) (Metadata, error) { args := []string{"-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,r_frame_rate", "-of", "json"} args = appendInputOptions(args, config) args = append(args, "-i", config.SourceURL) - output, err := exec.CommandContext(ctx, config.FFprobePath, args...).Output() + command := exec.CommandContext(ctx, config.FFprobePath, args...) + var stderr bytes.Buffer + command.Stderr = &stderr + output, err := command.Output() if err != nil { + log.Printf("ffprobe 探测失败:%s(%v)", strings.TrimSpace(redact(stderr.String(), config.SourceURL)), err) return Metadata{}, errors.New("unable to probe video metadata") } var parsed struct { @@ -230,7 +248,8 @@ func decode(ctx context.Context, config Config, metadata Metadata, publish func( args := append([]string{"-hide_banner", "-loglevel", "error"}, appendInputOptions(nil, config)...) args = append(args, "-i", config.SourceURL, "-an", "-sn", "-dn", "-f", "rawvideo", "-pix_fmt", "bgr24", "-") command := exec.CommandContext(ctx, config.FFmpegPath, args...) - command.Stderr = io.Discard + var stderr bytes.Buffer + command.Stderr = &stderr stdout, err := command.StdoutPipe() if err != nil { return errors.New("unable to read video frames") @@ -247,6 +266,7 @@ func decode(ctx context.Context, config Config, metadata Metadata, publish func( if ctx.Err() != nil { return ctx.Err() } + log.Printf("ffmpeg 解码停止:%s(%v)", strings.TrimSpace(redact(stderr.String(), config.SourceURL)), err) return errors.New("video decoder stopped") } publish(frame)