From 98b86b37defe56619039de399caebbe279bb7b23 Mon Sep 17 00:00:00 2001 From: Aaron Liu <912394456@qq.com> Date: Fri, 7 Apr 2023 19:30:41 +0800 Subject: [PATCH] feat(thumb): use ffmpeg to generate thumb --- models/defaults.go | 2 + pkg/filesystem/image.go | 8 +++- pkg/thumb/builtin.go | 2 +- pkg/thumb/ffmpeg.go | 94 +++++++++++++++++++++++++++++++++++++++++ pkg/thumb/pipeline.go | 8 ++-- pkg/thumb/vips.go | 6 +-- 6 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 pkg/thumb/ffmpeg.go diff --git a/models/defaults.go b/models/defaults.go index e1031fb..339c055 100644 --- a/models/defaults.go +++ b/models/defaults.go @@ -113,6 +113,8 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti {Name: "thumb_vips_path", Value: "vips", Type: "thumb"}, {Name: "thumb_vips_exts", Value: "csv,mat,img,hdr,pbm,pgm,ppm,pfm,pnm,svg,svgz,j2k,jp2,jpt,j2c,jpc,gif,png,jpg,jpeg,jpe,webp,tif,tiff,fits,fit,fts,exr,jxl,pdf,heic,heif,avif,svs,vms,vmu,ndpi,scn,mrxs,svslide,bif,raw", Type: "thumb"}, {Name: "thumb_ffmpeg_path", Value: "ffmpeg", Type: "thumb"}, + {Name: "thumb_ffmpeg_exts", Value: "3g2,3gp,asf,asx,avi,divx,flv,m2ts,m2v,m4v,mkv,mov,mp4,mpeg,mpg,mts,mxf,ogv,rm,swf,webm,wmv", Type: "thumb"}, + {Name: "thumb_ffmpeg_seek", Value: "00:00:01.00", Type: "thumb"}, {Name: "thumb_proxy_enabled", Value: "0", Type: "thumb"}, {Name: "thumb_proxy_policy", Value: "[]", Type: "thumb"}, {Name: "thumb_max_src_size", Value: "31457280", Type: "thumb"}, diff --git a/pkg/filesystem/image.go b/pkg/filesystem/image.go index b861d2b..b6ba66e 100644 --- a/pkg/filesystem/image.go +++ b/pkg/filesystem/image.go @@ -137,7 +137,13 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) e } defer source.Close() - thumbPath, err := thumb.Generators.Generate(ctx, source, file.Name, model.GetSettingByNames( + // Provide file source path for local policy files + src := "" + if file.GetPolicy().Type == "local" { + src = file.SourceName + } + + thumbPath, err := thumb.Generators.Generate(ctx, source, src, file.Name, model.GetSettingByNames( "thumb_width", "thumb_height", "thumb_builtin_enabled", diff --git a/pkg/thumb/builtin.go b/pkg/thumb/builtin.go index 2de4952..0fdd771 100644 --- a/pkg/thumb/builtin.go +++ b/pkg/thumb/builtin.go @@ -157,7 +157,7 @@ func (image *Thumb) CreateAvatar(uid uint) error { type Builtin struct{} -func (b Builtin) Generate(ctx context.Context, file io.Reader, name string, options map[string]string) (string, error) { +func (b Builtin) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (string, error) { img, err := NewThumbFromFile(file, name) if err != nil { return "", err diff --git a/pkg/thumb/ffmpeg.go b/pkg/thumb/ffmpeg.go new file mode 100644 index 0000000..004cd27 --- /dev/null +++ b/pkg/thumb/ffmpeg.go @@ -0,0 +1,94 @@ +package thumb + +import ( + "bytes" + "context" + "fmt" + model "github.com/cloudreve/Cloudreve/v3/models" + "github.com/cloudreve/Cloudreve/v3/pkg/util" + "github.com/gofrs/uuid" + "io" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func init() { + RegisterGenerator(&FfmpegGenerator{}) +} + +type FfmpegGenerator struct { + exts []string + lastRawExts string +} + +func (f *FfmpegGenerator) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (string, error) { + ffmpegOpts := model.GetSettingByNames("thumb_ffmpeg_path", "thumb_ffmpeg_exts", "thumb_ffmpeg_seek", "thumb_encode_method", "temp_path") + + if f.lastRawExts != ffmpegOpts["thumb_ffmpeg_exts"] { + f.exts = strings.Split(ffmpegOpts["thumb_ffmpeg_exts"], ",") + } + + if !util.IsInExtensionList(f.exts, name) { + return "", fmt.Errorf("unsupported video format: %w", ErrPassThrough) + } + + tempOutputPath := filepath.Join( + util.RelativePath(ffmpegOpts["temp_path"]), + "thumb", + fmt.Sprintf("thumb_%s.%s", uuid.Must(uuid.NewV4()).String(), ffmpegOpts["thumb_encode_method"]), + ) + + tempInputPath := src + if tempInputPath == "" { + // If not local policy files, download to temp folder + tempInputPath = filepath.Join( + util.RelativePath(ffmpegOpts["temp_path"]), + "thumb", + fmt.Sprintf("ffmpeg_%s%s", uuid.Must(uuid.NewV4()).String(), filepath.Ext(name)), + ) + + // Due to limitations of ffmpeg, we need to write the input file to disk first + tempInputFile, err := util.CreatNestedFile(tempInputPath) + if err != nil { + return "", fmt.Errorf("failed to create temp file: %w", err) + } + + defer os.Remove(tempInputPath) + defer tempInputFile.Close() + + if _, err = io.Copy(tempInputFile, file); err != nil { + return "", fmt.Errorf("failed to write input file: %w", err) + } + + tempInputFile.Close() + } + + // Invoke ffmpeg + scaleOpt := fmt.Sprintf("scale=%s:%s:force_original_aspect_ratio=decrease", options["thumb_width"], options["thumb_height"]) + inputFormat := filepath.Ext(name)[1:] + cmd := exec.CommandContext(ctx, + ffmpegOpts["thumb_ffmpeg_path"], "-ss", ffmpegOpts["thumb_ffmpeg_seek"], "-f", inputFormat, "-i", tempInputPath, + "-vf", scaleOpt, "-vframes", "1", tempOutputPath) + + // Redirect IO + var stdErr bytes.Buffer + cmd.Stdin = file + cmd.Stderr = &stdErr + + if err := cmd.Run(); err != nil { + util.Log().Warning("Failed to invoke ffmpeg: %s", stdErr.String()) + return "", fmt.Errorf("failed to invoke ffmpeg: %w", err) + } + + return tempOutputPath, nil +} + +func (f *FfmpegGenerator) Priority() int { + return 200 +} + +func (f *FfmpegGenerator) EnableFlag() string { + return "thumb_ffmpeg_enabled" +} diff --git a/pkg/thumb/pipeline.go b/pkg/thumb/pipeline.go index 676f8cf..bf5705d 100644 --- a/pkg/thumb/pipeline.go +++ b/pkg/thumb/pipeline.go @@ -13,7 +13,9 @@ import ( // Generator generates a thumbnail for a given reader. type Generator interface { - Generate(ctx context.Context, file io.Reader, name string, options map[string]string) (string, error) + // Generate generates a thumbnail for a given reader. Src is the original file path, only provided + // for local policy files. + Generate(ctx context.Context, file io.Reader, src string, name string, options map[string]string) (string, error) // Priority of execution order, smaller value means higher priority. Priority() int @@ -52,10 +54,10 @@ func RegisterGenerator(generator Generator) { sort.Sort(Generators) } -func (p GeneratorList) Generate(ctx context.Context, file io.Reader, name string, options map[string]string) (string, error) { +func (p GeneratorList) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (string, error) { for _, generator := range p { if model.IsTrueVal(options[generator.EnableFlag()]) { - res, err := generator.Generate(ctx, file, name, options) + res, err := generator.Generate(ctx, file, src, name, options) if errors.Is(err, ErrPassThrough) { util.Log().Debug("Failed to generate thumbnail for %s: %s, passing through to next generator.", name, err) continue diff --git a/pkg/thumb/vips.go b/pkg/thumb/vips.go index 3908e4c..2450617 100644 --- a/pkg/thumb/vips.go +++ b/pkg/thumb/vips.go @@ -22,7 +22,7 @@ type VipsGenerator struct { lastRawExts string } -func (v VipsGenerator) Generate(ctx context.Context, file io.Reader, name string, options map[string]string) (string, error) { +func (v *VipsGenerator) Generate(ctx context.Context, file io.Reader, src, name string, options map[string]string) (string, error) { vipsOpts := model.GetSettingByNames("thumb_vips_path", "thumb_vips_exts", "thumb_encode_quality", "thumb_encode_method", "temp_path") if v.lastRawExts != vipsOpts["thumb_vips_exts"] { @@ -69,10 +69,10 @@ func (v VipsGenerator) Generate(ctx context.Context, file io.Reader, name string return tempPath, nil } -func (v VipsGenerator) Priority() int { +func (v *VipsGenerator) Priority() int { return 100 } -func (v VipsGenerator) EnableFlag() string { +func (v *VipsGenerator) EnableFlag() string { return "thumb_vips_enabled" }