package interlace import ( "bufio" "context" "fmt" "os/exec" "regexp" "strconv" "strings" ) // DetectionResult contains the results of interlacing analysis type DetectionResult struct { // Frame counts from idet filter TFF int // Top Field First frames BFF int // Bottom Field First frames Progressive int // Progressive frames Undetermined int // Undetermined frames TotalFrames int // Total frames analyzed // Calculated metrics InterlacedPercent float64 // Percentage of interlaced frames Status string // "Progressive", "Interlaced", "Mixed" FieldOrder string // "TFF", "BFF", "Unknown" Confidence string // "High", "Medium", "Low" // Recommendations Recommendation string // Human-readable recommendation SuggestDeinterlace bool // Whether deinterlacing is recommended SuggestedFilter string // "yadif", "bwdif", etc. } // Detector analyzes video for interlacing type Detector struct { FFmpegPath string FFprobePath string } // NewDetector creates a new interlacing detector func NewDetector(ffmpegPath, ffprobePath string) *Detector { return &Detector{ FFmpegPath: ffmpegPath, FFprobePath: ffprobePath, } } // Analyze performs interlacing detection on a video file // sampleFrames: number of frames to analyze (0 = analyze entire video) func (d *Detector) Analyze(ctx context.Context, videoPath string, sampleFrames int) (*DetectionResult, error) { // Build FFmpeg command with idet filter args := []string{ "-i", videoPath, "-filter:v", "idet", "-frames:v", fmt.Sprintf("%d", sampleFrames), "-an", // No audio "-f", "null", "-", } if sampleFrames == 0 { // Remove frame limit to analyze entire video args = []string{ "-i", videoPath, "-filter:v", "idet", "-an", "-f", "null", "-", } } cmd := exec.CommandContext(ctx, d.FFmpegPath, args...) // Capture stderr (where idet outputs its stats) stderr, err := cmd.StderrPipe() if err != nil { return nil, fmt.Errorf("failed to get stderr pipe: %w", err) } if err := cmd.Start(); err != nil { return nil, fmt.Errorf("failed to start ffmpeg: %w", err) } // Parse idet output from stderr result := &DetectionResult{} scanner := bufio.NewScanner(stderr) // Regex patterns for idet statistics // Example: [Parsed_idet_0 @ 0x...] Multi frame detection: TFF:123 BFF:0 Progressive:456 Undetermined:7 multiFrameRE := regexp.MustCompile(`Multi frame detection:\s+TFF:\s*(\d+)\s+BFF:\s*(\d+)\s+Progressive:\s*(\d+)\s+Undetermined:\s*(\d+)`) for scanner.Scan() { line := scanner.Text() // Look for the final "Multi frame detection" line if matches := multiFrameRE.FindStringSubmatch(line); matches != nil { result.TFF, _ = strconv.Atoi(matches[1]) result.BFF, _ = strconv.Atoi(matches[2]) result.Progressive, _ = strconv.Atoi(matches[3]) result.Undetermined, _ = strconv.Atoi(matches[4]) } } if err := cmd.Wait(); err != nil { // FFmpeg might return error even on success with null output // Only fail if we got no results if result.TFF == 0 && result.BFF == 0 && result.Progressive == 0 { return nil, fmt.Errorf("ffmpeg failed: %w", err) } } // Calculate metrics result.TotalFrames = result.TFF + result.BFF + result.Progressive + result.Undetermined if result.TotalFrames == 0 { return nil, fmt.Errorf("no frames analyzed - check video file") } interlacedFrames := result.TFF + result.BFF result.InterlacedPercent = (float64(interlacedFrames) / float64(result.TotalFrames)) * 100 // Determine status if result.InterlacedPercent < 5 { result.Status = "Progressive" } else if result.InterlacedPercent > 95 { result.Status = "Interlaced" } else { result.Status = "Mixed Content" } // Determine field order if result.TFF > result.BFF*2 { result.FieldOrder = "TFF (Top Field First)" } else if result.BFF > result.TFF*2 { result.FieldOrder = "BFF (Bottom Field First)" } else if interlacedFrames > 0 { result.FieldOrder = "Mixed/Unknown" } else { result.FieldOrder = "N/A (Progressive)" } // Determine confidence uncertainRatio := float64(result.Undetermined) / float64(result.TotalFrames) if uncertainRatio < 0.05 { result.Confidence = "High" } else if uncertainRatio < 0.15 { result.Confidence = "Medium" } else { result.Confidence = "Low" } // Generate recommendation if result.InterlacedPercent < 5 { result.Recommendation = "Video is progressive. No deinterlacing needed." result.SuggestDeinterlace = false } else if result.InterlacedPercent > 95 { result.Recommendation = "Video is fully interlaced. Deinterlacing strongly recommended." result.SuggestDeinterlace = true result.SuggestedFilter = "yadif" } else { result.Recommendation = fmt.Sprintf("Video has %.1f%% interlaced frames. Deinterlacing recommended for mixed content.", result.InterlacedPercent) result.SuggestDeinterlace = true result.SuggestedFilter = "yadif" } return result, nil } // QuickAnalyze performs a fast analysis using only the first N frames func (d *Detector) QuickAnalyze(ctx context.Context, videoPath string) (*DetectionResult, error) { // Analyze first 500 frames for speed return d.Analyze(ctx, videoPath, 500) } // GenerateDeinterlacePreview generates a preview frame showing before/after deinterlacing func (d *Detector) GenerateDeinterlacePreview(ctx context.Context, videoPath string, timestamp float64, outputPath string) error { // Extract frame at timestamp, apply yadif filter, and save args := []string{ "-ss", fmt.Sprintf("%.2f", timestamp), "-i", videoPath, "-vf", "yadif=0:-1:0", // Deinterlace with yadif "-frames:v", "1", "-y", outputPath, } cmd := exec.CommandContext(ctx, d.FFmpegPath, args...) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to generate preview: %w", err) } return nil } // GenerateComparisonPreview generates a side-by-side comparison of original vs deinterlaced func (d *Detector) GenerateComparisonPreview(ctx context.Context, videoPath string, timestamp float64, outputPath string) error { // Create side-by-side comparison: original (left) vs deinterlaced (right) args := []string{ "-ss", fmt.Sprintf("%.2f", timestamp), "-i", videoPath, "-filter_complex", "[0:v]split=2[orig][deint];[deint]yadif=0:-1:0[d];[orig][d]hstack", "-frames:v", "1", "-y", outputPath, } cmd := exec.CommandContext(ctx, d.FFmpegPath, args...) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to generate comparison: %w", err) } return nil } // String returns a formatted string representation of the detection result func (r *DetectionResult) String() string { var sb strings.Builder sb.WriteString(fmt.Sprintf("Status: %s\n", r.Status)) sb.WriteString(fmt.Sprintf("Interlaced: %.1f%%\n", r.InterlacedPercent)) sb.WriteString(fmt.Sprintf("Field Order: %s\n", r.FieldOrder)) sb.WriteString(fmt.Sprintf("Confidence: %s\n", r.Confidence)) sb.WriteString(fmt.Sprintf("\nFrame Analysis:\n")) sb.WriteString(fmt.Sprintf(" Progressive: %d\n", r.Progressive)) sb.WriteString(fmt.Sprintf(" Top Field First: %d\n", r.TFF)) sb.WriteString(fmt.Sprintf(" Bottom Field First: %d\n", r.BFF)) sb.WriteString(fmt.Sprintf(" Undetermined: %d\n", r.Undetermined)) sb.WriteString(fmt.Sprintf(" Total Analyzed: %d\n", r.TotalFrames)) sb.WriteString(fmt.Sprintf("\nRecommendation: %s\n", r.Recommendation)) return sb.String() }