mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Fix a few cases where ffmpeg produces no output (#3161)
* Treat no output from ffmpeg as an error condition * Distinguish file vs. video duration, and use later where appropriate * Check for empty file in generateFile Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
045ba55def
commit
abc9ec648a
6 changed files with 44 additions and 20 deletions
|
|
@ -43,8 +43,8 @@ func (g *generatorInfo) calculateFrameRate(videoStream *ffmpeg.FFProbeStream) er
|
||||||
|
|
||||||
numberOfFrames, _ := strconv.Atoi(videoStream.NbFrames)
|
numberOfFrames, _ := strconv.Atoi(videoStream.NbFrames)
|
||||||
|
|
||||||
if numberOfFrames == 0 && isValidFloat64(framerate) && g.VideoFile.Duration > 0 { // TODO: test
|
if numberOfFrames == 0 && isValidFloat64(framerate) && g.VideoFile.VideoStreamDuration > 0 { // TODO: test
|
||||||
numberOfFrames = int(framerate * g.VideoFile.Duration)
|
numberOfFrames = int(framerate * g.VideoFile.VideoStreamDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are missing the frame count or frame rate then seek through the file and extract the info with regex
|
// If we are missing the frame count or frame rate then seek through the file and extract the info with regex
|
||||||
|
|
@ -68,7 +68,7 @@ func (g *generatorInfo) calculateFrameRate(videoStream *ffmpeg.FFProbeStream) er
|
||||||
"number of frames or framerate is 0. nb_frames <%s> framerate <%f> duration <%f>",
|
"number of frames or framerate is 0. nb_frames <%s> framerate <%f> duration <%f>",
|
||||||
videoStream.NbFrames,
|
videoStream.NbFrames,
|
||||||
framerate,
|
framerate,
|
||||||
g.VideoFile.Duration,
|
g.VideoFile.VideoStreamDuration,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,12 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO
|
||||||
chunkCount := rows * cols
|
chunkCount := rows * cols
|
||||||
|
|
||||||
// For files with small duration / low frame count try to seek using frame number intead of seconds
|
// 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.VideoStreamDuration < 5 || (0 < videoFile.FrameCount && videoFile.FrameCount <= int64(chunkCount)) { // some files can have FrameCount == 0, only use SlowSeek if duration < 5
|
||||||
if videoFile.Duration <= 0 {
|
if videoFile.VideoStreamDuration <= 0 {
|
||||||
s := fmt.Sprintf("video %s: duration(%.3f)/frame count(%d) invalid, skipping sprite creation", videoFile.Path, videoFile.Duration, videoFile.FrameCount)
|
s := fmt.Sprintf("video %s: duration(%.3f)/frame count(%d) invalid, skipping sprite creation", videoFile.Path, videoFile.VideoStreamDuration, videoFile.FrameCount)
|
||||||
return nil, errors.New(s)
|
return nil, errors.New(s)
|
||||||
}
|
}
|
||||||
logger.Warnf("[generator] video %s too short (%.3fs, %d frames), using frame seeking", videoFile.Path, videoFile.Duration, videoFile.FrameCount)
|
logger.Warnf("[generator] video %s too short (%.3fs, %d frames), using frame seeking", videoFile.Path, videoFile.VideoStreamDuration, videoFile.FrameCount)
|
||||||
slowSeek = true
|
slowSeek = true
|
||||||
// do an actual frame count of the file ( number of frames = read frames)
|
// do an actual frame count of the file ( number of frames = read frames)
|
||||||
ffprobe := GetInstance().FFProbe
|
ffprobe := GetInstance().FFProbe
|
||||||
|
|
@ -102,7 +102,7 @@ func (g *SpriteGenerator) generateSpriteImage() error {
|
||||||
if !g.SlowSeek {
|
if !g.SlowSeek {
|
||||||
logger.Infof("[generator] generating sprite image for %s", g.Info.VideoFile.Path)
|
logger.Infof("[generator] generating sprite image for %s", g.Info.VideoFile.Path)
|
||||||
// generate `ChunkCount` thumbnails
|
// generate `ChunkCount` thumbnails
|
||||||
stepSize := g.Info.VideoFile.Duration / float64(g.Info.ChunkCount)
|
stepSize := g.Info.VideoFile.VideoStreamDuration / float64(g.Info.ChunkCount)
|
||||||
|
|
||||||
for i := 0; i < g.Info.ChunkCount; i++ {
|
for i := 0; i < g.Info.ChunkCount; i++ {
|
||||||
time := float64(i) * stepSize
|
time := float64(i) * stepSize
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) {
|
||||||
|
|
||||||
videoChecksum := t.Scene.GetHash(t.fileNamingAlgorithm)
|
videoChecksum := t.Scene.GetHash(t.fileNamingAlgorithm)
|
||||||
|
|
||||||
if err := t.generateVideo(videoChecksum, videoFile.Duration); err != nil {
|
if err := t.generateVideo(videoChecksum, videoFile.VideoStreamDuration); err != nil {
|
||||||
logger.Errorf("error generating preview: %v", err)
|
logger.Errorf("error generating preview: %v", err)
|
||||||
logErrorOutput(err)
|
logErrorOutput(err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,20 @@ type VideoFile struct {
|
||||||
AudioStream *FFProbeStream
|
AudioStream *FFProbeStream
|
||||||
VideoStream *FFProbeStream
|
VideoStream *FFProbeStream
|
||||||
|
|
||||||
Path string
|
Path string
|
||||||
Title string
|
Title string
|
||||||
Comment string
|
Comment string
|
||||||
Container string
|
Container string
|
||||||
Duration float64
|
// FileDuration is the declared (meta-data) duration of the *file*.
|
||||||
StartTime float64
|
// In most cases (sprites, previews, etc.) we actually care about the duration of the video stream specifically,
|
||||||
Bitrate int64
|
// because those two can differ slightly (e.g. audio stream longer than the video stream, making the whole file
|
||||||
Size int64
|
// longer).
|
||||||
CreationTime time.Time
|
FileDuration float64
|
||||||
|
VideoStreamDuration float64
|
||||||
|
StartTime float64
|
||||||
|
Bitrate int64
|
||||||
|
Size int64
|
||||||
|
CreationTime time.Time
|
||||||
|
|
||||||
VideoCodec string
|
VideoCodec string
|
||||||
VideoBitrate int64
|
VideoBitrate int64
|
||||||
|
|
@ -127,7 +132,7 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
||||||
|
|
||||||
result.Container = probeJSON.Format.FormatName
|
result.Container = probeJSON.Format.FormatName
|
||||||
duration, _ := strconv.ParseFloat(probeJSON.Format.Duration, 64)
|
duration, _ := strconv.ParseFloat(probeJSON.Format.Duration, 64)
|
||||||
result.Duration = math.Round(duration*100) / 100
|
result.FileDuration = math.Round(duration*100) / 100
|
||||||
fileStat, err := os.Stat(filePath)
|
fileStat, err := os.Stat(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
statErr := fmt.Errorf("error statting file <%s>: %w", filePath, err)
|
statErr := fmt.Errorf("error statting file <%s>: %w", filePath, err)
|
||||||
|
|
@ -178,6 +183,11 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
||||||
result.Width = videoStream.Width
|
result.Width = videoStream.Width
|
||||||
result.Height = videoStream.Height
|
result.Height = videoStream.Height
|
||||||
}
|
}
|
||||||
|
result.VideoStreamDuration, err = strconv.ParseFloat(videoStream.Duration, 64)
|
||||||
|
if err != nil {
|
||||||
|
// Revert to the historical behaviour, which is still correct in the vast majority of cases.
|
||||||
|
result.VideoStreamDuration = result.FileDuration
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file
|
||||||
AudioCodec: videoFile.AudioCodec,
|
AudioCodec: videoFile.AudioCodec,
|
||||||
Width: videoFile.Width,
|
Width: videoFile.Width,
|
||||||
Height: videoFile.Height,
|
Height: videoFile.Height,
|
||||||
Duration: videoFile.Duration,
|
Duration: videoFile.FileDuration,
|
||||||
FrameRate: videoFile.FrameRate,
|
FrameRate: videoFile.FrameRate,
|
||||||
BitRate: videoFile.Bitrate,
|
BitRate: videoFile.Bitrate,
|
||||||
Interactive: interactive,
|
Interactive: interactive,
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,16 @@ func (g Generator) generateFile(lockCtx *fsutil.LockContext, p Paths, pattern st
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if generated empty file
|
||||||
|
stat, err := os.Stat(tmpFn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting file stat: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.Size() == 0 {
|
||||||
|
return fmt.Errorf("ffmpeg command produced no output")
|
||||||
|
}
|
||||||
|
|
||||||
if err := fsutil.SafeMove(tmpFn, output); err != nil {
|
if err := fsutil.SafeMove(tmpFn, output); err != nil {
|
||||||
return fmt.Errorf("moving %s to %s", tmpFn, output)
|
return fmt.Errorf("moving %s to %s", tmpFn, output)
|
||||||
}
|
}
|
||||||
|
|
@ -142,5 +152,9 @@ func (g Generator) generateOutput(lockCtx *fsutil.LockContext, args []string) ([
|
||||||
return nil, fmt.Errorf("error running ffmpeg command <%s>: %w", strings.Join(args, " "), err)
|
return nil, fmt.Errorf("error running ffmpeg command <%s>: %w", strings.Join(args, " "), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if stdout.Len() == 0 {
|
||||||
|
return nil, fmt.Errorf("ffmpeg command produced no output: <%s>", strings.Join(args, " "))
|
||||||
|
}
|
||||||
|
|
||||||
return stdout.Bytes(), nil
|
return stdout.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue