sojorn/go-backend/internal/services/video_processor.go
Patrick Britton 56a9dd032f feat: Add enhanced video moderation with frame extraction and implement placeholder UI methods
- Add VideoProcessor service to PostHandler for frame-based video moderation
- Implement multi-frame extraction and Azure OpenAI Vision analysis for video content
- Enhance VideoStitchingService with filters, speed control, and text overlays
- Add image upload dialogs for group avatar and banner in GroupCreationModal
- Implement navigation placeholders for mentions, hashtags, and URLs in sojornRichText
2026-02-17 13:32:58 -06:00

119 lines
3.1 KiB
Go

package services
import (
"context"
"fmt"
"os/exec"
"path/filepath"
"strings"
"time"
)
// VideoProcessor handles video frame extraction and analysis
type VideoProcessor struct {
ffmpegPath string
tempDir string
}
// NewVideoProcessor creates a new video processor service
func NewVideoProcessor() *VideoProcessor {
ffmpegPath, _ := exec.LookPath("ffmpeg")
return &VideoProcessor{
ffmpegPath: ffmpegPath,
tempDir: "/tmp", // Could be configurable
}
}
// ExtractFrames extracts key frames from a video URL for moderation analysis
// Returns URLs to extracted frame images
func (vp *VideoProcessor) ExtractFrames(ctx context.Context, videoURL string, frameCount int) ([]string, error) {
if vp.ffmpegPath == "" {
return nil, fmt.Errorf("ffmpeg not found on system")
}
// Generate unique temp filename
tempFile := filepath.Join(vp.tempDir, fmt.Sprintf("video_frames_%d.jpg", time.Now().UnixNano()))
// Extract 3 key frames: beginning, middle, end
cmd := exec.CommandContext(ctx, vp.ffmpegPath,
"-i", videoURL,
"-vf", fmt.Sprintf("select=not(mod(n\\,%d)),scale=640:480", frameCount),
"-frames:v", "3",
"-y",
tempFile,
)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("ffmpeg extraction failed: %v, output: %s", err, string(output))
}
// For now, return the temp file path
// In production, this should upload to R2 and return public URLs
return []string{tempFile}, nil
}
// GetVideoDuration returns the duration of a video in seconds
func (vp *VideoProcessor) GetVideoDuration(ctx context.Context, videoURL string) (float64, error) {
if vp.ffmpegPath == "" {
return 0, fmt.Errorf("ffmpeg not found on system")
}
cmd := exec.CommandContext(ctx, vp.ffmpegPath,
"-i", videoURL,
"-f", "null",
"-",
)
output, err := cmd.CombinedOutput()
if err != nil {
return 0, fmt.Errorf("failed to get video duration: %v", err)
}
// Parse duration from ffmpeg output
outputStr := string(output)
durationStr := ""
// Look for "Duration: HH:MM:SS.ms" pattern
lines := strings.Split(outputStr, "\n")
for _, line := range lines {
if strings.Contains(line, "Duration:") {
parts := strings.Split(line, "Duration:")
if len(parts) > 1 {
durationStr = strings.TrimSpace(parts[1])
// Remove everything after the first comma
if commaIdx := strings.Index(durationStr, ","); commaIdx != -1 {
durationStr = durationStr[:commaIdx]
}
break
}
}
}
if durationStr == "" {
return 0, fmt.Errorf("could not parse duration from ffmpeg output")
}
// Parse HH:MM:SS.ms format
var hours, minutes, seconds float64
_, err = fmt.Sscanf(durationStr, "%f:%f:%f", &hours, &minutes, &seconds)
if err != nil {
return 0, fmt.Errorf("failed to parse duration format: %v", err)
}
totalSeconds := hours*3600 + minutes*60 + seconds
return totalSeconds, nil
}
// IsVideoURL checks if a URL points to a video file
func IsVideoURL(url string) bool {
videoExtensions := []string{".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv", ".wmv", ".m4v"}
lowerURL := strings.ToLower(url)
for _, ext := range videoExtensions {
if strings.HasSuffix(lowerURL, ext) {
return true
}
}
return false
}