mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add in-memory screenshot generation for sprites and phash (#1316)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
package ffmpeg
|
package ffmpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"bytes"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -62,7 +62,7 @@ func KillRunningEncoders(path string) {
|
|||||||
|
|
||||||
for _, process := range processes {
|
for _, process := range processes {
|
||||||
// assume it worked, don't check for error
|
// assume it worked, don't check for error
|
||||||
fmt.Printf("Killing encoder process for file: %s", path)
|
logger.Infof("Killing encoder process for file: %s", path)
|
||||||
process.Kill()
|
process.Kill()
|
||||||
|
|
||||||
// wait for the process to die before returning
|
// wait for the process to die before returning
|
||||||
@@ -82,7 +82,8 @@ func KillRunningEncoders(path string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
|
// FFmpeg runner with progress output, used for transcodes
|
||||||
|
func (e *Encoder) runTranscode(probeResult VideoFile, args []string) (string, error) {
|
||||||
cmd := exec.Command(e.Path, args...)
|
cmd := exec.Command(e.Path, args...)
|
||||||
|
|
||||||
stderr, err := cmd.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
@@ -137,3 +138,26 @@ func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
|
|||||||
|
|
||||||
return stdoutString, nil
|
return stdoutString, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
|
||||||
|
cmd := exec.Command(e.Path, args...)
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
registerRunningEncoder(probeResult.Path, cmd.Process)
|
||||||
|
err := waitAndDeregister(probeResult.Path, cmd)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// error message should be in the stderr stream
|
||||||
|
logger.Errorf("ffmpeg error when running command <%s>: %s", strings.Join(cmd.Args, " "), stderr.String())
|
||||||
|
return stdout.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout.String(), nil
|
||||||
|
}
|
||||||
|
|||||||
38
pkg/ffmpeg/encoder_sprite_screenshot.go
Normal file
38
pkg/ffmpeg/encoder_sprite_screenshot.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpriteScreenshotOptions struct {
|
||||||
|
Time float64
|
||||||
|
Width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) SpriteScreenshot(probeResult VideoFile, options SpriteScreenshotOptions) (image.Image, error) {
|
||||||
|
args := []string{
|
||||||
|
"-v", "error",
|
||||||
|
"-ss", fmt.Sprintf("%v", options.Time),
|
||||||
|
"-i", probeResult.Path,
|
||||||
|
"-vframes", "1",
|
||||||
|
"-vf", fmt.Sprintf("scale=%v:-1", options.Width),
|
||||||
|
"-c:v", "bmp",
|
||||||
|
"-f", "rawvideo",
|
||||||
|
"-",
|
||||||
|
}
|
||||||
|
data, err := e.run(probeResult, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := strings.NewReader(data)
|
||||||
|
|
||||||
|
img, _, err := image.Decode(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, err
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
|
|||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
options.OutputPath,
|
options.OutputPath,
|
||||||
}
|
}
|
||||||
_, _ = e.run(probeResult, args)
|
_, _ = e.runTranscode(probeResult, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
//transcode the video, remove the audio
|
//transcode the video, remove the audio
|
||||||
@@ -84,7 +84,7 @@ func (e *Encoder) TranscodeVideo(probeResult VideoFile, options TranscodeOptions
|
|||||||
"-vf", "scale=" + scale,
|
"-vf", "scale=" + scale,
|
||||||
options.OutputPath,
|
options.OutputPath,
|
||||||
}
|
}
|
||||||
_, _ = e.run(probeResult, args)
|
_, _ = e.runTranscode(probeResult, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
//copy the video stream as is, transcode audio
|
//copy the video stream as is, transcode audio
|
||||||
@@ -96,7 +96,7 @@ func (e *Encoder) TranscodeAudio(probeResult VideoFile, options TranscodeOptions
|
|||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
options.OutputPath,
|
options.OutputPath,
|
||||||
}
|
}
|
||||||
_, _ = e.run(probeResult, args)
|
_, _ = e.runTranscode(probeResult, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
//copy the video stream as is, drop audio
|
//copy the video stream as is, drop audio
|
||||||
@@ -107,5 +107,5 @@ func (e *Encoder) CopyVideo(probeResult VideoFile, options TranscodeOptions) {
|
|||||||
"-c:v", "copy",
|
"-c:v", "copy",
|
||||||
options.OutputPath,
|
options.OutputPath,
|
||||||
}
|
}
|
||||||
_, _ = e.run(probeResult, args)
|
_, _ = e.runTranscode(probeResult, args)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,9 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/corona10/goimagehash"
|
"github.com/corona10/goimagehash"
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/fvbommel/sortorder"
|
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
@@ -67,37 +64,22 @@ func (g *PhashGenerator) generateSprite(encoder *ffmpeg.Encoder) (image.Image, e
|
|||||||
chunkCount := g.Columns * g.Rows
|
chunkCount := g.Columns * g.Rows
|
||||||
offset := 0.05 * g.Info.VideoFile.Duration
|
offset := 0.05 * g.Info.VideoFile.Duration
|
||||||
stepSize := (0.9 * g.Info.VideoFile.Duration) / float64(chunkCount)
|
stepSize := (0.9 * g.Info.VideoFile.Duration) / float64(chunkCount)
|
||||||
|
var images []image.Image
|
||||||
for i := 0; i < chunkCount; i++ {
|
for i := 0; i < chunkCount; i++ {
|
||||||
time := offset + (float64(i) * stepSize)
|
time := offset + (float64(i) * stepSize)
|
||||||
num := fmt.Sprintf("%.3d", i)
|
|
||||||
filename := "phash_" + g.VideoChecksum + "_" + num + ".bmp"
|
|
||||||
|
|
||||||
options := ffmpeg.ScreenshotOptions{
|
options := ffmpeg.SpriteScreenshotOptions{
|
||||||
OutputPath: instance.Paths.Generated.GetTmpPath(filename),
|
Time: time,
|
||||||
Time: time,
|
Width: 160,
|
||||||
Width: 160,
|
|
||||||
}
|
}
|
||||||
if err := encoder.Screenshot(g.Info.VideoFile, options); err != nil {
|
img, err := encoder.SpriteScreenshot(g.Info.VideoFile, options)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine all of the thumbnails into a sprite image
|
|
||||||
pattern := fmt.Sprintf("phash_%s_.+\\.bmp$", g.VideoChecksum)
|
|
||||||
imagePaths, err := utils.MatchEntries(instance.Paths.Generated.Tmp, pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sort.Sort(sortorder.Natural(imagePaths))
|
|
||||||
var images []image.Image
|
|
||||||
for _, imagePath := range imagePaths {
|
|
||||||
img, err := imaging.Open(imagePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
images = append(images, img)
|
images = append(images, img)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Combine all of the thumbnails into a sprite image
|
||||||
if len(images) == 0 {
|
if len(images) == 0 {
|
||||||
return nil, fmt.Errorf("images slice is empty, failed to generate phash sprite for %s", g.Info.VideoFile.Path)
|
return nil, fmt.Errorf("images slice is empty, failed to generate phash sprite for %s", g.Info.VideoFile.Path)
|
||||||
}
|
}
|
||||||
@@ -113,9 +95,5 @@ func (g *PhashGenerator) generateSprite(encoder *ffmpeg.Encoder) (image.Image, e
|
|||||||
montage = imaging.Paste(montage, img, image.Pt(x, y))
|
montage = imaging.Paste(montage, img, image.Pt(x, y))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, imagePath := range imagePaths {
|
|
||||||
os.Remove(imagePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return montage, nil
|
return montage, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/fvbommel/sortorder"
|
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
@@ -75,29 +73,15 @@ func (g *SpriteGenerator) generateSpriteImage(encoder *ffmpeg.Encoder) error {
|
|||||||
|
|
||||||
// Create `this.chunkCount` thumbnails in the tmp directory
|
// Create `this.chunkCount` thumbnails in the tmp directory
|
||||||
stepSize := g.Info.VideoFile.Duration / float64(g.Info.ChunkCount)
|
stepSize := g.Info.VideoFile.Duration / float64(g.Info.ChunkCount)
|
||||||
|
var images []image.Image
|
||||||
for i := 0; i < g.Info.ChunkCount; i++ {
|
for i := 0; i < g.Info.ChunkCount; i++ {
|
||||||
time := float64(i) * stepSize
|
time := float64(i) * stepSize
|
||||||
num := fmt.Sprintf("%.3d", i)
|
|
||||||
filename := "thumbnail_" + g.VideoChecksum + "_" + num + ".jpg"
|
|
||||||
|
|
||||||
options := ffmpeg.ScreenshotOptions{
|
options := ffmpeg.SpriteScreenshotOptions{
|
||||||
OutputPath: instance.Paths.Generated.GetTmpPath(filename),
|
Time: time,
|
||||||
Time: time,
|
Width: 160,
|
||||||
Width: 160,
|
|
||||||
}
|
}
|
||||||
encoder.Screenshot(g.Info.VideoFile, options)
|
img, err := encoder.SpriteScreenshot(g.Info.VideoFile, options)
|
||||||
}
|
|
||||||
|
|
||||||
// Combine all of the thumbnails into a sprite image
|
|
||||||
pattern := fmt.Sprintf("thumbnail_%s_.+\\.jpg$", g.VideoChecksum)
|
|
||||||
imagePaths, err := utils.MatchEntries(instance.Paths.Generated.Tmp, pattern)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sort.Sort(sortorder.Natural(imagePaths))
|
|
||||||
var images []image.Image
|
|
||||||
for _, imagePath := range imagePaths {
|
|
||||||
img, err := imaging.Open(imagePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -107,6 +91,7 @@ func (g *SpriteGenerator) generateSpriteImage(encoder *ffmpeg.Encoder) error {
|
|||||||
if len(images) == 0 {
|
if len(images) == 0 {
|
||||||
return fmt.Errorf("images slice is empty, failed to generate sprite images for %s", g.Info.VideoFile.Path)
|
return fmt.Errorf("images slice is empty, failed to generate sprite images for %s", g.Info.VideoFile.Path)
|
||||||
}
|
}
|
||||||
|
// Combine all of the thumbnails into a sprite image
|
||||||
width := images[0].Bounds().Size().X
|
width := images[0].Bounds().Size().X
|
||||||
height := images[0].Bounds().Size().Y
|
height := images[0].Bounds().Size().Y
|
||||||
canvasWidth := width * g.Columns
|
canvasWidth := width * g.Columns
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
* Added scene queue.
|
* Added scene queue.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Improve sprite generation performance when using network storage.
|
||||||
* Remove duplicate values when scraping lists of elements.
|
* Remove duplicate values when scraping lists of elements.
|
||||||
* Improved performance of the auto-tagger.
|
* Improved performance of the auto-tagger.
|
||||||
* Clean generation artifacts after generating each scene.
|
* Clean generation artifacts after generating each scene.
|
||||||
|
|||||||
Reference in New Issue
Block a user