package transcoder import ( "encoding/json" "os/exec" "strconv" "strings" "jukebox_maker/internal/disk" ) type VideoInfo struct { Codec string Width int Height int FPS float64 DurationSec float64 VideoBitrate int64 AudioCodec string AudioChannels int } func ProbeVideo(path string) (VideoInfo, error) { out, err := exec.Command("ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "-show_format", path, ).Output() if err != nil { return VideoInfo{}, err } var raw struct { Streams []struct { CodecType string `json:"codec_type"` CodecName string `json:"codec_name"` Width int `json:"width"` Height int `json:"height"` RFrameRate string `json:"r_frame_rate"` BitRate string `json:"bit_rate"` Channels int `json:"channels"` } `json:"streams"` Format struct { Duration string `json:"duration"` BitRate string `json:"bit_rate"` } `json:"format"` } if err := json.Unmarshal(out, &raw); err != nil { return VideoInfo{}, err } var info VideoInfo for _, s := range raw.Streams { switch s.CodecType { case "video": info.Codec = s.CodecName info.Width = s.Width info.Height = s.Height info.FPS = parseFraction(s.RFrameRate) if br, err := strconv.ParseInt(s.BitRate, 10, 64); err == nil { info.VideoBitrate = br } case "audio": if info.AudioCodec == "" { info.AudioCodec = s.CodecName info.AudioChannels = s.Channels } } } if d, err := strconv.ParseFloat(raw.Format.Duration, 64); err == nil { info.DurationSec = d } // Если стрим-битрейт не известен — используем битрейт контейнера if info.VideoBitrate == 0 { if br, err := strconv.ParseInt(raw.Format.BitRate, 10, 64); err == nil { info.VideoBitrate = br } } return info, nil } func NeedsTranscode(info VideoInfo, p *disk.TranscodeProfile) bool { // Несовместимый видеокодек if normalizeCodec(info.Codec) != p.VideoCodec { return true } // Разрешение превышает лимит if maxH := maxHeight(p.MaxResolution); maxH > 0 && info.Height > maxH { return true } // Битрейт видео превышает лимит if p.MaxVideoBitrate != "" { if maxBR := parseBitrate(p.MaxVideoBitrate); maxBR > 0 && info.VideoBitrate > maxBR { return true } } // FPS превышает лимит if p.MaxFPS > 0 && info.FPS > float64(p.MaxFPS)+0.01 { return true } // Несовместимый аудиокодек if p.AudioCodec != "" && normalizeCodec(info.AudioCodec) != p.AudioCodec { return true } // Каналов больше лимита if p.MaxAudioChannels > 0 && info.AudioChannels > p.MaxAudioChannels { return true } return false } func normalizeCodec(codec string) string { switch strings.ToLower(codec) { case "hevc", "h265": return "h265" case "h264", "avc", "avc1": return "h264" case "mpeg4", "mp4v": return "mpeg4" case "aac": return "aac" case "mp3": return "mp3" default: return strings.ToLower(codec) } } func maxHeight(res string) int { switch res { case "480p": return 480 case "720p": return 720 case "1080p": return 1080 default: return 0 } } func parseBitrate(s string) int64 { s = strings.ToLower(strings.TrimSpace(s)) if strings.HasSuffix(s, "k") { v, _ := strconv.ParseInt(s[:len(s)-1], 10, 64) return v * 1000 } v, _ := strconv.ParseInt(s, 10, 64) return v } func parseFraction(s string) float64 { parts := strings.SplitN(s, "/", 2) if len(parts) != 2 { v, _ := strconv.ParseFloat(s, 64) return v } num, _ := strconv.ParseFloat(parts[0], 64) den, _ := strconv.ParseFloat(parts[1], 64) if den == 0 { return 0 } return num / den }