// Package probe 负责给媒体文件"体检": // - 图片:读出它的宽高(尺寸),特殊格式(WebP/BMP/PSD)有专门的读法 // - 视频/音频:调 ffprobe 程序读出编码、时长、宽高等信息 // - 明暗分析:图片是偏暗还是偏亮(给图片分类用) // // 大白话理解:这一步负责搞清楚"这个文件到底是什么样的",好让后面决定把它归到哪。 // 体检结果统一放进 ProbeResult 这一个结构里,图片、视频、音频都往里填。 package probe import ( "context" "encoding/json" "fmt" "os/exec" "strconv" "time" ) // ProbeResult 一份"体检报告",图片/视频/音频通用。 type ProbeResult struct { HasVideo bool // 有没有视频流 HasAudio bool // 有没有音频流 Width int // 宽(像素) Height int // 高(像素) Duration float64 // 时长(秒) VideoCodec string // 视频编码(如 h264) AudioCodec string // 音频编码(如 aac) Format string // 容器格式(如 mp4) RawJSON string // ffprobe 读出的原始信息,存下来留底 } // streamInfo 是 ffprobe 输出里"一条流"的 JSON 结构(视频流/音频流都会列出来)。 type streamInfo struct { Index int `json:"index"` CodecType string `json:"codec_type"` // video / audio CodecName string `json:"codec_name"` Width int `json:"width"` Height int `json:"height"` } // formatInfo 是 ffprobe 输出里"容器格式"部分的 JSON 结构。 type formatInfo struct { Duration string `json:"duration"` Format string `json:"format_name"` } // probeOutput 是 ffprobe 完整输出的 JSON 结构。 type probeOutput struct { Streams []streamInfo `json:"streams"` Format formatInfo `json:"format"` } // ffprobeTimeout 调 ffprobe 的超时时间。超过就当探测失败, // 免得遇到损坏文件时卡住整个程序。 const ffprobeTimeout = 30 * time.Second // ProbeMediaFile 调 ffprobe 探测一个媒体文件,一次调用就把 // 视频流/音频流/时长/格式全部拿到。 func ProbeMediaFile(ffprobePath, filePath string) (*ProbeResult, error) { // 带超时的执行环境:文件坏了也不至于永远卡住 ctx, cancel := context.WithTimeout(context.Background(), ffprobeTimeout) defer cancel() // 用 ffprobe 读信息,指定输出 JSON 格式 cmd := exec.CommandContext(ctx, ffprobePath, "-v", "error", "-show_entries", "stream=index,codec_type,codec_name,width,height:format=duration,format_name", "-of", "json", filePath) output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("ffprobe failed: %w", err) } // 把 JSON 输出翻译成结构体 var out probeOutput if err := json.Unmarshal(output, &out); err != nil { return nil, fmt.Errorf("parse ffprobe output: %w", err) } res := &ProbeResult{ Format: out.Format.Format, RawJSON: string(output), } // 时长(秒) if d, err := strconv.ParseFloat(out.Format.Duration, 64); err == nil { res.Duration = d } // 把每条流的信息填进报告: // 视频流 → 记下"有视频",补上编码和宽高; // 音频流 → 记下"有音频",补上编码。 for _, s := range out.Streams { switch s.CodecType { case "video": res.HasVideo = true if res.VideoCodec == "" { res.VideoCodec = s.CodecName } if res.Width == 0 && s.Width > 0 && s.Height > 0 { res.Width, res.Height = s.Width, s.Height } case "audio": res.HasAudio = true if res.AudioCodec == "" { res.AudioCodec = s.CodecName } } } return res, nil }