Skip to content

Commit 31a7470

Browse files
authored
feat(local): support both time and percent for video thumbnail (#7802)
* feat(local): support percent for video thumbnail The percentage determines the point in the video (as a percentage of the total duration) at which the thumbnail will be generated. * feat(local): support both time and percent for video thumbnail
1 parent 687124c commit 31a7470

File tree

3 files changed

+77
-4
lines changed

3 files changed

+77
-4
lines changed

drivers/local/driver.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ func (d *Local) Init(ctx context.Context) error {
7979
} else {
8080
d.thumbTokenBucket = NewStaticTokenBucketWithMigration(d.thumbTokenBucket, d.thumbConcurrency)
8181
}
82+
// Check the VideoThumbPos value
83+
if d.VideoThumbPos == "" {
84+
d.VideoThumbPos = "20%"
85+
}
86+
if strings.HasSuffix(d.VideoThumbPos, "%") {
87+
percentage := strings.TrimSuffix(d.VideoThumbPos, "%")
88+
val, err := strconv.ParseFloat(percentage, 64)
89+
if err != nil {
90+
return fmt.Errorf("invalid video_thumb_pos value: %s, err: %s", d.VideoThumbPos, err)
91+
}
92+
if val < 0 || val > 100 {
93+
return fmt.Errorf("invalid video_thumb_pos value: %s, the precentage must be a number between 0 and 100", d.VideoThumbPos)
94+
}
95+
} else {
96+
val, err := strconv.ParseFloat(d.VideoThumbPos, 64)
97+
if err != nil {
98+
return fmt.Errorf("invalid video_thumb_pos value: %s, err: %s", d.VideoThumbPos, err)
99+
}
100+
if val < 0 {
101+
return fmt.Errorf("invalid video_thumb_pos value: %s, the time must be a positive number", d.VideoThumbPos)
102+
}
103+
}
82104
return nil
83105
}
84106

drivers/local/meta.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type Addition struct {
1010
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
1111
ThumbCacheFolder string `json:"thumb_cache_folder"`
1212
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."`
13+
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."`
1314
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
1415
MkdirPerm string `json:"mkdir_perm" default:"777"`
1516
RecycleBinPath string `json:"recycle_bin_path" default:"delete permanently" help:"path to recycle bin, delete permanently if empty or keep 'delete permanently'"`

drivers/local/util.go

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package local
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"fmt"
67
"io/fs"
78
"os"
89
"path/filepath"
910
"sort"
11+
"strconv"
1012
"strings"
1113

1214
"github.com/alist-org/alist/v3/internal/conf"
@@ -34,10 +36,58 @@ func isSymlinkDir(f fs.FileInfo, path string) bool {
3436
return false
3537
}
3638

37-
func GetSnapshot(videoPath string, frameNum int) (imgData *bytes.Buffer, err error) {
39+
// Get the snapshot of the video
40+
func (d *Local) GetSnapshot(videoPath string) (imgData *bytes.Buffer, err error) {
41+
// Run ffprobe to get the video duration
42+
jsonOutput, err := ffmpeg.Probe(videoPath)
43+
if err != nil {
44+
return nil, err
45+
}
46+
// get format.duration from the json string
47+
type probeFormat struct {
48+
Duration string `json:"duration"`
49+
}
50+
type probeData struct {
51+
Format probeFormat `json:"format"`
52+
}
53+
var probe probeData
54+
err = json.Unmarshal([]byte(jsonOutput), &probe)
55+
if err != nil {
56+
return nil, err
57+
}
58+
totalDuration, err := strconv.ParseFloat(probe.Format.Duration, 64)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
var ss string
64+
if strings.HasSuffix(d.VideoThumbPos, "%") {
65+
percentage, err := strconv.ParseFloat(strings.TrimSuffix(d.VideoThumbPos, "%"), 64)
66+
if err != nil {
67+
return nil, err
68+
}
69+
ss = fmt.Sprintf("%f", totalDuration*percentage/100)
70+
} else {
71+
val, err := strconv.ParseFloat(d.VideoThumbPos, 64)
72+
if err != nil {
73+
return nil, err
74+
}
75+
// If the value is greater than the total duration, use the total duration
76+
if val > totalDuration {
77+
ss = fmt.Sprintf("%f", totalDuration)
78+
} else {
79+
ss = d.VideoThumbPos
80+
}
81+
}
82+
83+
// Run ffmpeg to get the snapshot
3884
srcBuf := bytes.NewBuffer(nil)
39-
stream := ffmpeg.Input(videoPath).
40-
Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}).
85+
// If the remaining time from the seek point to the end of the video is less
86+
// than the duration of a single frame, ffmpeg cannot extract any frames
87+
// within the specified range and will exit with an error.
88+
// The "noaccurate_seek" option prevents this error and would also speed up
89+
// the seek process.
90+
stream := ffmpeg.Input(videoPath, ffmpeg.KwArgs{"ss": ss, "noaccurate_seek": ""}).
4191
Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}).
4292
GlobalArgs("-loglevel", "error").Silent(true).
4393
WithOutput(srcBuf, os.Stdout)
@@ -77,7 +127,7 @@ func (d *Local) getThumb(file model.Obj) (*bytes.Buffer, *string, error) {
77127
}
78128
var srcBuf *bytes.Buffer
79129
if utils.GetFileType(file.GetName()) == conf.VIDEO {
80-
videoBuf, err := GetSnapshot(fullPath, 10)
130+
videoBuf, err := d.GetSnapshot(fullPath)
81131
if err != nil {
82132
return nil, nil, err
83133
}

0 commit comments

Comments
 (0)