Use ffprobe json parsing for thumbnail video info
This commit is contained in:
parent
799102cac7
commit
eff752a97c
|
|
@ -11,21 +11,21 @@ import (
|
||||||
|
|
||||||
// Config contains configuration for thumbnail generation
|
// Config contains configuration for thumbnail generation
|
||||||
type Config struct {
|
type Config struct {
|
||||||
VideoPath string
|
VideoPath string
|
||||||
OutputDir string
|
OutputDir string
|
||||||
Count int // Number of thumbnails to generate
|
Count int // Number of thumbnails to generate
|
||||||
Interval float64 // Interval in seconds between thumbnails (alternative to Count)
|
Interval float64 // Interval in seconds between thumbnails (alternative to Count)
|
||||||
Width int // Thumbnail width (0 = auto based on height)
|
Width int // Thumbnail width (0 = auto based on height)
|
||||||
Height int // Thumbnail height (0 = auto based on width)
|
Height int // Thumbnail height (0 = auto based on width)
|
||||||
Quality int // JPEG quality 1-100 (0 = PNG lossless)
|
Quality int // JPEG quality 1-100 (0 = PNG lossless)
|
||||||
Format string // "png" or "jpg"
|
Format string // "png" or "jpg"
|
||||||
StartOffset float64 // Start generating from this timestamp
|
StartOffset float64 // Start generating from this timestamp
|
||||||
EndOffset float64 // Stop generating before this timestamp
|
EndOffset float64 // Stop generating before this timestamp
|
||||||
ContactSheet bool // Generate a single contact sheet instead of individual files
|
ContactSheet bool // Generate a single contact sheet instead of individual files
|
||||||
Columns int // Contact sheet columns (if ContactSheet=true)
|
Columns int // Contact sheet columns (if ContactSheet=true)
|
||||||
Rows int // Contact sheet rows (if ContactSheet=true)
|
Rows int // Contact sheet rows (if ContactSheet=true)
|
||||||
ShowTimestamp bool // Overlay timestamp on thumbnails
|
ShowTimestamp bool // Overlay timestamp on thumbnails
|
||||||
ShowMetadata bool // Show metadata header on contact sheet
|
ShowMetadata bool // Show metadata header on contact sheet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generator creates thumbnails from videos
|
// Generator creates thumbnails from videos
|
||||||
|
|
@ -151,7 +151,7 @@ func (g *Generator) getVideoInfo(ctx context.Context, videoPath string) (duratio
|
||||||
"-select_streams", "v:0",
|
"-select_streams", "v:0",
|
||||||
"-show_entries", "stream=width,height,duration",
|
"-show_entries", "stream=width,height,duration",
|
||||||
"-show_entries", "format=duration",
|
"-show_entries", "format=duration",
|
||||||
"-of", "default=noprint_wrappers=1",
|
"-of", "json",
|
||||||
videoPath,
|
videoPath,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -160,18 +160,47 @@ func (g *Generator) getVideoInfo(ctx context.Context, videoPath string) (duratio
|
||||||
return 0, 0, 0, fmt.Errorf("ffprobe failed: %w", err)
|
return 0, 0, 0, fmt.Errorf("ffprobe failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse output
|
// Parse JSON for robust extraction
|
||||||
var w, h int
|
type streamInfo struct {
|
||||||
var d float64
|
Width int `json:"width"`
|
||||||
_, _ = fmt.Sscanf(string(output), "width=%d\nheight=%d\nduration=%f", &w, &h, &d)
|
Height int `json:"height"`
|
||||||
|
Duration string `json:"duration"`
|
||||||
// If stream duration not available, try format duration
|
}
|
||||||
if d == 0 {
|
type formatInfo struct {
|
||||||
_, _ = fmt.Sscanf(string(output), "width=%d\nheight=%d\nwidth=%*d\nheight=%*d\nduration=%f", &w, &h, &d)
|
Duration string `json:"duration"`
|
||||||
|
}
|
||||||
|
type ffprobeResp struct {
|
||||||
|
Streams []streamInfo `json:"streams"`
|
||||||
|
Format formatInfo `json:"format"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if w == 0 || h == 0 || d == 0 {
|
var resp ffprobeResp
|
||||||
return 0, 0, 0, fmt.Errorf("failed to parse video info")
|
if err := json.Unmarshal(output, &resp); err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("failed to parse ffprobe json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var w, h int
|
||||||
|
var d float64
|
||||||
|
if len(resp.Streams) > 0 {
|
||||||
|
w = resp.Streams[0].Width
|
||||||
|
h = resp.Streams[0].Height
|
||||||
|
if resp.Streams[0].Duration != "" {
|
||||||
|
if val, err := strconv.ParseFloat(resp.Streams[0].Duration, 64); err == nil {
|
||||||
|
d = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d == 0 && resp.Format.Duration != "" {
|
||||||
|
if val, err := strconv.ParseFloat(resp.Format.Duration, 64); err == nil {
|
||||||
|
d = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if w == 0 || h == 0 {
|
||||||
|
return 0, 0, 0, fmt.Errorf("failed to parse video info (missing width/height)")
|
||||||
|
}
|
||||||
|
if d == 0 {
|
||||||
|
return 0, 0, 0, fmt.Errorf("failed to parse video info (missing duration)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return d, w, h, nil
|
return d, w, h, nil
|
||||||
|
|
@ -466,9 +495,9 @@ func (g *Generator) buildMetadataFilter(config Config, duration float64, thumbWi
|
||||||
// App background color: #0B0F1A (dark navy blue)
|
// App background color: #0B0F1A (dark navy blue)
|
||||||
filter := fmt.Sprintf(
|
filter := fmt.Sprintf(
|
||||||
"%s,%s,pad=%d:%d:0:%d:0x0B0F1A,"+
|
"%s,%s,pad=%d:%d:0:%d:0x0B0F1A,"+
|
||||||
"drawtext=text='%s':fontcolor=white:fontsize=13:font='DejaVu Sans Mono':x=10:y=10,"+
|
"drawtext=text='%s':fontcolor=white:fontsize=13:font='DejaVu Sans Mono':x=10:y=10,"+
|
||||||
"drawtext=text='%s':fontcolor=white:fontsize=12:font='DejaVu Sans Mono':x=10:y=35,"+
|
"drawtext=text='%s':fontcolor=white:fontsize=12:font='DejaVu Sans Mono':x=10:y=35,"+
|
||||||
"drawtext=text='%s':fontcolor=white:fontsize=11:font='DejaVu Sans Mono':x=10:y=60",
|
"drawtext=text='%s':fontcolor=white:fontsize=11:font='DejaVu Sans Mono':x=10:y=60",
|
||||||
selectFilter,
|
selectFilter,
|
||||||
tileFilter,
|
tileFilter,
|
||||||
sheetWidth,
|
sheetWidth,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user