Videos: Transcode supported formats if bitrate exceeds limit #2461

Implements Option (1) as described in the issue comments.
This commit is contained in:
Michael Mayer 2022-06-26 19:47:12 +02:00
parent 1a6f108bbe
commit b09112058e
6 changed files with 87 additions and 9 deletions

View file

@ -3,6 +3,7 @@ package api
import (
"fmt"
"net/http"
"strings"
"github.com/photoprism/photoprism/pkg/video"
@ -64,6 +65,14 @@ func GetVideo(router *gin.RouterGroup) {
}
fileName := photoprism.FileName(f.FileRoot, f.FileName)
fileBitrate := f.Bitrate()
// File format supported by the client/browser?
supported := f.FileCodec != "" && f.FileCodec == string(format.Codec) || format.Codec == video.UnknownCodec && f.FileType == string(format.File)
// File bitrate too high (for streaming)?
conf := service.Config()
transcode := !supported || conf.FFmpegEnabled() && conf.FFmpegBitrateExceeded(fileBitrate)
if mf, err := photoprism.NewMediaFile(fileName); err != nil {
// Set missing flag so that the file doesn't show up in search results anymore.
@ -73,15 +82,13 @@ func GetVideo(router *gin.RouterGroup) {
log.Errorf("video: file %s is missing", clean.Log(f.FileName))
fileName = service.Config().StaticFile("video/404.mp4")
AddContentTypeHeader(c, ContentTypeAvc)
} else if f.FileCodec != "" && f.FileCodec == string(format.Codec) || format.Codec == video.UnknownCodec && f.FileType == string(format.File) {
if f.FileCodec != "" && f.FileCodec != f.FileType {
log.Debugf("video: %s has matching codec %s", clean.Log(f.FileName), clean.Log(f.FileCodec))
AddContentTypeHeader(c, fmt.Sprintf("%s; codecs=\"%s\"", f.FileMime, clean.Codec(f.FileCodec)))
} else if transcode {
if f.FileCodec != "" {
log.Debugf("video: %s is %s compressed and cannot be streamed directly, average bitrate %.1f MBit/s", clean.Log(f.FileName), clean.Log(strings.ToUpper(f.FileCodec)), fileBitrate)
} else {
log.Debugf("video: %s has matching type %s", clean.Log(f.FileName), clean.Log(f.FileType))
AddContentTypeHeader(c, f.FileMime)
log.Debugf("video: %s cannot be streamed directly, average bitrate %.1f MBit/s", clean.Log(f.FileName), fileBitrate)
}
} else {
conv := service.Convert()
if avcFile, err := conv.ToAvc(mf, service.Config().FFmpegEncoder(), false, false); err != nil {
@ -93,6 +100,14 @@ func GetVideo(router *gin.RouterGroup) {
}
AddContentTypeHeader(c, ContentTypeAvc)
} else {
if f.FileCodec != "" && f.FileCodec != f.FileType {
log.Debugf("video: %s is %s compressed and requires no transcoding, average bitrate %.1f MBit/s", clean.Log(f.FileName), clean.Log(strings.ToUpper(f.FileCodec)), fileBitrate)
AddContentTypeHeader(c, fmt.Sprintf("%s; codecs=\"%s\"", f.FileMime, clean.Codec(f.FileCodec)))
} else {
log.Debugf("video: %s is streamed directly, average bitrate %.1f MBit/s", clean.Log(f.FileName), fileBitrate)
AddContentTypeHeader(c, f.FileMime)
}
}
if c.Query("download") != "" {

View file

@ -28,3 +28,14 @@ func (c *Config) FFmpegBitrate() int {
return c.options.FFmpegBitrate
}
}
// FFmpegBitrateExceeded tests if the ffmpeg bitrate limit is exceeded.
func (c *Config) FFmpegBitrateExceeded(mbit float64) bool {
if mbit <= 0 {
return false
} else if max := c.FFmpegBitrate(); max <= 0 {
return false
} else {
return mbit > float64(max)
}
}

View file

@ -42,3 +42,24 @@ func TestConfig_FFmpegBitrate(t *testing.T) {
c.options.FFmpegBitrate = 800
assert.Equal(t, 800, c.FFmpegBitrate())
}
func TestConfig_FFmpegBitrateExceeded(t *testing.T) {
c := NewConfig(CliTestContext())
c.options.FFmpegBitrate = 0
assert.False(t, c.FFmpegBitrateExceeded(0.95))
assert.False(t, c.FFmpegBitrateExceeded(1.05))
assert.False(t, c.FFmpegBitrateExceeded(2.05))
c.options.FFmpegBitrate = 1
assert.False(t, c.FFmpegBitrateExceeded(0.95))
assert.False(t, c.FFmpegBitrateExceeded(1.0))
assert.True(t, c.FFmpegBitrateExceeded(1.05))
assert.True(t, c.FFmpegBitrateExceeded(2.05))
c.options.FFmpegBitrate = 50
assert.False(t, c.FFmpegBitrateExceeded(0.95))
assert.False(t, c.FFmpegBitrateExceeded(1.05))
assert.False(t, c.FFmpegBitrateExceeded(2.05))
c.options.FFmpegBitrate = -5
assert.False(t, c.FFmpegBitrateExceeded(0.95))
assert.False(t, c.FFmpegBitrateExceeded(1.05))
assert.False(t, c.FFmpegBitrateExceeded(2.05))
}

View file

@ -661,6 +661,17 @@ func (m *File) SetDuration(d time.Duration) {
}
}
// Bitrate returns the average bitrate in MBit/s if the file has a duration.
func (m *File) Bitrate() float64 {
// Make sure size and duration have a positive value.
if m.FileSize <= 0 || m.FileDuration <= 0 {
return 0
}
// Divide number of bits through the duration in seconds.
return ((float64(m.FileSize) * 8) / m.FileDuration.Seconds()) / 1e6
}
// SetFPS sets the average number of frames per second.
func (m *File) SetFPS(frameRate float64) {
if frameRate <= 0 {

View file

@ -780,3 +780,21 @@ func TestFile_SetDuration(t *testing.T) {
assert.Equal(t, 216000, m.FileFrames)
})
}
func TestFile_Bitrate(t *testing.T) {
t.Run("HasDuration", func(t *testing.T) {
m := File{FileDuration: 1e9 * 20.302, FileSize: 1826192}
assert.InEpsilon(t, 0.719, m.Bitrate(), 0.01)
})
t.Run("NoDuration", func(t *testing.T) {
m := File{FileDuration: 0, FileSize: 1826192}
assert.Equal(t, float64(0), m.Bitrate())
})
t.Run("NoSize", func(t *testing.T) {
m := File{FileDuration: 1e9 * 20.302, FileSize: 0}
assert.Equal(t, float64(0), m.Bitrate())
})
}

View file

@ -139,9 +139,11 @@ func (c *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg
case bitrate == "":
return nil, false, fmt.Errorf("convert: transcoding bitrate is empty - possible bug")
case ffmpegBin == "":
return nil, false, fmt.Errorf("convert: ffmpeg must be installed to transcode %s to avc", clean.Log(f.BaseName()))
return nil, false, fmt.Errorf("convert: ffmpeg must be installed to transcode %s", clean.Log(f.BaseName()))
case c.conf.DisableFFmpeg():
return nil, false, fmt.Errorf("convert: ffmpeg must be enabled to transcode %s", clean.Log(f.BaseName()))
case !f.IsAnimated():
return nil, false, fmt.Errorf("convert: file type %s of %s cannot be transcoded to avc", f.FileType(), clean.Log(f.BaseName()))
return nil, false, fmt.Errorf("convert: file type %s of %s cannot be transcoded", f.FileType(), clean.Log(f.BaseName()))
}
return ffmpeg.AvcConvertCommand(fileName, avcName, ffmpegBin, c.AvcBitrate(f), encoder)