Added scene preview generator

This commit is contained in:
Stash Dev
2019-02-09 21:30:54 -08:00
parent 16ead91a05
commit 1aa7557432
12 changed files with 575 additions and 167 deletions

View File

@@ -21,7 +21,8 @@ func (r *queryResolver) MetadataExport(ctx context.Context) (string, error) {
}
func (r *queryResolver) MetadataGenerate(ctx context.Context) (string, error) {
panic("not implemented")
manager.GetInstance().Generate(true, true, true, true)
return "todo", nil
}
func (r *queryResolver) MetadataClean(ctx context.Context) (string, error) {

View File

@@ -1,7 +1,6 @@
package ffmpeg
import (
"fmt"
"github.com/stashapp/stash/logger"
"io/ioutil"
"os/exec"
@@ -11,65 +10,17 @@ import (
var progressRegex = regexp.MustCompile(`time=(\d+):(\d+):(\d+.\d+)`)
type encoder struct {
type Encoder struct {
Path string
}
func NewEncoder(ffmpegPath string) encoder {
return encoder{
func NewEncoder(ffmpegPath string) Encoder {
return Encoder{
Path: ffmpegPath,
}
}
type ScreenshotOptions struct {
OutputPath string
Quality int
Time float64
Width int
Verbosity string
}
type TranscodeOptions struct {
OutputPath string
}
func (e *encoder) Screenshot(probeResult FFProbeResult, options ScreenshotOptions) {
if options.Verbosity == "" {
options.Verbosity = "quiet"
}
if options.Quality == 0 {
options.Quality = 1
}
args := []string{
"-v", options.Verbosity,
"-ss", fmt.Sprintf("%v", options.Time),
"-y",
"-i", probeResult.Path, // TODO: Wrap in quotes?
"-vframes", "1",
"-q:v", fmt.Sprintf("%v", options.Quality),
"-vf", fmt.Sprintf("scale=%v:-1", options.Width),
"-f", "image2",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}
func (e *encoder) Transcode(probeResult FFProbeResult, options TranscodeOptions) {
args := []string{
"-i", probeResult.Path,
"-c:v", "libx264",
"-profile:v", "high",
"-level", "4.2",
"-preset", "superfast",
"-crf", "23",
"-vf", "scale=iw:-2",
"-c:a", "aac",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}
func (e *encoder) run(probeResult FFProbeResult, args []string) (string, error) {
func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
cmd := exec.Command(e.Path, args...)
stderr, err := cmd.StderrPipe()

View File

@@ -0,0 +1,65 @@
package ffmpeg
import (
"fmt"
"strconv"
)
type ScenePreviewChunkOptions struct {
Time int
Width int
OutputPath string
}
func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePreviewChunkOptions) {
args := []string{
"-v", "quiet",
"-ss", strconv.Itoa(options.Time),
"-t", "0.75",
"-i", probeResult.Path,
"-y",
"-c:v", "libx264",
"-profile:v", "high",
"-level", "4.2",
"-preset", "veryslow",
"-crf", "21",
"-threads", "4",
"-vf", fmt.Sprintf("scale=%v:-2", options.Width),
"-c:a", "aac",
"-b:a", "128k",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}
func (e *Encoder) ScenePreviewVideoChunkCombine(probeResult VideoFile, concatFilePath string, outputPath string) {
args := []string{
"-v", "quiet",
"-f", "concat",
"-i", concatFilePath,
"-y",
"-c", "copy",
outputPath,
}
_, _ = e.run(probeResult, args)
}
func (e *Encoder) ScenePreviewVideoToImage(probeResult VideoFile, width int, videoPreviewPath string, outputPath string) error {
args := []string{
"-v", "quiet",
"-i", videoPreviewPath,
"-y",
"-c:v", "libwebp",
"-lossless", "1",
"-q:v", "70",
"-compression_level", "6",
"-preset", "default",
"-loop", "0",
"-threads", "4",
"-vf", fmt.Sprintf("scale=%v:-2,fps=12", width),
"-an",
outputPath,
}
_, err := e.run(probeResult, args)
return err
}

View File

@@ -0,0 +1,32 @@
package ffmpeg
import "fmt"
type ScreenshotOptions struct {
OutputPath string
Quality int
Time float64
Width int
Verbosity string
}
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
if options.Verbosity == "" {
options.Verbosity = "quiet"
}
if options.Quality == 0 {
options.Quality = 1
}
args := []string{
"-v", options.Verbosity,
"-ss", fmt.Sprintf("%v", options.Time),
"-y",
"-i", probeResult.Path, // TODO: Wrap in quotes?
"-vframes", "1",
"-q:v", fmt.Sprintf("%v", options.Quality),
"-vf", fmt.Sprintf("scale=%v:-1", options.Width),
"-f", "image2",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}

View File

@@ -0,0 +1,20 @@
package ffmpeg
type TranscodeOptions struct {
OutputPath string
}
func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
args := []string{
"-i", probeResult.Path,
"-c:v", "libx264",
"-profile:v", "high",
"-level", "4.2",
"-preset", "superfast",
"-crf", "23",
"-vf", "scale=iw:-2",
"-c:a", "aac",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}

View File

@@ -6,18 +6,26 @@ import (
"math"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
)
type ffprobeExecutable struct {
Path string
var ValidCodecs = []string{"h264", "h265", "vp8", "vp9"}
func IsValidCodec(codecName string) bool {
for _, c := range ValidCodecs {
if c == codecName {
return true
}
}
return false
}
type FFProbeResult struct {
JSON ffprobeJSON
type VideoFile struct {
JSON FFProbeJSON
AudioStream *FFProbeStream
VideoStream *FFProbeStream
Path string
Container string
@@ -37,52 +45,62 @@ type FFProbeResult struct {
AudioCodec string
}
func NewFFProbe(ffprobePath string) ffprobeExecutable {
return ffprobeExecutable{
Path: ffprobePath,
}
}
// Execute exec command and bind result to struct.
func (ffp *ffprobeExecutable) ProbeVideo(filePath string) (*FFProbeResult, error) {
args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", filePath}
// Extremely slow on windows for some reason
if runtime.GOOS != "windows" {
args = append(args, "-count_frames")
}
out, err := exec.Command(ffp.Path, args...).Output()
func NewVideoFile(ffprobePath string, videoPath string) (*VideoFile, error) {
args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", videoPath}
//// Extremely slow on windows for some reason
//if runtime.GOOS != "windows" {
// args = append(args, "-count_frames")
//}
out, err := exec.Command(ffprobePath, args...).Output()
if err != nil {
return nil, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", filePath, string(out), err.Error())
return nil, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", videoPath, string(out), err.Error())
}
probeJSON := &ffprobeJSON{}
probeJSON := &FFProbeJSON{}
if err := json.Unmarshal(out, probeJSON); err != nil {
return nil, err
}
result := ffp.newProbeResult(filePath, *probeJSON)
return result, nil
return parse(videoPath, probeJSON)
}
func (ffp *ffprobeExecutable) newProbeResult(filePath string, probeJson ffprobeJSON) *FFProbeResult {
videoStreamIndex := ffp.getStreamIndex("video", probeJson)
audioStreamIndex := ffp.getStreamIndex("audio", probeJson)
func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
if probeJSON == nil {
return nil, fmt.Errorf("failed to get ffprobe json")
}
result := &VideoFile{}
result.JSON = *probeJSON
if result.JSON.Error.Code != 0 {
return nil, fmt.Errorf("ffprobe error code %d: %s", result.JSON.Error.Code, result.JSON.Error.String)
}
//} else if (ffprobeResult.stderr.includes("could not find codec parameters")) {
// throw new Error(`FFProbe [${filePath}] -> Could not find codec parameters`);
//} // TODO nil_or_unsupported.(video_stream) && nil_or_unsupported.(audio_stream)
result := &FFProbeResult{}
result.JSON = probeJson
result.Path = filePath
result.Container = probeJson.Format.FormatName
duration, _ := strconv.ParseFloat(probeJson.Format.Duration, 64)
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
result.StartTime, _ = strconv.ParseFloat(probeJson.Format.StartTime, 64)
result.Bitrate, _ = strconv.ParseInt(probeJson.Format.BitRate, 10, 64)
fileStat, _ := os.Stat(filePath)
result.Size = fileStat.Size()
result.CreationTime = probeJson.Format.Tags.CreationTime
result.StartTime, _ = strconv.ParseFloat(probeJSON.Format.StartTime, 64)
result.CreationTime = probeJSON.Format.Tags.CreationTime
if videoStreamIndex != -1 {
videoStream := probeJson.Streams[videoStreamIndex]
audioStream := result.GetAudioStream()
if audioStream != nil {
result.AudioCodec = audioStream.CodecName
result.AudioStream = audioStream
}
videoStream := result.GetVideoStream()
if videoStream != nil {
result.VideoStream = videoStream
result.VideoCodec = videoStream.CodecName
result.VideoBitrate, _ = strconv.ParseInt(videoStream.BitRate, 10, 64)
var framerate float64
@@ -104,14 +122,26 @@ func (ffp *ffprobeExecutable) newProbeResult(filePath string, probeJson ffprobeJ
}
}
if audioStreamIndex != -1 {
result.AudioCodec = probeJson.Streams[audioStreamIndex].CodecName
return result, nil
}
return result
func (v *VideoFile) GetAudioStream() *FFProbeStream {
index := v.getStreamIndex("audio", v.JSON)
if index != -1 {
return &v.JSON.Streams[index]
}
return nil
}
func (ffp *ffprobeExecutable) getStreamIndex(fileType string, probeJson ffprobeJSON) int {
func (v *VideoFile) GetVideoStream() *FFProbeStream {
index := v.getStreamIndex("video", v.JSON)
if index != -1 {
return &v.JSON.Streams[index]
}
return nil
}
func (v *VideoFile) getStreamIndex(fileType string, probeJson FFProbeJSON) int {
for i, stream := range probeJson.Streams {
if stream.CodecType == fileType {
return i

View File

@@ -4,7 +4,7 @@ import (
"time"
)
type ffprobeJSON struct {
type FFProbeJSON struct {
Format struct {
BitRate string `json:"bit_rate"`
Duration string `json:"duration"`
@@ -24,7 +24,14 @@ type ffprobeJSON struct {
MinorVersion string `json:"minor_version"`
} `json:"tags"`
} `json:"format"`
Streams []struct {
Streams []FFProbeStream `json:"streams"`
Error struct {
Code int `json:"code"`
String string `json:"string"`
} `json:"error"`
}
type FFProbeStream struct {
AvgFrameRate string `json:"avg_frame_rate"`
BitRate string `json:"bit_rate"`
BitsPerRawSample string `json:"bits_per_raw_sample,omitempty"`
@@ -82,9 +89,4 @@ type ffprobeJSON struct {
MaxBitRate string `json:"max_bit_rate,omitempty"`
SampleFmt string `json:"sample_fmt,omitempty"`
SampleRate string `json:"sample_rate,omitempty"`
} `json:"streams"`
Error struct {
Code int `json:"code"`
String string `json:"string"`
} `json:"error"`
}

62
manager/generator.go Normal file
View File

@@ -0,0 +1,62 @@
package manager
import (
"fmt"
"github.com/stashapp/stash/ffmpeg"
"github.com/stashapp/stash/logger"
"github.com/stashapp/stash/utils"
"os/exec"
"strconv"
)
type Generator struct {
ChunkCount int
FrameRate float64
NumberOfFrames int
NthFrame int
VideoFile ffmpeg.VideoFile
}
func newGenerator(videoFile ffmpeg.VideoFile) (*Generator, error) {
exists, err := utils.FileExists(videoFile.Path)
if !exists {
logger.Errorf("video file not found")
return nil, err
}
generator := &Generator{VideoFile: videoFile}
return generator, nil
}
func (g *Generator) configure() error {
videoStream := g.VideoFile.VideoStream
if videoStream == nil {
return fmt.Errorf("missing video stream")
}
var framerate float64
if g.VideoFile.FrameRate == 0 {
framerate, _ = strconv.ParseFloat(videoStream.RFrameRate, 64)
} else {
framerate = g.VideoFile.FrameRate
}
g.FrameRate = framerate
numberOfFrames, _ := strconv.Atoi(videoStream.NbFrames)
if numberOfFrames == 0 {
command := `ffmpeg -nostats -i `+g.VideoFile.Path+` -vcodec copy -f rawvideo -y /dev/null 2>&1 | \
grep frame | \
awk '{split($0,a,"fps")}END{print a[1]}' | \
sed 's/.*= *//'`
commandResult, _ := exec.Command(command).Output()
numberOfFrames, _ := strconv.Atoi(string(commandResult))
if numberOfFrames == 0 { // TODO: test
numberOfFrames = int(framerate * g.VideoFile.Duration)
}
}
g.NumberOfFrames = numberOfFrames
g.NthFrame = g.NumberOfFrames / g.ChunkCount
return nil
}

View File

@@ -0,0 +1,127 @@
package manager
import (
"bufio"
"fmt"
"github.com/stashapp/stash/ffmpeg"
"github.com/stashapp/stash/logger"
"github.com/stashapp/stash/utils"
"os"
"path"
)
type PreviewGenerator struct {
generator *Generator
VideoFilename string
ImageFilename string
OutputDirectory string
}
func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, imageFilename string, outputDirectory string) (*PreviewGenerator, error) {
exists, err := utils.FileExists(videoFile.Path)
if !exists {
return nil, err
}
generator, err := newGenerator(videoFile)
if err != nil {
return nil, err
}
generator.ChunkCount = 12 // 12 segments to the preview
if err := generator.configure(); err != nil {
return nil, err
}
return &PreviewGenerator{
generator: generator,
VideoFilename: videoFilename,
ImageFilename: imageFilename,
OutputDirectory: outputDirectory,
}, nil
}
func (g *PreviewGenerator) Generate() error {
instance.Paths.Generated.EmptyTmpDir()
logger.Infof("[generator] generating scene preview for %s", g.generator.VideoFile.Path)
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG)
if err := g.generateConcatFile(); err != nil {
return err
}
if err := g.generateVideo(&encoder); err != nil {
return err
}
if err := g.generateImage(&encoder); err != nil {
return err
}
return nil
}
func (g *PreviewGenerator) generateConcatFile() error {
f, err := os.Create(g.getConcatFilePath())
if err != nil {
return err
}
defer f.Close()
w := bufio.NewWriter(f)
for i := 0; i < g.generator.ChunkCount; i++ {
num := fmt.Sprintf("%.3d", i)
filename := "preview"+num+".mp4"
_, _ = w.WriteString(fmt.Sprintf("file '%s'\n", filename))
}
return w.Flush()
}
func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
outputPath := path.Join(g.OutputDirectory, g.VideoFilename)
outputExists, _ := utils.FileExists(outputPath)
if outputExists {
return nil
}
stepSize := int(g.generator.VideoFile.Duration / float64(g.generator.ChunkCount))
for i := 0; i < g.generator.ChunkCount; i++ {
time := i * stepSize
num := fmt.Sprintf("%.3d", i)
filename := "preview"+num+".mp4"
chunkOutputPath := instance.Paths.Generated.GetTmpPath(filename)
options := ffmpeg.ScenePreviewChunkOptions{
Time: time,
Width: 640,
OutputPath: chunkOutputPath,
}
encoder.ScenePreviewVideoChunk(g.generator.VideoFile, options)
}
videoOutputPath := path.Join(g.OutputDirectory, g.VideoFilename)
encoder.ScenePreviewVideoChunkCombine(g.generator.VideoFile, g.getConcatFilePath(), videoOutputPath)
logger.Debug("created video preview: ", videoOutputPath)
return nil
}
func (g *PreviewGenerator) generateImage(encoder *ffmpeg.Encoder) error {
outputPath := path.Join(g.OutputDirectory, g.ImageFilename)
outputExists, _ := utils.FileExists(outputPath)
if outputExists {
return nil
}
videoPreviewPath := path.Join(g.OutputDirectory, g.VideoFilename)
tmpOutputPath := instance.Paths.Generated.GetTmpPath(g.ImageFilename)
if err := encoder.ScenePreviewVideoToImage(g.generator.VideoFile, 640, videoPreviewPath, tmpOutputPath); err != nil {
return err
} else {
if err := os.Rename(tmpOutputPath, outputPath); err != nil {
return err
}
logger.Debug("created video preview image: ", outputPath)
}
return nil
}
func (g *PreviewGenerator) getConcatFilePath() string {
return instance.Paths.Generated.GetTmpPath("files.txt")
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/bmatcuk/doublestar"
"github.com/stashapp/stash/logger"
"github.com/stashapp/stash/manager/paths"
"github.com/stashapp/stash/models"
"path/filepath"
"sync"
)
@@ -85,10 +86,70 @@ func (s *singleton) Export() {
}()
}
func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcodes bool) {
if s.Status != Idle { return }
s.Status = Generate
qb := models.NewSceneQueryBuilder()
//this.job.total = await ObjectionUtils.getCount(Scene);
instance.Paths.Generated.EnsureTmpDir()
go func() {
defer s.returnToIdleState()
scenes, err := qb.All()
if err != nil {
logger.Errorf("failed to get scenes for generate")
return
}
delta := btoi(sprites) + btoi(previews) + btoi(markers) + btoi(transcodes)
var wg sync.WaitGroup
for _, scene := range scenes {
wg.Add(delta)
if sprites {
go func() {
wg.Done() // TODO
}()
}
if previews {
task := GeneratePreviewTask{Scene: scene}
go task.Start(&wg)
}
if markers {
go func() {
wg.Done() // TODO
}()
}
if transcodes {
go func() {
wg.Done() // TODO
}()
}
wg.Wait()
}
}()
}
func (s *singleton) returnToIdleState() {
if r := recover(); r!= nil {
logger.Info("recovered from ", r)
}
if s.Status == Generate {
instance.Paths.Generated.RemoveTmpDir()
}
s.Status = Idle
}
func btoi(b bool) int {
if b {
return 1
}
return 0
}

View File

@@ -0,0 +1,58 @@
package manager
import (
"github.com/stashapp/stash/ffmpeg"
"github.com/stashapp/stash/logger"
"github.com/stashapp/stash/models"
"github.com/stashapp/stash/utils"
"sync"
)
type GeneratePreviewTask struct {
Scene models.Scene
}
func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
videoFilename := t.videoFilename()
imageFilename := t.imageFilename()
if t.doesPreviewExist(videoFilename, imageFilename) {
wg.Done()
return
}
videoFile, err := ffmpeg.NewVideoFile(instance.Paths.FixedPaths.FFProbe, t.Scene.Path)
if err != nil {
logger.Errorf("error reading video file: %s", err.Error())
wg.Done()
return
}
generator, err := NewPreviewGenerator(*videoFile, videoFilename, imageFilename, instance.Paths.Generated.Screenshots)
if err != nil {
logger.Errorf("error creating preview generator: %s", err.Error())
wg.Done()
return
}
if err := generator.Generate(); err != nil {
logger.Errorf("error generating preview: %s", err.Error())
wg.Done()
return
}
wg.Done()
}
func (t *GeneratePreviewTask) doesPreviewExist(videoFilename string, imageFilename string) bool {
videoExists, _ := utils.FileExists(instance.Paths.Scene.GetStreamPreviewPath(videoFilename))
imageExists, _ := utils.FileExists(instance.Paths.Scene.GetStreamPreviewImagePath(imageFilename))
return videoExists && imageExists
}
func (t *GeneratePreviewTask) videoFilename() string {
return t.Scene.Checksum + ".mp4"
}
func (t *GeneratePreviewTask) imageFilename() string {
return t.Scene.Checksum + ".webp"
}

View File

@@ -70,8 +70,7 @@ func (t *ScanTask) scanGallery() {
}
func (t *ScanTask) scanScene() {
ffprobe := ffmpeg.NewFFProbe(instance.Paths.FixedPaths.FFProbe)
ffprobeResult, err := ffprobe.ProbeVideo(t.FilePath)
videoFile, err := ffmpeg.NewVideoFile(instance.Paths.FixedPaths.FFProbe, t.FilePath)
if err != nil {
logger.Error(err.Error())
return
@@ -90,7 +89,7 @@ func (t *ScanTask) scanScene() {
return
}
t.makeScreenshots(*ffprobeResult, checksum)
t.makeScreenshots(*videoFile, checksum)
scene, _ = qb.FindByChecksum(checksum)
ctx := context.TODO()
@@ -105,14 +104,14 @@ func (t *ScanTask) scanScene() {
newScene := models.Scene{
Checksum: checksum,
Path: t.FilePath,
Duration: sql.NullFloat64{Float64: ffprobeResult.Duration, Valid: true },
VideoCodec: sql.NullString{ String: ffprobeResult.VideoCodec, Valid: true},
AudioCodec: sql.NullString{ String: ffprobeResult.AudioCodec, Valid: true},
Width: sql.NullInt64{ Int64: int64(ffprobeResult.Width), Valid: true },
Height: sql.NullInt64{ Int64: int64(ffprobeResult.Height), Valid: true },
Framerate: sql.NullFloat64{ Float64: ffprobeResult.FrameRate, Valid: true },
Bitrate: sql.NullInt64{ Int64: ffprobeResult.Bitrate, Valid: true },
Size: sql.NullString{ String: strconv.Itoa(int(ffprobeResult.Size)), Valid: true },
Duration: sql.NullFloat64{Float64: videoFile.Duration, Valid: true },
VideoCodec: sql.NullString{ String: videoFile.VideoCodec, Valid: true},
AudioCodec: sql.NullString{ String: videoFile.AudioCodec, Valid: true},
Width: sql.NullInt64{ Int64: int64(videoFile.Width), Valid: true },
Height: sql.NullInt64{ Int64: int64(videoFile.Height), Valid: true },
Framerate: sql.NullFloat64{ Float64: videoFile.FrameRate, Valid: true },
Bitrate: sql.NullInt64{ Int64: videoFile.Bitrate, Valid: true },
Size: sql.NullString{ String: strconv.Itoa(int(videoFile.Size)), Valid: true },
CreatedAt: models.SQLiteTimestamp{ Timestamp: currentTime },
UpdatedAt: models.SQLiteTimestamp{ Timestamp: currentTime },
}
@@ -127,7 +126,7 @@ func (t *ScanTask) scanScene() {
}
}
func (t *ScanTask) makeScreenshots(probeResult ffmpeg.FFProbeResult, checksum string) {
func (t *ScanTask) makeScreenshots(probeResult ffmpeg.VideoFile, checksum string) {
thumbPath := instance.Paths.Scene.GetThumbnailScreenshotPath(checksum)
normalPath := instance.Paths.Scene.GetScreenshotPath(checksum)
@@ -142,7 +141,7 @@ func (t *ScanTask) makeScreenshots(probeResult ffmpeg.FFProbeResult, checksum st
t.makeScreenshot(probeResult, normalPath, 2, probeResult.Width)
}
func (t *ScanTask) makeScreenshot(probeResult ffmpeg.FFProbeResult, outputPath string, quality int, width int) {
func (t *ScanTask) makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int) {
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG)
options := ffmpeg.ScreenshotOptions{
OutputPath: outputPath,