2019-02-10 10:57:56 +00:00
package manager
import (
2021-12-16 00:35:22 +00:00
"errors"
2019-02-10 10:57:56 +00:00
"fmt"
"image"
"image/color"
"math"
2020-08-21 07:57:07 +00:00
"os"
2019-02-10 10:57:56 +00:00
"path/filepath"
"strings"
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"
"github.com/stashapp/stash/pkg/utils"
2019-02-10 10:57:56 +00:00
)
type SpriteGenerator struct {
Info * GeneratorInfo
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
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
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
}
}
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 ,
} , nil
}
func ( g * SpriteGenerator ) Generate ( ) error {
2021-10-14 23:39:48 +00:00
encoder := instance . FFMPEG
2019-02-10 10:57:56 +00:00
if err := g . generateSpriteImage ( & encoder ) ; err != nil {
return err
}
if err := g . generateSpriteVTT ( & encoder ) ; err != nil {
return err
}
return nil
}
func ( g * SpriteGenerator ) generateSpriteImage ( encoder * ffmpeg . Encoder ) 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
options := ffmpeg . SpriteScreenshotOptions {
Time : time ,
Width : 160 ,
}
img , err := encoder . SpriteScreenshot ( g . Info . VideoFile , options )
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" )
}
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 )
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 )
}
2021-05-05 03:22:05 +00:00
// Combine all of the thumbnails into a sprite image
2019-02-10 10:57:56 +00:00
width := images [ 0 ] . Bounds ( ) . Size ( ) . X
height := images [ 0 ] . Bounds ( ) . Size ( ) . Y
canvasWidth := width * g . Columns
canvasHeight := height * g . Rows
montage := imaging . New ( canvasWidth , canvasHeight , color . NRGBA { } )
for index := 0 ; index < len ( images ) ; index ++ {
x := width * ( index % g . Columns )
2019-02-14 22:53:32 +00:00
y := height * int ( math . Floor ( float64 ( index ) / float64 ( g . Rows ) ) )
2019-02-10 10:57:56 +00:00
img := images [ index ]
montage = imaging . Paste ( montage , img , image . Pt ( x , y ) )
}
return imaging . Save ( montage , g . ImageOutputPath )
}
func ( g * SpriteGenerator ) generateSpriteVTT ( encoder * ffmpeg . Encoder ) 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 )
2020-08-21 07:57:07 +00:00
spriteImage , err := os . Open ( g . ImageOutputPath )
2019-02-10 10:57:56 +00:00
if err != nil {
return err
}
2020-08-21 07:57:07 +00:00
defer spriteImage . Close ( )
2019-02-10 10:57:56 +00:00
spriteImageName := filepath . Base ( g . ImageOutputPath )
2020-08-21 07:57:07 +00:00
image , _ , err := image . DecodeConfig ( spriteImage )
2021-05-25 08:40:51 +00:00
if err != nil {
return err
}
2020-08-21 07:57:07 +00:00
width := image . Width / g . Columns
height := image . Height / g . Rows
2019-02-10 10:57:56 +00:00
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
vttLines := [ ] string { "WEBVTT" , "" }
for index := 0 ; index < g . Info . ChunkCount ; index ++ {
x := width * ( index % g . Columns )
2019-02-14 22:53:32 +00:00
y := height * int ( math . Floor ( float64 ( index ) / float64 ( g . Rows ) ) )
2019-02-10 10:57:56 +00:00
startTime := utils . GetVTTTime ( float64 ( index ) * stepSize )
2019-02-14 22:53:32 +00:00
endTime := utils . GetVTTTime ( float64 ( index + 1 ) * stepSize )
2019-02-10 10:57:56 +00:00
2019-02-14 22:53:32 +00:00
vttLines = append ( vttLines , startTime + " --> " + endTime )
2019-02-10 10:57:56 +00:00
vttLines = append ( vttLines , fmt . Sprintf ( "%s#xywh=%d,%d,%d,%d" , spriteImageName , x , y , width , height ) )
vttLines = append ( vttLines , "" )
}
vtt := strings . Join ( vttLines , "\n" )
2021-09-27 00:55:23 +00:00
return os . WriteFile ( g . VTTOutputPath , [ ] byte ( vtt ) , 0644 )
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
}