- 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
119 lines
3.1 KiB
Go
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
|
|
}
|