145 lines
3.7 KiB
Go
145 lines
3.7 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const generatedVideoMetadataProbeTimeout = 8 * time.Second
|
|
|
|
type generatedVideoMetadata struct {
|
|
Duration float64
|
|
HasAudio bool
|
|
HasAudioKnown bool
|
|
}
|
|
|
|
type ffprobeVideoMetadata struct {
|
|
Format struct {
|
|
Duration string `json:"duration"`
|
|
} `json:"format"`
|
|
Streams []struct {
|
|
CodecType string `json:"codec_type"`
|
|
} `json:"streams"`
|
|
}
|
|
|
|
func (s *Service) enrichGeneratedVideoMetadata(ctx context.Context, taskKind string, result map[string]any) map[string]any {
|
|
if taskKind != "videos.generations" {
|
|
return result
|
|
}
|
|
data, _ := result["data"].([]any)
|
|
if len(data) == 0 {
|
|
return result
|
|
}
|
|
for _, raw := range data {
|
|
item, _ := raw.(map[string]any)
|
|
if len(item) == 0 || !isGeneratedVideoItem(item) {
|
|
continue
|
|
}
|
|
needsDuration := floatFromAny(item["duration"]) <= 0
|
|
_, hasAudioMetadata := boolishOptional(firstPresentValue(item, "has_audio", "hasAudio"))
|
|
if !needsDuration && hasAudioMetadata {
|
|
continue
|
|
}
|
|
urlValue := firstNonEmptyStringValue(item, "video_url", "videoUrl", "url")
|
|
if urlValue == "" {
|
|
continue
|
|
}
|
|
metadata, err := s.probeVideoMetadata(ctx, urlValue)
|
|
if err != nil {
|
|
if s.logger != nil {
|
|
s.logger.Debug("probe generated video metadata failed", "url", trimForLog(urlValue), "error", err)
|
|
}
|
|
continue
|
|
}
|
|
if needsDuration && metadata.Duration > 0 {
|
|
item["duration"] = metadata.Duration
|
|
}
|
|
if !hasAudioMetadata && metadata.HasAudioKnown {
|
|
item["has_audio"] = metadata.HasAudio
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func isGeneratedVideoItem(item map[string]any) bool {
|
|
itemType := strings.TrimSpace(stringFromAny(item["type"]))
|
|
if itemType == "video" {
|
|
return true
|
|
}
|
|
if firstNonEmptyStringValue(item, "video_url", "videoUrl") != "" {
|
|
return true
|
|
}
|
|
urlValue := strings.ToLower(firstNonEmptyStringValue(item, "url"))
|
|
return strings.Contains(urlValue, ".mp4") ||
|
|
strings.Contains(urlValue, ".mov") ||
|
|
strings.Contains(urlValue, ".webm") ||
|
|
strings.Contains(urlValue, ".m3u8")
|
|
}
|
|
|
|
func (s *Service) probeVideoMetadata(ctx context.Context, rawURL string) (generatedVideoMetadata, error) {
|
|
if _, err := exec.LookPath("ffprobe"); err != nil {
|
|
return generatedVideoMetadata{}, err
|
|
}
|
|
probeURL := rawURL
|
|
if s != nil {
|
|
if resolved, err := s.generatedAssetFetchURL(rawURL); err == nil && strings.TrimSpace(resolved) != "" {
|
|
probeURL = resolved
|
|
}
|
|
}
|
|
probeCtx, cancel := context.WithTimeout(ctx, generatedVideoMetadataProbeTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(
|
|
probeCtx,
|
|
"ffprobe",
|
|
"-v", "error",
|
|
"-show_entries", "format=duration:stream=codec_type",
|
|
"-of", "json",
|
|
probeURL,
|
|
)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return generatedVideoMetadata{}, err
|
|
}
|
|
var probed ffprobeVideoMetadata
|
|
if err := json.Unmarshal(output, &probed); err != nil {
|
|
return generatedVideoMetadata{}, err
|
|
}
|
|
metadata := generatedVideoMetadata{}
|
|
if durationText := strings.TrimSpace(probed.Format.Duration); durationText != "" {
|
|
if duration, err := strconv.ParseFloat(durationText, 64); err == nil && duration > 0 && !math.IsNaN(duration) && !math.IsInf(duration, 0) {
|
|
rounded := math.Round(duration)
|
|
if rounded <= 0 {
|
|
rounded = 1
|
|
}
|
|
metadata.Duration = rounded
|
|
}
|
|
}
|
|
if probed.Streams != nil {
|
|
metadata.HasAudioKnown = true
|
|
for _, stream := range probed.Streams {
|
|
if strings.TrimSpace(stream.CodecType) == "audio" {
|
|
metadata.HasAudio = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if metadata.Duration <= 0 && !metadata.HasAudioKnown {
|
|
return metadata, fmt.Errorf("invalid video metadata: %q", trimForLog(string(output)))
|
|
}
|
|
return metadata, nil
|
|
}
|
|
|
|
func trimForLog(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if len(value) <= 120 {
|
|
return value
|
|
}
|
|
return value[:120] + "..."
|
|
}
|