2019-02-10 10:57:56 +00:00
package manager
import (
2022-04-18 00:50:10 +00:00
"context"
2021-12-16 00:35:22 +00:00
"errors"
2019-02-10 10:57:56 +00:00
"fmt"
"image"
"math"
2020-07-19 01:59:18 +00:00
"github.com/disintegration/imaging"
2021-01-14 01:53:42 +00:00
2020-07-19 01:59:18 +00:00
"github.com/stashapp/stash/pkg/ffmpeg"
2022-03-17 00:33:59 +00:00
"github.com/stashapp/stash/pkg/fsutil"
2020-07-19 01:59:18 +00:00
"github.com/stashapp/stash/pkg/logger"
2022-04-18 00:50:10 +00:00
"github.com/stashapp/stash/pkg/scene/generate"
2019-02-10 10:57:56 +00:00
)
type SpriteGenerator struct {
2022-04-18 00:50:10 +00:00
Info * generatorInfo
2019-02-10 10:57:56 +00:00
2020-11-25 01:45:10 +00:00
VideoChecksum string
2019-02-10 10:57:56 +00:00
ImageOutputPath string
VTTOutputPath string
Rows int
Columns int
2022-01-04 02:46:53 +00:00
SlowSeek bool // use alternate seek function, very slow!
2020-07-19 01:59:18 +00:00
Overwrite bool
2022-04-18 00:50:10 +00:00
g * generate . Generator
2019-02-10 10:57:56 +00:00
}
2020-11-25 01:45:10 +00:00
func NewSpriteGenerator ( videoFile ffmpeg . VideoFile , videoChecksum string , imageOutputPath string , vttOutputPath string , rows int , cols int ) ( * SpriteGenerator , error ) {
2022-03-17 00:33:59 +00:00
exists , err := fsutil . FileExists ( videoFile . Path )
2019-02-10 10:57:56 +00:00
if ! exists {
return nil , err
}
2022-01-04 02:46:53 +00:00
slowSeek := false
chunkCount := rows * cols
// 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
2022-04-18 00:50:10 +00:00
fc , err := ffprobe . GetReadFrameCount ( videoFile . Path )
2022-01-04 02:46:53 +00:00
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
}
}
2021-12-16 00:35:22 +00:00
}
2019-02-10 10:57:56 +00:00
generator , err := newGeneratorInfo ( videoFile )
if err != nil {
return nil , err
}
2022-01-04 02:46:53 +00:00
generator . ChunkCount = chunkCount
2019-02-10 10:57:56 +00:00
if err := generator . configure ( ) ; err != nil {
return nil , err
}
return & SpriteGenerator {
Info : generator ,
2020-11-25 01:45:10 +00:00
VideoChecksum : videoChecksum ,
2019-02-10 10:57:56 +00:00
ImageOutputPath : imageOutputPath ,
VTTOutputPath : vttOutputPath ,
Rows : rows ,
2022-01-04 02:46:53 +00:00
SlowSeek : slowSeek ,
2019-02-10 10:57:56 +00:00
Columns : cols ,
2022-04-18 00:50:10 +00:00
g : & generate . Generator {
Encoder : instance . FFMPEG ,
LockManager : instance . ReadLockManager ,
ScenePaths : instance . Paths . Scene ,
} ,
2019-02-10 10:57:56 +00:00
} , nil
}
func ( g * SpriteGenerator ) Generate ( ) error {
2022-04-18 00:50:10 +00:00
if err := g . generateSpriteImage ( ) ; err != nil {
2019-02-10 10:57:56 +00:00
return err
}
2022-04-18 00:50:10 +00:00
if err := g . generateSpriteVTT ( ) ; err != nil {
2019-02-10 10:57:56 +00:00
return err
}
return nil
}
2022-04-18 00:50:10 +00:00
func ( g * SpriteGenerator ) generateSpriteImage ( ) error {
2020-07-19 01:59:18 +00:00
if ! g . Overwrite && g . imageExists ( ) {
2019-07-09 00:36:18 +00:00
return nil
}
2019-02-10 10:57:56 +00:00
2021-05-05 03:22:05 +00:00
var images [ ] image . Image
2019-02-10 10:57:56 +00:00
2022-01-04 02:46:53 +00:00
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
2022-04-18 00:50:10 +00:00
img , err := g . g . SpriteScreenshot ( context . TODO ( ) , g . Info . VideoFile . Path , time )
2022-01-04 02:46:53 +00:00
if err != nil {
return err
}
images = append ( images , img )
2019-02-10 10:57:56 +00:00
}
2022-01-04 02:46:53 +00:00
} 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" )
}
2022-04-18 00:50:10 +00:00
img , err := g . g . SpriteScreenshotSlow ( context . TODO ( ) , g . Info . VideoFile . Path , int ( frame ) )
2022-01-04 02:46:53 +00:00
if err != nil {
return err
}
images = append ( images , img )
2019-02-10 10:57:56 +00:00
}
2022-01-04 02:46:53 +00:00
2019-02-10 10:57:56 +00:00
}
2019-03-26 16:45:08 +00:00
if len ( images ) == 0 {
return fmt . Errorf ( "images slice is empty, failed to generate sprite images for %s" , g . Info . VideoFile . Path )
}
2019-02-10 10:57:56 +00:00
2022-04-18 00:50:10 +00:00
return imaging . Save ( g . g . CombineSpriteImages ( images ) , g . ImageOutputPath )
2019-02-10 10:57:56 +00:00
}
2022-04-18 00:50:10 +00:00
func ( g * SpriteGenerator ) generateSpriteVTT ( ) error {
2020-07-19 01:59:18 +00:00
if ! g . Overwrite && g . vttExists ( ) {
2019-07-09 00:36:18 +00:00
return nil
}
2019-02-10 10:57:56 +00:00
logger . Infof ( "[generator] generating sprite vtt for %s" , g . Info . VideoFile . Path )
2022-01-04 02:46:53 +00:00
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
}
2019-02-10 10:57:56 +00:00
2022-04-18 00:50:10 +00:00
return g . g . SpriteVTT ( context . TODO ( ) , g . VTTOutputPath , g . ImageOutputPath , stepSize )
2019-07-09 00:36:18 +00:00
}
func ( g * SpriteGenerator ) imageExists ( ) bool {
2022-03-17 00:33:59 +00:00
exists , _ := fsutil . FileExists ( g . ImageOutputPath )
2019-07-09 00:36:18 +00:00
return exists
}
func ( g * SpriteGenerator ) vttExists ( ) bool {
2022-03-17 00:33:59 +00:00
exists , _ := fsutil . FileExists ( g . VTTOutputPath )
2019-07-09 00:36:18 +00:00
return exists
2019-02-10 10:57:56 +00:00
}