mirror of https://github.com/Xhofe/alist
feat(local): add options to use ffmpeg to generate thumbnail
parent
ffa03bfda1
commit
60c203b354
|
@ -39,6 +39,10 @@ type Local struct {
|
||||||
// video thumb position
|
// video thumb position
|
||||||
videoThumbPos float64
|
videoThumbPos float64
|
||||||
videoThumbPosIsPercentage bool
|
videoThumbPosIsPercentage bool
|
||||||
|
thumbPixel int
|
||||||
|
|
||||||
|
// use ffmpeg
|
||||||
|
useFFmpeg bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Local) Config() driver.Config {
|
func (d *Local) Config() driver.Config {
|
||||||
|
@ -65,6 +69,9 @@ func (d *Local) Init(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
d.Addition.RootFolderPath = abs
|
d.Addition.RootFolderPath = abs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.useFFmpeg = d.UseFFmpeg
|
||||||
|
|
||||||
if d.ThumbCacheFolder != "" && !utils.Exists(d.ThumbCacheFolder) {
|
if d.ThumbCacheFolder != "" && !utils.Exists(d.ThumbCacheFolder) {
|
||||||
err := os.MkdirAll(d.ThumbCacheFolder, os.FileMode(d.mkdirPerm))
|
err := os.MkdirAll(d.ThumbCacheFolder, os.FileMode(d.mkdirPerm))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -78,6 +85,14 @@ func (d *Local) Init(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
d.thumbConcurrency = int(v)
|
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 {
|
if d.thumbConcurrency == 0 {
|
||||||
d.thumbTokenBucket = NewNopTokenBucket()
|
d.thumbTokenBucket = NewNopTokenBucket()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,8 +8,10 @@ import (
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootPath
|
driver.RootPath
|
||||||
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
|
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"`
|
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."`
|
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."`
|
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"`
|
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
|
||||||
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
||||||
|
|
|
@ -36,6 +36,87 @@ func isSymlinkDir(f fs.FileInfo, path string) bool {
|
||||||
return false
|
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
|
// Get the snapshot of the video
|
||||||
func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error) {
|
func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error) {
|
||||||
// Run ffprobe to get the video duration
|
// Run ffprobe to get the video duration
|
||||||
|
@ -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 "noaccurate_seek" option prevents this error and would also speed up
|
||||||
// the seek process.
|
// the seek process.
|
||||||
stream := ffmpeg.Input(videoPath, ffmpeg.KwArgs{"ss": ss, "noaccurate_seek": ""}).
|
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).
|
GlobalArgs("-loglevel", "error").Silent(true).
|
||||||
WithOutput(srcBuf, os.Stdout)
|
WithOutput(srcBuf, os.Stdout)
|
||||||
if err = stream.Run(); err != nil {
|
if err = stream.Run(); err != nil {
|
||||||
|
@ -125,29 +206,26 @@ func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
|
||||||
}
|
}
|
||||||
srcBuf = videoBuf
|
srcBuf = videoBuf
|
||||||
} else {
|
} else {
|
||||||
imgData, err := os.ReadFile(fullPath)
|
if d.useFFmpeg {
|
||||||
if err != nil {
|
imgData, err := resizeImageToBufferWithFFmpegGo(fullPath, d.thumbPixel, "image2pipe")
|
||||||
return nil, nil, err
|
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 != "" {
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &buf, nil, nil
|
return srcBuf, nil, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue