Skip to content

Commit 17f6b98

Browse files
Copilotanacrolix
andcommitted
Add thumbnail.go with ffmpeg-based thumbnail generator
Co-authored-by: anacrolix <[email protected]>
1 parent 00df681 commit 17f6b98

File tree

2 files changed

+108
-1
lines changed

2 files changed

+108
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Binaries for programs and plugins
2-
dms
2+
/dms
33
*.exe
44
*.exe~
55
*.dll

dlna/dms/thumbnail.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package dms
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"strconv"
10+
11+
"github.com/anacrolix/ffprobe"
12+
)
13+
14+
// generateThumbnailFFmpeg generates a thumbnail using ffmpeg instead of ffmpegthumbnailer.
15+
// This provides better cross-platform compatibility, especially on Windows.
16+
func generateThumbnailFFmpeg(ctx context.Context, inputPath string) ([]byte, error) {
17+
// Get video duration to calculate the seek position
18+
ss, err := getThumbnailSeekPosition(ctx, inputPath)
19+
if err != nil {
20+
return nil, fmt.Errorf("determining seek position: %w", err)
21+
}
22+
23+
// Build ffmpeg arguments for thumbnail generation
24+
args := []string{
25+
"-xerror",
26+
"-loglevel", "warning",
27+
"-ss", ss,
28+
"-i", inputPath,
29+
"-vf", "thumbnail",
30+
"-frames:v", "1",
31+
"-f", "image2",
32+
"-c:v", "png",
33+
"pipe:",
34+
}
35+
36+
cmd := exec.Command("ffmpeg", args...)
37+
cmd.Stderr = os.Stderr
38+
39+
// Start the command
40+
stdout, err := cmd.StdoutPipe()
41+
if err != nil {
42+
return nil, fmt.Errorf("creating stdout pipe: %w", err)
43+
}
44+
45+
if err := cmd.Start(); err != nil {
46+
return nil, fmt.Errorf("starting ffmpeg: %w", err)
47+
}
48+
49+
// Set up context cancellation
50+
go func() {
51+
<-ctx.Done()
52+
if cmd.Process != nil {
53+
cmd.Process.Kill()
54+
}
55+
}()
56+
57+
// Read the thumbnail data
58+
thumbnailData, err := io.ReadAll(stdout)
59+
if err != nil {
60+
return nil, fmt.Errorf("reading thumbnail data: %w", err)
61+
}
62+
63+
// Wait for command to finish
64+
if err := cmd.Wait(); err != nil {
65+
if ctx.Err() != nil {
66+
return nil, ctx.Err()
67+
}
68+
return nil, fmt.Errorf("ffmpeg command failed: %w", err)
69+
}
70+
71+
return thumbnailData, nil
72+
}
73+
74+
// getThumbnailSeekPosition calculates the seek position for thumbnail extraction.
75+
// It seeks to 1/4 of the video duration by default.
76+
func getThumbnailSeekPosition(ctx context.Context, inputPath string) (string, error) {
77+
// Use ffprobe to get video duration
78+
pc, err := ffprobe.Start(inputPath)
79+
if err != nil {
80+
// If ffprobe fails, default to 10 seconds
81+
return "10", nil
82+
}
83+
84+
select {
85+
case <-ctx.Done():
86+
if pc.Cmd.Process != nil {
87+
pc.Cmd.Process.Kill()
88+
}
89+
return "", ctx.Err()
90+
case <-pc.Done:
91+
}
92+
93+
if pc.Err != nil {
94+
// If ffprobe fails, default to 10 seconds
95+
return "10", nil
96+
}
97+
98+
duration, err := pc.Info.Duration()
99+
if err != nil {
100+
// If duration cannot be determined, default to 10 seconds
101+
return "10", nil
102+
}
103+
104+
// Seek to 1/4 of the duration
105+
seekDuration := duration / 4
106+
return strconv.FormatFloat(seekDuration.Seconds(), 'f', -1, 64), nil
107+
}

0 commit comments

Comments
 (0)