Fix scrubber sprite creation for short video files (#2167)

* Fix scrubber sprite creation for small files
* accept only valid ffprobe nbReadFrames
This commit is contained in:
bnkai
2022-01-04 04:46:53 +02:00
committed by GitHub
parent bd784cdf96
commit 849c590b2a
5 changed files with 131 additions and 18 deletions

View File

@@ -8,6 +8,7 @@ import (
type SpriteScreenshotOptions struct {
Time float64
Frame int
Width int
}
@@ -36,3 +37,31 @@ func (e *Encoder) SpriteScreenshot(probeResult VideoFile, options SpriteScreensh
return img, err
}
// SpriteScreenshotSlow uses the select filter to get a single frame from a videofile instead of seeking
// It is very slow and should only be used for files with very small duration in secs / frame count
func (e *Encoder) SpriteScreenshotSlow(probeResult VideoFile, options SpriteScreenshotOptions) (image.Image, error) {
args := []string{
"-v", "error",
"-i", probeResult.Path,
"-vsync", "0", // do not create/drop frames
"-vframes", "1",
"-vf", fmt.Sprintf("select=eq(n\\,%d),scale=%v:-1", options.Frame, options.Width), // keep only frame number options.Frame
"-c:v", "bmp",
"-f", "rawvideo",
"-",
}
data, err := e.run(probeResult.Path, args, nil)
if err != nil {
return nil, err
}
reader := strings.NewReader(data)
img, _, err := image.Decode(reader)
if err != nil {
return nil, err
}
return img, err
}

View File

@@ -218,6 +218,7 @@ type VideoFile struct {
Height int
FrameRate float64
Rotation int64
FrameCount int64
AudioCodec string
}
@@ -242,6 +243,24 @@ func (f *FFProbe) NewVideoFile(videoPath string, stripExt bool) (*VideoFile, err
return parse(videoPath, probeJSON, stripExt)
}
// GetReadFrameCount counts the actual frames of the video file
func (f *FFProbe) GetReadFrameCount(vf *VideoFile) (int64, error) {
args := []string{"-v", "quiet", "-print_format", "json", "-count_frames", "-show_format", "-show_streams", "-show_error", vf.Path}
out, err := exec.Command(string(*f), args...).Output()
if err != nil {
return 0, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", vf.Path, string(out), err.Error())
}
probeJSON := &FFProbeJSON{}
if err := json.Unmarshal(out, probeJSON); err != nil {
return 0, fmt.Errorf("error unmarshalling video data for <%s>: %s", vf.Path, err.Error())
}
fc, err := parse(vf.Path, probeJSON, false)
return fc.FrameCount, err
}
func parse(filePath string, probeJSON *FFProbeJSON, stripExt bool) (*VideoFile, error) {
if probeJSON == nil {
return nil, fmt.Errorf("failed to get ffprobe json for <%s>", filePath)
@@ -263,8 +282,8 @@ func parse(filePath string, probeJSON *FFProbeJSON, stripExt bool) (*VideoFile,
}
result.Comment = probeJSON.Format.Tags.Comment
result.Bitrate, _ = strconv.ParseInt(probeJSON.Format.BitRate, 10, 64)
result.Container = probeJSON.Format.FormatName
duration, _ := strconv.ParseFloat(probeJSON.Format.Duration, 64)
result.Duration = math.Round(duration*100) / 100
@@ -288,6 +307,15 @@ func parse(filePath string, probeJSON *FFProbeJSON, stripExt bool) (*VideoFile,
if videoStream != nil {
result.VideoStream = videoStream
result.VideoCodec = videoStream.CodecName
result.FrameCount, _ = strconv.ParseInt(videoStream.NbFrames, 10, 64)
if videoStream.NbReadFrames != "" { // if ffprobe counted the frames use that instead
fc, _ := strconv.ParseInt(videoStream.NbReadFrames, 10, 64)
if fc > 0 {
result.FrameCount, _ = strconv.ParseInt(videoStream.NbReadFrames, 10, 64)
} else {
logger.Debugf("[ffprobe] <%s> invalid Read Frames count", videoStream.NbReadFrames)
}
}
result.VideoBitrate, _ = strconv.ParseInt(videoStream.BitRate, 10, 64)
var framerate float64
if strings.Contains(videoStream.AvgFrameRate, "/") {

View File

@@ -70,6 +70,7 @@ type FFProbeStream struct {
Level int `json:"level,omitempty"`
NalLengthSize string `json:"nal_length_size,omitempty"`
NbFrames string `json:"nb_frames"`
NbReadFrames string `json:"nb_read_frames"`
PixFmt string `json:"pix_fmt,omitempty"`
Profile string `json:"profile"`
RFrameRate string `json:"r_frame_rate"`

View File

@@ -25,6 +25,7 @@ type SpriteGenerator struct {
VTTOutputPath string
Rows int
Columns int
SlowSeek bool // use alternate seek function, very slow!
Overwrite bool
}
@@ -34,17 +35,33 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO
if !exists {
return nil, err
}
slowSeek := false
chunkCount := rows * cols
// FFMPEG bombs out if we try to request 89 snapshots from a 2 second video
if videoFile.Duration < 3 {
return nil, errors.New("video too short to create sprite")
// For files with small duration / low frame count try to seek using frame number intead of seconds
if videoFile.Duration < 5 || (0 < videoFile.FrameCount && videoFile.FrameCount <= int64(chunkCount)) { // some files can have FrameCount == 0, only use SlowSeek if duration < 5
if videoFile.Duration <= 0 {
s := fmt.Sprintf("video %s: duration(%.3f)/frame count(%d) invalid, skipping sprite creation", videoFile.Path, videoFile.Duration, videoFile.FrameCount)
return nil, errors.New(s)
}
logger.Warnf("[generator] video %s too short (%.3fs, %d frames), using frame seeking", videoFile.Path, videoFile.Duration, videoFile.FrameCount)
slowSeek = true
// do an actual frame count of the file ( number of frames = read frames)
ffprobe := GetInstance().FFProbe
fc, err := ffprobe.GetReadFrameCount(&videoFile)
if err == nil {
if fc != videoFile.FrameCount {
logger.Warnf("[generator] updating framecount (%d) for %s with read frames count (%d)", videoFile.FrameCount, videoFile.Path, fc)
videoFile.FrameCount = fc
}
}
}
generator, err := newGeneratorInfo(videoFile)
if err != nil {
return nil, err
}
generator.ChunkCount = rows * cols
generator.ChunkCount = chunkCount
if err := generator.configure(); err != nil {
return nil, err
}
@@ -55,6 +72,7 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO
ImageOutputPath: imageOutputPath,
VTTOutputPath: vttOutputPath,
Rows: rows,
SlowSeek: slowSeek,
Columns: cols,
}, nil
}
@@ -75,11 +93,14 @@ func (g *SpriteGenerator) generateSpriteImage(encoder *ffmpeg.Encoder) error {
if !g.Overwrite && g.imageExists() {
return nil
}
logger.Infof("[generator] generating sprite image for %s", g.Info.VideoFile.Path)
// Create `this.chunkCount` thumbnails in the tmp directory
stepSize := g.Info.VideoFile.Duration / float64(g.Info.ChunkCount)
var images []image.Image
if !g.SlowSeek {
logger.Infof("[generator] generating sprite image for %s", g.Info.VideoFile.Path)
// generate `ChunkCount` thumbnails
stepSize := g.Info.VideoFile.Duration / float64(g.Info.ChunkCount)
for i := 0; i < g.Info.ChunkCount; i++ {
time := float64(i) * stepSize
@@ -87,12 +108,37 @@ func (g *SpriteGenerator) generateSpriteImage(encoder *ffmpeg.Encoder) error {
Time: time,
Width: 160,
}
img, err := encoder.SpriteScreenshot(g.Info.VideoFile, options)
if err != nil {
return err
}
images = append(images, img)
}
} else {
logger.Infof("[generator] generating sprite image for %s (%d frames)", g.Info.VideoFile.Path, g.Info.VideoFile.FrameCount)
stepFrame := float64(g.Info.VideoFile.FrameCount-1) / float64(g.Info.ChunkCount)
for i := 0; i < g.Info.ChunkCount; i++ {
// generate exactly `ChunkCount` thumbnails, using duplicate frames if needed
frame := math.Round(float64(i) * stepFrame)
if frame >= math.MaxInt || frame <= math.MinInt {
return errors.New("invalid frame number conversion")
}
options := ffmpeg.SpriteScreenshotOptions{
Frame: int(frame),
Width: 160,
}
img, err := encoder.SpriteScreenshotSlow(g.Info.VideoFile, options)
if err != nil {
return err
}
images = append(images, img)
}
}
if len(images) == 0 {
return fmt.Errorf("images slice is empty, failed to generate sprite images for %s", g.Info.VideoFile.Path)
@@ -132,7 +178,15 @@ func (g *SpriteGenerator) generateSpriteVTT(encoder *ffmpeg.Encoder) error {
width := image.Width / g.Columns
height := image.Height / g.Rows
stepSize := float64(g.Info.NthFrame) / g.Info.FrameRate
var stepSize float64
if !g.SlowSeek {
stepSize = float64(g.Info.NthFrame) / g.Info.FrameRate
} else {
// for files with a low framecount (<ChunkCount) g.Info.NthFrame can be zero
// so recalculate from scratch
stepSize = float64(g.Info.VideoFile.FrameCount-1) / float64(g.Info.ChunkCount)
stepSize /= g.Info.FrameRate
}
vttLines := []string{"WEBVTT", ""}
for index := 0; index < g.Info.ChunkCount; index++ {

View File

@@ -1,4 +1,5 @@
### 🐛 Bug fixes
* Generate sprites for short video files. ([#2167](https://github.com/stashapp/stash/pull/2167))
* Fix stash-box scraping including underscores in ethnicity. ([#2191](https://github.com/stashapp/stash/pull/2191))
* Fix stash-box batch performer task not setting birthdate. ([#2189](https://github.com/stashapp/stash/pull/2189))
* Fix error when scanning symlinks. ([#2196](https://github.com/stashapp/stash/issues/2196))