Skip to content

Commit 60c203b

Browse files
committed
feat(local): add options to use ffmpeg to generate thumbnail
1 parent ffa03bf commit 60c203b

File tree

3 files changed

+113
-18
lines changed

3 files changed

+113
-18
lines changed

drivers/local/driver.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ type Local struct {
3939
// video thumb position
4040
videoThumbPos float64
4141
videoThumbPosIsPercentage bool
42+
thumbPixel int
43+
44+
// use ffmpeg
45+
useFFmpeg bool
4246
}
4347

4448
func (d *Local) Config() driver.Config {
@@ -65,6 +69,9 @@ func (d *Local) Init(ctx context.Context) error {
6569
}
6670
d.Addition.RootFolderPath = abs
6771
}
72+
73+
d.useFFmpeg = d.UseFFmpeg
74+
6875
if d.ThumbCacheFolder != "" && !utils.Exists(d.ThumbCacheFolder) {
6976
err := os.MkdirAll(d.ThumbCacheFolder, os.FileMode(d.mkdirPerm))
7077
if err != nil {
@@ -78,6 +85,14 @@ func (d *Local) Init(ctx context.Context) error {
7885
}
7986
d.thumbConcurrency = int(v)
8087
}
88+
if d.ThumbPixel != "" {
89+
v, err := strconv.ParseUint(d.ThumbPixel, 10, 32)
90+
if err != nil {
91+
return err
92+
}
93+
d.thumbPixel = int(v)
94+
}
95+
8196
if d.thumbConcurrency == 0 {
8297
d.thumbTokenBucket = NewNopTokenBucket()
8398
} else {

drivers/local/meta.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
type Addition struct {
99
driver.RootPath
1010
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
11+
UseFFmpeg bool `json:"use_ffmpeg" required:"true" help:"use ffmpeg to generate thumbnail"`
1112
ThumbCacheFolder string `json:"thumb_cache_folder"`
1213
ThumbConcurrency string `json:"thumb_concurrency" default:"16" required:"false" help:"Number of concurrent thumbnail generation goroutines. This controls how many thumbnails can be generated in parallel."`
14+
ThumbPixel string `json:"thumb_pixel" default:"320" required:"false" help:"Specifies the target width for image thumbnails in pixels. The height of the thumbnail will be calculated automatically to maintain the original aspect ratio of the image."`
1315
VideoThumbPos string `json:"video_thumb_pos" default:"20%" required:"false" help:"The position of the video thumbnail. If the value is a number (integer ot floating point), it represents the time in seconds. If the value ends with '%', it represents the percentage of the video duration."`
1416
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
1517
MkdirPerm string `json:"mkdir_perm" default:"777"`

drivers/local/util.go

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,87 @@ func isSymlinkDir(f fs.FileInfo, path string) bool {
3636
return false
3737
}
3838

39+
// resizeImageToBufferWithFFmpegGo 使用 ffmpeg-go 调整图片大小并输出到内存缓冲区
40+
func resizeImageToBufferWithFFmpegGo(inputFile string, width int, outputFormat string /* e.g., "image2pipe", "png_pipe", "mjpeg" */) (*bytes.Buffer, error) {
41+
outBuffer := bytes.NewBuffer(nil)
42+
43+
// Determine codec based on desired output format for piping
44+
// For generic image piping, 'image2' is often used with -f image2pipe
45+
// For specific formats to buffer, you might specify the codec directly
46+
var vcodec string
47+
switch outputFormat {
48+
case "png_pipe": // if you want to ensure PNG format in buffer
49+
vcodec = "png"
50+
case "mjpeg": // if you want to ensure JPEG format in buffer
51+
vcodec = "mjpeg"
52+
// default or "image2pipe" could leave codec choice more to ffmpeg or require -c:v later
53+
}
54+
55+
outputArgs := ffmpeg.KwArgs{
56+
"vf": fmt.Sprintf("scale=%d:-1:flags=lanczos,format=yuv444p", width),
57+
"vframes": "1",
58+
"f": outputFormat, // Format for piping (e.g., image2pipe, png_pipe)
59+
}
60+
if vcodec != "" {
61+
outputArgs["vcodec"] = vcodec
62+
}
63+
if outputFormat == "mjpeg" {
64+
outputArgs["q:v"] = "3"
65+
}
66+
67+
err := ffmpeg.Input(inputFile).
68+
Output("pipe:", outputArgs). // Output to pipe (stdout)
69+
GlobalArgs("-loglevel", "error").
70+
Silent(true). // Suppress ffmpeg's own console output
71+
WithOutput(outBuffer, os.Stderr). // Capture stdout to outBuffer, stderr to os.Stderr
72+
// ErrorToStdOut(). // Alternative: send ffmpeg's stderr to Go's stdout
73+
Run()
74+
75+
if err != nil {
76+
return nil, fmt.Errorf("ffmpeg-go failed to resize image %s to buffer: %w", inputFile, err)
77+
}
78+
if outBuffer.Len() == 0 {
79+
return nil, fmt.Errorf("ffmpeg-go produced empty buffer for %s", inputFile)
80+
}
81+
82+
return outBuffer, nil
83+
}
84+
85+
func generateThumbnailWithImagingOptimized(imagePath string, targetWidth int, quality int) (*bytes.Buffer, error) {
86+
87+
file, err := os.Open(imagePath)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to open image: %w", err)
90+
}
91+
defer file.Close()
92+
93+
img, err := imaging.Decode(file, imaging.AutoOrientation(true))
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to decode image: %w", err)
96+
}
97+
98+
thumbImg := imaging.Resize(img, targetWidth, 0, imaging.Lanczos)
99+
img = nil
100+
101+
var buf bytes.Buffer
102+
// imaging.Encode
103+
// imaging.PNG, imaging.JPEG, imaging.GIF, imaging.BMP, imaging.TIFF
104+
outputFormat := imaging.JPEG
105+
encodeOptions := []imaging.EncodeOption{imaging.JPEGQuality(quality)}
106+
107+
// outputFormat := imaging.PNG
108+
// encodeOptions := []imaging.EncodeOption{}
109+
110+
err = imaging.Encode(&buf, thumbImg, outputFormat, encodeOptions...)
111+
if err != nil {
112+
return nil, fmt.Errorf("failed to encode thumbnail: %w", err)
113+
}
114+
115+
thumbImg = nil
116+
117+
return &buf, nil
118+
}
119+
39120
// Get the snapshot of the video
40121
func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error) {
41122
// Run ffprobe to get the video duration
@@ -80,7 +161,7 @@ func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error)
80161
// The "noaccurate_seek" option prevents this error and would also speed up
81162
// the seek process.
82163
stream := ffmpeg.Input(videoPath, ffmpeg.KwArgs{"ss": ss, "noaccurate_seek": ""}).
83-
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
164+
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg", "vf": fmt.Sprintf("scale=%d:-1:flags=lanczos", d.thumbPixel)}).
84165
GlobalArgs("-loglevel", "error").Silent(true).
85166
WithOutput(srcBuf, os.Stdout)
86167
if err = stream.Run(); err != nil {
@@ -125,29 +206,26 @@ func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
125206
}
126207
srcBuf = videoBuf
127208
} else {
128-
imgData, err := os.ReadFile(fullPath)
129-
if err != nil {
130-
return nil, nil, err
209+
if d.useFFmpeg {
210+
imgData, err := resizeImageToBufferWithFFmpegGo(fullPath, d.thumbPixel, "image2pipe")
211+
srcBuf = imgData
212+
if err != nil {
213+
return nil, nil, err
214+
}
215+
} else {
216+
imgData, err := generateThumbnailWithImagingOptimized(fullPath, d.thumbPixel, 70)
217+
srcBuf = imgData
218+
if err != nil {
219+
return nil, nil, err
220+
}
131221
}
132-
imgBuf := bytes.NewBuffer(imgData)
133-
srcBuf = imgBuf
134222
}
135223

136-
image, err := imaging.Decode(srcBuf, imaging.AutoOrientation(true))
137-
if err != nil {
138-
return nil, nil, err
139-
}
140-
thumbImg := imaging.Resize(image, 144, 0, imaging.Lanczos)
141-
var buf bytes.Buffer
142-
err = imaging.Encode(&buf, thumbImg, imaging.PNG)
143-
if err != nil {
144-
return nil, nil, err
145-
}
146224
if d.ThumbCacheFolder != "" {
147-
err = os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), buf.Bytes(), 0666)
225+
err := os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), srcBuf.Bytes(), 0666)
148226
if err != nil {
149227
return nil, nil, err
150228
}
151229
}
152-
return &buf, nil, nil
230+
return srcBuf, nil, nil
153231
}

0 commit comments

Comments
 (0)