fix: fix container duration offset eof
This commit is contained in:
103
generator.go
103
generator.go
@@ -2,6 +2,8 @@ package thumbnailgen
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -46,6 +48,59 @@ func (options *Options) Apply(opts ...func(*Options)) *Options {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type videoStreamProbe struct {
|
||||||
|
Streams []struct {
|
||||||
|
CodecType string `json:"codec_type"`
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
} `json:"streams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getThumbnailDuration(path string) (float64, error) {
|
||||||
|
stderr := bytes.NewBuffer(nil)
|
||||||
|
stdout := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
cmd := exec.Command(
|
||||||
|
"ffprobe",
|
||||||
|
"-v", "error",
|
||||||
|
"-select_streams", "v:0",
|
||||||
|
"-show_entries", "stream=codec_type,duration",
|
||||||
|
"-of", "json",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
cmd.Stdout = stdout
|
||||||
|
cmd.Stderr = stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"probe video stream: %w: %s",
|
||||||
|
err,
|
||||||
|
strings.TrimSpace(stderr.String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var probe videoStreamProbe
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &probe); err != nil {
|
||||||
|
return 0, fmt.Errorf("decode video-stream probe: %w", err)
|
||||||
|
}
|
||||||
|
if len(probe.Streams) == 0 || probe.Streams[0].CodecType != "video" {
|
||||||
|
return 0, errors.New("input has no video stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
durationText := strings.TrimSpace(probe.Streams[0].Duration)
|
||||||
|
if durationText == "" || durationText == "N/A" {
|
||||||
|
return GetVideoLength(path) // container-duration fallback for animated WebP
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := strconv.ParseFloat(durationText, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parse video-stream duration %q: %w", durationText, err)
|
||||||
|
}
|
||||||
|
if duration <= 0 {
|
||||||
|
return 0, fmt.Errorf("invalid video-stream duration %v", duration)
|
||||||
|
}
|
||||||
|
return duration, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (opts *Options) GetThumbnail(path string) ([][]byte, int, error) {
|
func (opts *Options) GetThumbnail(path string) ([][]byte, int, error) {
|
||||||
var filters []TimeFilter
|
var filters []TimeFilter
|
||||||
if opts.EnableFilter {
|
if opts.EnableFilter {
|
||||||
@@ -56,7 +111,7 @@ func (opts *Options) GetThumbnail(path string) ([][]byte, int, error) {
|
|||||||
filters = f
|
filters = f
|
||||||
}
|
}
|
||||||
|
|
||||||
length, err := GetVideoLength(path)
|
length, err := getThumbnailDuration(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -130,21 +185,44 @@ func FrameLiesWithinFilter(time float64, filters []TimeFilter) (bool, float64) {
|
|||||||
func GetImage(buf *bytes.Buffer, path string, timestamp int, format string, scale string) error {
|
func GetImage(buf *bytes.Buffer, path string, timestamp int, format string, scale string) error {
|
||||||
var t time.Time
|
var t time.Time
|
||||||
t = t.Add(time.Duration(timestamp) * time.Second)
|
t = t.Add(time.Duration(timestamp) * time.Second)
|
||||||
cmd := exec.Command("ffmpeg", "-ss", t.Format("15:04:05"), "-i", path, "-vframes", "1", "-c:v", format, "-filter:v", fmt.Sprintf("scale=%s", scale), "-f", "image2pipe", "-")
|
stderr := bytes.NewBuffer(nil)
|
||||||
|
cmd := exec.Command(
|
||||||
|
"ffmpeg",
|
||||||
|
"-ss",
|
||||||
|
t.Format("15:04:05"),
|
||||||
|
"-i",
|
||||||
|
path,
|
||||||
|
"-vframes",
|
||||||
|
"1",
|
||||||
|
"-c:v",
|
||||||
|
format,
|
||||||
|
"-filter:v",
|
||||||
|
fmt.Sprintf("scale=%s", scale),
|
||||||
|
"-f",
|
||||||
|
"image2pipe",
|
||||||
|
"-",
|
||||||
|
)
|
||||||
cmd.Stdout = buf
|
cmd.Stdout = buf
|
||||||
err := cmd.Run()
|
cmd.Stderr = stderr
|
||||||
return err
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("generate thumbnail at %s: %w: %s", t.Format("15:04:05"), err, strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetVideoLength(path string) (float64, error) {
|
func GetVideoLength(path string) (float64, error) {
|
||||||
cmd := exec.Command("ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path)
|
cmd := exec.Command("ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path)
|
||||||
|
stderr := bytes.NewBuffer(nil)
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
cmd.Stdout = buf
|
cmd.Stdout = buf
|
||||||
err := cmd.Run()
|
cmd.Stderr = stderr
|
||||||
if err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return 0, err
|
return 0, fmt.Errorf(
|
||||||
|
"probe video duration: %w: %s",
|
||||||
|
err,
|
||||||
|
strings.TrimSpace(stderr.String()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return strconv.ParseFloat(strings.ReplaceAll(buf.String(), "\n", ""), 64)
|
return strconv.ParseFloat(strings.ReplaceAll(buf.String(), "\n", ""), 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,12 +240,15 @@ func escapeFilterValue(value string) string {
|
|||||||
|
|
||||||
func GetFilter(path string) ([]TimeFilter, error) {
|
func GetFilter(path string) ([]TimeFilter, error) {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
|
stderr := bytes.NewBuffer(nil)
|
||||||
filter := fmt.Sprintf("movie=filename='%s',blackdetect[out0]", escapeFilterValue(path))
|
filter := fmt.Sprintf("movie=filename='%s',blackdetect[out0]", escapeFilterValue(path))
|
||||||
|
|
||||||
cmd := exec.Command("ffprobe", "-f", "lavfi", "-i", filter, "-show_entries", "tags=lavfi.black_start,lavfi.black_end", "-of", "default=nw=1", "-v", "quiet")
|
cmd := exec.Command("ffprobe", "-f", "lavfi", "-i", filter, "-show_entries", "tags=lavfi.black_start,lavfi.black_end", "-of", "default=nw=1", "-v", "quiet")
|
||||||
|
cmd.Stderr = stderr
|
||||||
cmd.Stdout = buf
|
cmd.Stdout = buf
|
||||||
err := cmd.Run()
|
if err := cmd.Run(); err != nil {
|
||||||
// TODO: Replace with strings.Builder
|
return nil, fmt.Errorf("detect black frames: %w: %s", err, strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
filterStr := buf.String()
|
filterStr := buf.String()
|
||||||
filters := strings.Split(filterStr, "\n")
|
filters := strings.Split(filterStr, "\n")
|
||||||
filters = slices.Compact(filters)
|
filters = slices.Compact(filters)
|
||||||
@@ -207,7 +288,7 @@ func GetFilter(path string) ([]TimeFilter, error) {
|
|||||||
i += 2
|
i += 2
|
||||||
}
|
}
|
||||||
|
|
||||||
return blackFilters, err
|
return blackFilters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFramerate(path string) (float64, error) {
|
func GetFramerate(path string) (float64, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user