Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions drivers/local/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type Local struct {
// video thumb position
videoThumbPos float64
videoThumbPosIsPercentage bool
thumbPixel int

// use ffmpeg
useFFmpeg bool
}

func (d *Local) Config() driver.Config {
Expand All @@ -65,6 +69,9 @@ func (d *Local) Init(ctx context.Context) error {
}
d.Addition.RootFolderPath = abs
}

d.useFFmpeg = d.UseFFmpeg

if d.ThumbCacheFolder != "" && !utils.Exists(d.ThumbCacheFolder) {
err := os.MkdirAll(d.ThumbCacheFolder, os.FileMode(d.mkdirPerm))
if err != nil {
Expand All @@ -78,6 +85,14 @@ func (d *Local) Init(ctx context.Context) error {
}
d.thumbConcurrency = int(v)
}
if d.ThumbPixel != "" {
v, err := strconv.ParseUint(d.ThumbPixel, 10, 32)
if err != nil {
return err
}
d.thumbPixel = int(v)
}

if d.thumbConcurrency == 0 {
d.thumbTokenBucket = NewNopTokenBucket()
} else {
Expand Down
2 changes: 2 additions & 0 deletions drivers/local/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
type Addition struct {
driver.RootPath
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
UseFFmpeg bool `json:"use_ffmpeg" required:"true" help:"use ffmpeg to generate thumbnail"`
ThumbCacheFolder string `json:"thumb_cache_folder"`
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."`
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."`
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."`
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
MkdirPerm string `json:"mkdir_perm" default:"777"`
Expand Down
114 changes: 96 additions & 18 deletions drivers/local/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,87 @@ func isSymlinkDir(f fs.FileInfo, path string) bool {
return false
}

// resizeImageToBufferWithFFmpegGo 使用 ffmpeg-go 调整图片大小并输出到内存缓冲区
func resizeImageToBufferWithFFmpegGo(inputFile string, width int, outputFormat string /* e.g., "image2pipe", "png_pipe", "mjpeg" */) (*bytes.Buffer, error) {
outBuffer := bytes.NewBuffer(nil)

// Determine codec based on desired output format for piping
// For generic image piping, 'image2' is often used with -f image2pipe
// For specific formats to buffer, you might specify the codec directly
var vcodec string
switch outputFormat {
case "png_pipe": // if you want to ensure PNG format in buffer
vcodec = "png"
case "mjpeg": // if you want to ensure JPEG format in buffer
vcodec = "mjpeg"
// default or "image2pipe" could leave codec choice more to ffmpeg or require -c:v later
}

outputArgs := ffmpeg.KwArgs{
"vf": fmt.Sprintf("scale=%d:-1:flags=lanczos,format=yuv444p", width),
"vframes": "1",
"f": outputFormat, // Format for piping (e.g., image2pipe, png_pipe)
}
if vcodec != "" {
outputArgs["vcodec"] = vcodec
}
if outputFormat == "mjpeg" {
outputArgs["q:v"] = "3"
}

err := ffmpeg.Input(inputFile).
Output("pipe:", outputArgs). // Output to pipe (stdout)
GlobalArgs("-loglevel", "error").
Silent(true). // Suppress ffmpeg's own console output
WithOutput(outBuffer, os.Stderr). // Capture stdout to outBuffer, stderr to os.Stderr
// ErrorToStdOut(). // Alternative: send ffmpeg's stderr to Go's stdout
Run()

if err != nil {
return nil, fmt.Errorf("ffmpeg-go failed to resize image %s to buffer: %w", inputFile, err)
}
if outBuffer.Len() == 0 {
return nil, fmt.Errorf("ffmpeg-go produced empty buffer for %s", inputFile)
}

return outBuffer, nil
}

func generateThumbnailWithImagingOptimized(imagePath string, targetWidth int, quality int) (*bytes.Buffer, error) {

file, err := os.Open(imagePath)
if err != nil {
return nil, fmt.Errorf("failed to open image: %w", err)
}
defer file.Close()

img, err := imaging.Decode(file, imaging.AutoOrientation(true))
if err != nil {
return nil, fmt.Errorf("failed to decode image: %w", err)
}

thumbImg := imaging.Resize(img, targetWidth, 0, imaging.Lanczos)
img = nil

var buf bytes.Buffer
// imaging.Encode
// imaging.PNG, imaging.JPEG, imaging.GIF, imaging.BMP, imaging.TIFF
outputFormat := imaging.JPEG
encodeOptions := []imaging.EncodeOption{imaging.JPEGQuality(quality)}

// outputFormat := imaging.PNG
// encodeOptions := []imaging.EncodeOption{}

err = imaging.Encode(&buf, thumbImg, outputFormat, encodeOptions...)
if err != nil {
return nil, fmt.Errorf("failed to encode thumbnail: %w", err)
}

thumbImg = nil

return &buf, nil
}

// Get the snapshot of the video
func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error) {
// Run ffprobe to get the video duration
Expand Down Expand Up @@ -80,7 +161,7 @@ func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error)
// The "noaccurate_seek" option prevents this error and would also speed up
// the seek process.
stream := ffmpeg.Input(videoPath, ffmpeg.KwArgs{"ss": ss, "noaccurate_seek": ""}).
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg", "vf": fmt.Sprintf("scale=%d:-1:flags=lanczos", d.thumbPixel)}).
GlobalArgs("-loglevel", "error").Silent(true).
WithOutput(srcBuf, os.Stdout)
if err = stream.Run(); err != nil {
Expand Down Expand Up @@ -125,29 +206,26 @@ func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
}
srcBuf = videoBuf
} else {
imgData, err := os.ReadFile(fullPath)
if err != nil {
return nil, nil, err
if d.useFFmpeg {
imgData, err := resizeImageToBufferWithFFmpegGo(fullPath, d.thumbPixel, "image2pipe")
srcBuf = imgData
if err != nil {
return nil, nil, err
}
} else {
imgData, err := generateThumbnailWithImagingOptimized(fullPath, d.thumbPixel, 70)
srcBuf = imgData
if err != nil {
return nil, nil, err
}
}
imgBuf := bytes.NewBuffer(imgData)
srcBuf = imgBuf
}

image, err := imaging.Decode(srcBuf, imaging.AutoOrientation(true))
if err != nil {
return nil, nil, err
}
thumbImg := imaging.Resize(image, 144, 0, imaging.Lanczos)
var buf bytes.Buffer
err = imaging.Encode(&buf, thumbImg, imaging.PNG)
if err != nil {
return nil, nil, err
}
if d.ThumbCacheFolder != "" {
err = os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), buf.Bytes(), 0666)
err := os.WriteFile(filepath.Join(d.ThumbCacheFolder, thumbName), srcBuf.Bytes(), 0666)
if err != nil {
return nil, nil, err
}
}
return &buf, nil, nil
return srcBuf, nil, nil
}
Loading