mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Config: Add preset, quality, and device options for FFmpeg #4969
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
|||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FFmpegBin returns the ffmpeg executable file name.
|
// FFmpegBin returns the ffmpeg executable file name.
|
||||||
@@ -33,13 +35,31 @@ func (c *Config) FFmpegSize() int {
|
|||||||
return thumb.VideoSize(c.options.FFmpegSize).Width
|
return thumb.VideoSize(c.options.FFmpegSize).Width
|
||||||
}
|
}
|
||||||
|
|
||||||
// FFmpegBitrate returns the ffmpeg bitrate limit in Mbps.
|
// FFmpegQuality returns the ffmpeg encoding quality from 1 to 100,
|
||||||
|
// with a default of 50 and where 100 is almost lossless.
|
||||||
|
func (c *Config) FFmpegQuality() int {
|
||||||
|
switch {
|
||||||
|
case c.options.FFmpegQuality <= 0:
|
||||||
|
return encode.DefaultQuality
|
||||||
|
case c.options.FFmpegQuality < encode.WorstQuality:
|
||||||
|
return encode.WorstQuality
|
||||||
|
case c.options.FFmpegQuality > 100:
|
||||||
|
return encode.BestQuality
|
||||||
|
default:
|
||||||
|
return c.options.FFmpegQuality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFmpegBitrate returns the ffmpeg bitrate limit in Mbps for non-AVC videos to be transcoded
|
||||||
|
// even if they could be played natively (optional).
|
||||||
func (c *Config) FFmpegBitrate() int {
|
func (c *Config) FFmpegBitrate() int {
|
||||||
switch {
|
switch {
|
||||||
case c.options.FFmpegBitrate <= 0:
|
case c.options.FFmpegBitrate <= 0:
|
||||||
return 60
|
return encode.DefaultBitrateLimit
|
||||||
case c.options.FFmpegBitrate >= 960:
|
case c.options.FFmpegBitrate < encode.MinBitrateLimit:
|
||||||
return 960
|
return encode.MinBitrateLimit
|
||||||
|
case c.options.FFmpegBitrate >= encode.MaxBitrateLimit:
|
||||||
|
return encode.MaxBitrateLimit
|
||||||
default:
|
default:
|
||||||
return c.options.FFmpegBitrate
|
return c.options.FFmpegBitrate
|
||||||
}
|
}
|
||||||
@@ -56,10 +76,33 @@ func (c *Config) FFmpegBitrateExceeded(bitrate float64) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FFmpegPreset returns the ffmpeg encoding preset from "ultrafast" to "veryslow",
|
||||||
|
// see https://trac.ffmpeg.org/wiki/Encode/H.264#Preset.
|
||||||
|
func (c *Config) FFmpegPreset() string {
|
||||||
|
if c.options.FFmpegPreset == "" {
|
||||||
|
return encode.PresetFast
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.FFmpegPreset
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFmpegDevice returns the ffmpeg device path for supported hardware encoders (optional).
|
||||||
|
func (c *Config) FFmpegDevice() string {
|
||||||
|
if c.options.FFmpegDevice == "" {
|
||||||
|
return ""
|
||||||
|
} else if txt.IsUInt(c.options.FFmpegDevice) {
|
||||||
|
return c.options.FFmpegDevice
|
||||||
|
} else if fs.DeviceExists(c.options.FFmpegDevice) {
|
||||||
|
return c.options.FFmpegDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// FFmpegMapVideo returns the video streams to be transcoded as string.
|
// FFmpegMapVideo returns the video streams to be transcoded as string.
|
||||||
func (c *Config) FFmpegMapVideo() string {
|
func (c *Config) FFmpegMapVideo() string {
|
||||||
if c.options.FFmpegMapVideo == "" {
|
if c.options.FFmpegMapVideo == "" {
|
||||||
return encode.MapVideo
|
return encode.DefaultMapVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.options.FFmpegMapVideo
|
return c.options.FFmpegMapVideo
|
||||||
@@ -68,7 +111,7 @@ func (c *Config) FFmpegMapVideo() string {
|
|||||||
// FFmpegMapAudio returns the audio streams to be transcoded as string.
|
// FFmpegMapAudio returns the audio streams to be transcoded as string.
|
||||||
func (c *Config) FFmpegMapAudio() string {
|
func (c *Config) FFmpegMapAudio() string {
|
||||||
if c.options.FFmpegMapAudio == "" {
|
if c.options.FFmpegMapAudio == "" {
|
||||||
return encode.MapAudio
|
return encode.DefaultMapAudio
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.options.FFmpegMapAudio
|
return c.options.FFmpegMapAudio
|
||||||
@@ -77,7 +120,7 @@ func (c *Config) FFmpegMapAudio() string {
|
|||||||
// FFmpegOptions returns the FFmpeg options to use for video transcoding.
|
// FFmpegOptions returns the FFmpeg options to use for video transcoding.
|
||||||
func (c *Config) FFmpegOptions(encoder encode.Encoder, bitrate string) (encode.Options, error) {
|
func (c *Config) FFmpegOptions(encoder encode.Encoder, bitrate string) (encode.Options, error) {
|
||||||
// Get options to transcode other formats with FFmpeg.
|
// Get options to transcode other formats with FFmpeg.
|
||||||
opt := encode.NewVideoOptions(c.FFmpegBin(), encoder, c.FFmpegSize(), bitrate, c.FFmpegMapVideo(), c.FFmpegMapAudio())
|
opt := encode.NewVideoOptions(c.FFmpegBin(), encoder, c.FFmpegSize(), c.FFmpegQuality(), c.FFmpegPreset(), c.FFmpegDevice(), c.FFmpegMapVideo(), c.FFmpegMapAudio())
|
||||||
|
|
||||||
// Check options and return error if invalid.
|
// Check options and return error if invalid.
|
||||||
if opt.Bin == "" {
|
if opt.Bin == "" {
|
||||||
|
|||||||
@@ -32,13 +32,16 @@ func TestConfig_FFmpegEnabled(t *testing.T) {
|
|||||||
|
|
||||||
func TestConfig_FFmpegBitrate(t *testing.T) {
|
func TestConfig_FFmpegBitrate(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
assert.Equal(t, 60, c.FFmpegBitrate())
|
assert.Equal(t, encode.DefaultBitrateLimit, c.FFmpegBitrate())
|
||||||
|
|
||||||
c.options.FFmpegBitrate = 1000
|
c.options.FFmpegBitrate = 1000
|
||||||
assert.Equal(t, 960, c.FFmpegBitrate())
|
assert.Equal(t, encode.MaxBitrateLimit, c.FFmpegBitrate())
|
||||||
|
|
||||||
c.options.FFmpegBitrate = -5
|
c.options.FFmpegBitrate = -5
|
||||||
assert.Equal(t, 60, c.FFmpegBitrate())
|
assert.Equal(t, encode.DefaultBitrateLimit, c.FFmpegBitrate())
|
||||||
|
|
||||||
|
c.options.FFmpegBitrate = 1
|
||||||
|
assert.Equal(t, encode.MinBitrateLimit, c.FFmpegBitrate())
|
||||||
|
|
||||||
c.options.FFmpegBitrate = 800
|
c.options.FFmpegBitrate = 800
|
||||||
assert.Equal(t, 800, c.FFmpegBitrate())
|
assert.Equal(t, 800, c.FFmpegBitrate())
|
||||||
@@ -70,6 +73,11 @@ func TestConfig_FFmpegSize(t *testing.T) {
|
|||||||
assert.Equal(t, thumb.Sizes[thumb.Fit7680].Width, c.FFmpegSize())
|
assert.Equal(t, thumb.Sizes[thumb.Fit7680].Width, c.FFmpegSize())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_FFmpegQuality(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
assert.Equal(t, encode.DefaultQuality, c.FFmpegQuality())
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfig_FFmpegBitrateExceeded(t *testing.T) {
|
func TestConfig_FFmpegBitrateExceeded(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
c.options.FFmpegBitrate = 0
|
c.options.FFmpegBitrate = 0
|
||||||
@@ -80,7 +88,7 @@ func TestConfig_FFmpegBitrateExceeded(t *testing.T) {
|
|||||||
assert.False(t, c.FFmpegBitrateExceeded(0.95))
|
assert.False(t, c.FFmpegBitrateExceeded(0.95))
|
||||||
assert.False(t, c.FFmpegBitrateExceeded(1.0))
|
assert.False(t, c.FFmpegBitrateExceeded(1.0))
|
||||||
assert.True(t, c.FFmpegBitrateExceeded(1.05))
|
assert.True(t, c.FFmpegBitrateExceeded(1.05))
|
||||||
assert.True(t, c.FFmpegBitrateExceeded(2.05))
|
assert.True(t, c.FFmpegBitrateExceeded(6.05))
|
||||||
c.options.FFmpegBitrate = 50
|
c.options.FFmpegBitrate = 50
|
||||||
assert.False(t, c.FFmpegBitrateExceeded(0.95))
|
assert.False(t, c.FFmpegBitrateExceeded(0.95))
|
||||||
assert.False(t, c.FFmpegBitrateExceeded(1.05))
|
assert.False(t, c.FFmpegBitrateExceeded(1.05))
|
||||||
@@ -91,14 +99,28 @@ func TestConfig_FFmpegBitrateExceeded(t *testing.T) {
|
|||||||
assert.False(t, c.FFmpegBitrateExceeded(2.05))
|
assert.False(t, c.FFmpegBitrateExceeded(2.05))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_FFmpegPreset(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
assert.Equal(t, encode.PresetFast, c.FFmpegPreset())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_FFmpegDevice(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
assert.Equal(t, "", c.FFmpegDevice())
|
||||||
|
c.options.FFmpegDevice = "0"
|
||||||
|
assert.Equal(t, "0", c.FFmpegDevice())
|
||||||
|
c.options.FFmpegDevice = ""
|
||||||
|
assert.Equal(t, "", c.FFmpegDevice())
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfig_FFmpegMapVideo(t *testing.T) {
|
func TestConfig_FFmpegMapVideo(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
assert.Equal(t, encode.MapVideo, c.FFmpegMapVideo())
|
assert.Equal(t, encode.DefaultMapVideo, c.FFmpegMapVideo())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_FFmpegMapAudio(t *testing.T) {
|
func TestConfig_FFmpegMapAudio(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
assert.Equal(t, encode.MapAudio, c.FFmpegMapAudio())
|
assert.Equal(t, encode.DefaultMapAudio, c.FFmpegMapAudio())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_FFmpegOptions(t *testing.T) {
|
func TestConfig_FFmpegOptions(t *testing.T) {
|
||||||
@@ -108,9 +130,8 @@ func TestConfig_FFmpegOptions(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, c.FFmpegBin(), opt.Bin)
|
assert.Equal(t, c.FFmpegBin(), opt.Bin)
|
||||||
assert.Equal(t, encode.SoftwareAvc, opt.Encoder)
|
assert.Equal(t, encode.SoftwareAvc, opt.Encoder)
|
||||||
assert.Equal(t, bitrate, opt.BitrateLimit)
|
assert.Equal(t, encode.DefaultMapVideo, opt.MapVideo)
|
||||||
assert.Equal(t, encode.MapVideo, opt.MapVideo)
|
assert.Equal(t, encode.DefaultMapAudio, opt.MapAudio)
|
||||||
assert.Equal(t, encode.MapAudio, opt.MapAudio)
|
|
||||||
assert.Equal(t, c.FFmpegMapVideo(), opt.MapVideo)
|
assert.Equal(t, c.FFmpegMapVideo(), opt.MapVideo)
|
||||||
assert.Equal(t, c.FFmpegMapAudio(), opt.MapAudio)
|
assert.Equal(t, c.FFmpegMapAudio(), opt.MapAudio)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -794,36 +794,51 @@ var Flags = CliFlags{
|
|||||||
Flag: &cli.StringFlag{
|
Flag: &cli.StringFlag{
|
||||||
Name: "ffmpeg-encoder",
|
Name: "ffmpeg-encoder",
|
||||||
Aliases: []string{"vc"},
|
Aliases: []string{"vc"},
|
||||||
Usage: "FFmpeg AVC encoder `NAME`",
|
Usage: "FFmpeg AVC video encoder `NAME`",
|
||||||
Value: "libx264",
|
Value: "libx264",
|
||||||
EnvVars: EnvVars("FFMPEG_ENCODER"),
|
EnvVars: EnvVars("FFMPEG_ENCODER"),
|
||||||
}}, {
|
}}, {
|
||||||
Flag: &cli.IntFlag{
|
Flag: &cli.IntFlag{
|
||||||
Name: "ffmpeg-size",
|
Name: "ffmpeg-size",
|
||||||
Aliases: []string{"vs"},
|
Usage: "encoding resolution limit in `PIXELS` (720-7680)",
|
||||||
Usage: "maximum video size in `PIXELS` (720-7680)",
|
|
||||||
Value: thumb.Sizes[thumb.Fit4096].Width,
|
Value: thumb.Sizes[thumb.Fit4096].Width,
|
||||||
EnvVars: EnvVars("FFMPEG_SIZE"),
|
EnvVars: EnvVars("FFMPEG_SIZE"),
|
||||||
}}, {
|
}}, {
|
||||||
|
Flag: &cli.IntFlag{
|
||||||
|
Name: "ffmpeg-quality",
|
||||||
|
Usage: fmt.Sprintf("encoding `QUALITY` (%d-%d, where %d is almost lossless)", encode.WorstQuality, encode.BestQuality, encode.BestQuality),
|
||||||
|
Value: encode.DefaultQuality,
|
||||||
|
EnvVars: EnvVars("FFMPEG_QUALITY"),
|
||||||
|
}}, {
|
||||||
Flag: &cli.IntFlag{
|
Flag: &cli.IntFlag{
|
||||||
Name: "ffmpeg-bitrate",
|
Name: "ffmpeg-bitrate",
|
||||||
Aliases: []string{"vb"},
|
Usage: fmt.Sprintf("bitrate `LIMIT` in Mbps for forced transcoding of non-AVC videos (%d-%d; %d to disable)", encode.MinBitrateLimit, encode.MaxBitrateLimit, encode.NoBitrateLimit),
|
||||||
Usage: "maximum video `BITRATE` in Mbps",
|
Value: encode.DefaultBitrateLimit,
|
||||||
Value: 60,
|
|
||||||
EnvVars: EnvVars("FFMPEG_BITRATE"),
|
EnvVars: EnvVars("FFMPEG_BITRATE"),
|
||||||
}}, {
|
}}, {
|
||||||
|
Flag: &cli.StringFlag{
|
||||||
|
Name: "ffmpeg-preset",
|
||||||
|
Usage: "FFmpeg compression `PRESET` when using an encoder that supports it, e.g. fast, medium, or slow",
|
||||||
|
Value: encode.PresetFast,
|
||||||
|
EnvVars: EnvVars("FFMPEG_PRESET"),
|
||||||
|
}}, {
|
||||||
|
Flag: &cli.StringFlag{
|
||||||
|
Name: "ffmpeg-device",
|
||||||
|
Usage: "FFmpeg device `PATH` when using a hardware encoder that supports it as parameter",
|
||||||
|
EnvVars: EnvVars("FFMPEG_DEVICE"),
|
||||||
|
}}, {
|
||||||
Flag: &cli.StringFlag{
|
Flag: &cli.StringFlag{
|
||||||
Name: "ffmpeg-map-video",
|
Name: "ffmpeg-map-video",
|
||||||
Usage: "video `STREAMS` that should be transcoded",
|
Usage: "transcoding video stream `MAP`",
|
||||||
Value: encode.MapVideo,
|
Value: encode.DefaultMapVideo,
|
||||||
EnvVars: EnvVars("FFMPEG_MAP_VIDEO"),
|
EnvVars: EnvVars("FFMPEG_MAP_VIDEO"),
|
||||||
}, DocDefault: fmt.Sprintf("`%s`", encode.MapVideo)}, {
|
}, DocDefault: fmt.Sprintf("`%s`", encode.DefaultMapVideo)}, {
|
||||||
Flag: &cli.StringFlag{
|
Flag: &cli.StringFlag{
|
||||||
Name: "ffmpeg-map-audio",
|
Name: "ffmpeg-map-audio",
|
||||||
Usage: "audio `STREAMS` that should be transcoded",
|
Usage: "transcoding audio stream `MAP`",
|
||||||
Value: encode.MapAudio,
|
Value: encode.DefaultMapAudio,
|
||||||
EnvVars: EnvVars("FFMPEG_MAP_AUDIO"),
|
EnvVars: EnvVars("FFMPEG_MAP_AUDIO"),
|
||||||
}, DocDefault: fmt.Sprintf("`%s`", encode.MapAudio)}, {
|
}, DocDefault: fmt.Sprintf("`%s`", encode.DefaultMapAudio)}, {
|
||||||
Flag: &cli.StringFlag{
|
Flag: &cli.StringFlag{
|
||||||
Name: "exiftool-bin",
|
Name: "exiftool-bin",
|
||||||
Usage: "ExifTool `COMMAND` for extracting metadata",
|
Usage: "ExifTool `COMMAND` for extracting metadata",
|
||||||
|
|||||||
@@ -167,7 +167,10 @@ type Options struct {
|
|||||||
FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"`
|
FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"`
|
||||||
FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"`
|
FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"`
|
||||||
FFmpegSize int `yaml:"FFmpegSize" json:"FFmpegSize" flag:"ffmpeg-size"`
|
FFmpegSize int `yaml:"FFmpegSize" json:"FFmpegSize" flag:"ffmpeg-size"`
|
||||||
|
FFmpegQuality int `yaml:"FFmpegQuality" json:"FFmpegQuality" flag:"ffmpeg-quality"`
|
||||||
FFmpegBitrate int `yaml:"FFmpegBitrate" json:"FFmpegBitrate" flag:"ffmpeg-bitrate"`
|
FFmpegBitrate int `yaml:"FFmpegBitrate" json:"FFmpegBitrate" flag:"ffmpeg-bitrate"`
|
||||||
|
FFmpegPreset string `yaml:"FFmpegPreset" json:"FFmpegPreset" flag:"ffmpeg-preset"`
|
||||||
|
FFmpegDevice string `yaml:"FFmpegDevice" json:"-" flag:"ffmpeg-device"`
|
||||||
FFmpegMapVideo string `yaml:"FFmpegMapVideo" json:"FFmpegMapVideo" flag:"ffmpeg-map-video"`
|
FFmpegMapVideo string `yaml:"FFmpegMapVideo" json:"FFmpegMapVideo" flag:"ffmpeg-map-video"`
|
||||||
FFmpegMapAudio string `yaml:"FFmpegMapAudio" json:"FFmpegMapAudio" flag:"ffmpeg-map-audio"`
|
FFmpegMapAudio string `yaml:"FFmpegMapAudio" json:"FFmpegMapAudio" flag:"ffmpeg-map-audio"`
|
||||||
ExifToolBin string `yaml:"ExifToolBin" json:"-" flag:"exiftool-bin"`
|
ExifToolBin string `yaml:"ExifToolBin" json:"-" flag:"exiftool-bin"`
|
||||||
|
|||||||
@@ -211,7 +211,10 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||||||
{"ffmpeg-bin", c.FFmpegBin()},
|
{"ffmpeg-bin", c.FFmpegBin()},
|
||||||
{"ffmpeg-encoder", c.FFmpegEncoder().String()},
|
{"ffmpeg-encoder", c.FFmpegEncoder().String()},
|
||||||
{"ffmpeg-size", fmt.Sprintf("%d", c.FFmpegSize())},
|
{"ffmpeg-size", fmt.Sprintf("%d", c.FFmpegSize())},
|
||||||
|
{"ffmpeg-quality", fmt.Sprintf("%d", c.FFmpegQuality())},
|
||||||
{"ffmpeg-bitrate", fmt.Sprintf("%d", c.FFmpegBitrate())},
|
{"ffmpeg-bitrate", fmt.Sprintf("%d", c.FFmpegBitrate())},
|
||||||
|
{"ffmpeg-preset", c.FFmpegPreset()},
|
||||||
|
{"ffmpeg-device", c.FFmpegDevice()},
|
||||||
{"ffmpeg-map-video", c.FFmpegMapVideo()},
|
{"ffmpeg-map-video", c.FFmpegMapVideo()},
|
||||||
{"ffmpeg-map-audio", c.FFmpegMapAudio()},
|
{"ffmpeg-map-audio", c.FFmpegMapAudio()},
|
||||||
{"exiftool-bin", c.ExifToolBin()},
|
{"exiftool-bin", c.ExifToolBin()},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
|||||||
// ffmpeg -hide_banner -h encoder=h264_videotoolbox
|
// ffmpeg -hide_banner -h encoder=h264_videotoolbox
|
||||||
return exec.Command(
|
return exec.Command(
|
||||||
opt.Bin,
|
opt.Bin,
|
||||||
"-y",
|
"-hide_banner", "-y",
|
||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
"-i", srcName,
|
"-i", srcName,
|
||||||
"-c:v", opt.Encoder.String(),
|
"-c:v", opt.Encoder.String(),
|
||||||
@@ -22,7 +22,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
|||||||
"-profile", "high",
|
"-profile", "high",
|
||||||
"-level", "51",
|
"-level", "51",
|
||||||
"-r", "30",
|
"-r", "30",
|
||||||
"-b:v", opt.BitrateLimit,
|
"-q:v", opt.QvQuality(),
|
||||||
"-f", "mp4",
|
"-f", "mp4",
|
||||||
"-movflags", opt.MovFlags,
|
"-movflags", opt.MovFlags,
|
||||||
destName,
|
destName,
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ import "os/exec"
|
|||||||
func TranscodeToAvcCmd(srcName, destName string, opt Options) *exec.Cmd {
|
func TranscodeToAvcCmd(srcName, destName string, opt Options) *exec.Cmd {
|
||||||
return exec.Command(
|
return exec.Command(
|
||||||
opt.Bin,
|
opt.Bin,
|
||||||
"-y",
|
"-hide_banner", "-y",
|
||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
"-i", srcName,
|
"-i", srcName,
|
||||||
"-c:v", opt.Encoder.String(),
|
"-c:v", opt.Encoder.String(),
|
||||||
"-map", opt.MapVideo,
|
"-map", opt.MapVideo,
|
||||||
"-map", opt.MapAudio,
|
"-map", opt.MapAudio,
|
||||||
"-c:a", "aac",
|
"-c:a", "aac",
|
||||||
|
"-preset", opt.Preset,
|
||||||
"-vf", opt.VideoFilter(FormatYUV420P),
|
"-vf", opt.VideoFilter(FormatYUV420P),
|
||||||
"-max_muxing_queue_size", "1024",
|
"-max_muxing_queue_size", "1024",
|
||||||
"-crf", "23",
|
|
||||||
"-r", "30",
|
"-r", "30",
|
||||||
"-b:v", opt.BitrateLimit,
|
"-crf", opt.CrfQuality(),
|
||||||
"-f", "mp4",
|
"-f", "mp4",
|
||||||
"-movflags", opt.MovFlags,
|
"-movflags", opt.MovFlags,
|
||||||
destName,
|
destName,
|
||||||
|
|||||||
18
internal/ffmpeg/encode/const.go
Normal file
18
internal/ffmpeg/encode/const.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package encode
|
||||||
|
|
||||||
|
// FFmpegBin defines the default ffmpeg binary name.
|
||||||
|
const FFmpegBin = "ffmpeg"
|
||||||
|
|
||||||
|
// Bitrate limit min, max, and default settings in MBps.
|
||||||
|
const (
|
||||||
|
NoBitrateLimit = -1
|
||||||
|
MinBitrateLimit = 1
|
||||||
|
DefaultBitrateLimit = 60
|
||||||
|
MaxBitrateLimit = 960
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default video and audio track mapping.
|
||||||
|
const (
|
||||||
|
DefaultMapVideo = "0:v:0"
|
||||||
|
DefaultMapAudio = "0:a:0?"
|
||||||
|
)
|
||||||
@@ -7,9 +7,3 @@ package encode
|
|||||||
// - https://medium.com/@vlad.pbr/in-browser-live-video-using-fragmented-mp4-3aedb600a07e
|
// - https://medium.com/@vlad.pbr/in-browser-live-video-using-fragmented-mp4-3aedb600a07e
|
||||||
// - https://github.com/video-dev/hls.js?tab=readme-ov-file#features
|
// - https://github.com/video-dev/hls.js?tab=readme-ov-file#features
|
||||||
var MovFlags = "frag_keyframe+empty_moov+default_base_moof+faststart"
|
var MovFlags = "frag_keyframe+empty_moov+default_base_moof+faststart"
|
||||||
|
|
||||||
const (
|
|
||||||
FFmpegBin = "ffmpeg"
|
|
||||||
MapVideo = "0:v:0"
|
|
||||||
MapAudio = "0:a:0?"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -7,19 +7,21 @@ import (
|
|||||||
|
|
||||||
// Options represents FFmpeg encoding options.
|
// Options represents FFmpeg encoding options.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg.
|
Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg
|
||||||
Encoder Encoder // Supported FFmpeg output Encoder.
|
Encoder Encoder // Supported FFmpeg output Encoder
|
||||||
SizeLimit int // Maximum width and height of the output video file in pixels.
|
SizeLimit int // Maximum width and height of the output video file in pixels.
|
||||||
BitrateLimit string // See https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate.
|
Quality int // See https://ffmpeg.org/ffmpeg-codecs.html
|
||||||
MapVideo string // See https://trac.ffmpeg.org/wiki/Map#Videostreamsonly.
|
Preset string // See https://trac.ffmpeg.org/wiki/Encode/H.264#Preset
|
||||||
MapAudio string // See https://trac.ffmpeg.org/wiki/Map#Audiostreamsonly.
|
Device string // See https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||||
TimeOffset string // See https://trac.ffmpeg.org/wiki/Seeking and https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax.
|
MapVideo string // See https://trac.ffmpeg.org/wiki/Map#Videostreamsonly
|
||||||
Duration time.Duration // See https://ffmpeg.org/ffmpeg.html#Main-options.
|
MapAudio string // See https://trac.ffmpeg.org/wiki/Map#Audiostreamsonly
|
||||||
MovFlags string
|
TimeOffset string // See https://trac.ffmpeg.org/wiki/Seeking and https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax
|
||||||
|
Duration time.Duration // See https://ffmpeg.org/ffmpeg.html#Main-options
|
||||||
|
MovFlags string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVideoOptions creates and returns new FFmpeg video transcoding options.
|
// NewVideoOptions creates and returns new FFmpeg video transcoding options.
|
||||||
func NewVideoOptions(ffmpegBin string, encoder Encoder, sizeLimit int, bitrateLimit, mapVideo, mapAudio string) Options {
|
func NewVideoOptions(ffmpegBin string, encoder Encoder, sizeLimit, quality int, preset, device, mapVideo, mapAudio string) Options {
|
||||||
if ffmpegBin == "" {
|
if ffmpegBin == "" {
|
||||||
ffmpegBin = FFmpegBin
|
ffmpegBin = FFmpegBin
|
||||||
}
|
}
|
||||||
@@ -34,26 +36,36 @@ func NewVideoOptions(ffmpegBin string, encoder Encoder, sizeLimit int, bitrateLi
|
|||||||
sizeLimit = 15360
|
sizeLimit = 15360
|
||||||
}
|
}
|
||||||
|
|
||||||
if bitrateLimit == "" {
|
if quality <= 0 {
|
||||||
bitrateLimit = "60M"
|
quality = DefaultQuality
|
||||||
|
} else if quality < WorstQuality {
|
||||||
|
quality = WorstQuality
|
||||||
|
} else if quality >= BestQuality {
|
||||||
|
quality = BestQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
if preset == "" {
|
||||||
|
preset = PresetFast
|
||||||
}
|
}
|
||||||
|
|
||||||
if mapVideo == "" {
|
if mapVideo == "" {
|
||||||
mapVideo = MapVideo
|
mapVideo = DefaultMapVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
if mapAudio == "" {
|
if mapAudio == "" {
|
||||||
mapAudio = MapAudio
|
mapAudio = DefaultMapAudio
|
||||||
}
|
}
|
||||||
|
|
||||||
return Options{
|
return Options{
|
||||||
Bin: ffmpegBin,
|
Bin: ffmpegBin,
|
||||||
Encoder: encoder,
|
Encoder: encoder,
|
||||||
SizeLimit: sizeLimit,
|
SizeLimit: sizeLimit,
|
||||||
BitrateLimit: bitrateLimit,
|
Quality: quality,
|
||||||
MapVideo: mapVideo,
|
Preset: preset,
|
||||||
MapAudio: mapAudio,
|
Device: device,
|
||||||
MovFlags: MovFlags,
|
MapVideo: mapVideo,
|
||||||
|
MapAudio: mapAudio,
|
||||||
|
MovFlags: MovFlags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,3 +88,28 @@ func (o *Options) VideoFilter(format PixelFormat) string {
|
|||||||
return fmt.Sprintf("scale='if(gte(iw,ih), min(%d, iw), -2):if(gte(iw,ih), -2, min(%d, ih))',format=%s", o.SizeLimit, o.SizeLimit, format)
|
return fmt.Sprintf("scale='if(gte(iw,ih), min(%d, iw), -2):if(gte(iw,ih), -2, min(%d, ih))',format=%s", o.SizeLimit, o.SizeLimit, format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QvQuality returns the video encoding quality as "-q:v" parameter string.
|
||||||
|
func (o *Options) QvQuality() string {
|
||||||
|
return QvQuality(o.Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalQuality returns the video encoding quality as "-global_quality" parameter string.
|
||||||
|
func (o *Options) GlobalQuality() string {
|
||||||
|
return GlobalQuality(o.Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrfQuality returns the video encoding quality as "-crf" parameter string.
|
||||||
|
func (o *Options) CrfQuality() string {
|
||||||
|
return CrfQuality(o.Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QpQuality returns the video encoding quality as "-qp" parameter string.
|
||||||
|
func (o *Options) QpQuality() string {
|
||||||
|
return QpQuality(o.Quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CqQuality returns the video encoding quality as "-cq" parameter string.
|
||||||
|
func (o *Options) CqQuality() string {
|
||||||
|
return CqQuality(o.Quality)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,29 +8,34 @@ import (
|
|||||||
|
|
||||||
func TestNewOptions(t *testing.T) {
|
func TestNewOptions(t *testing.T) {
|
||||||
t.Run("Defaults", func(t *testing.T) {
|
t.Run("Defaults", func(t *testing.T) {
|
||||||
opt := NewVideoOptions("", "", 0, "", "", "")
|
opt := NewVideoOptions("", "", 0, 50, "", "", "", "")
|
||||||
assert.Equal(t, "ffmpeg", opt.Bin)
|
assert.Equal(t, "ffmpeg", opt.Bin)
|
||||||
assert.Equal(t, FFmpegBin, opt.Bin)
|
assert.Equal(t, FFmpegBin, opt.Bin)
|
||||||
assert.Equal(t, DefaultAvcEncoder(), opt.Encoder)
|
assert.Equal(t, DefaultAvcEncoder(), opt.Encoder)
|
||||||
assert.Equal(t, 1920, opt.SizeLimit)
|
assert.Equal(t, 1920, opt.SizeLimit)
|
||||||
assert.Equal(t, "60M", opt.BitrateLimit)
|
assert.Equal(t, DefaultQuality, opt.Quality)
|
||||||
|
assert.Equal(t, "50", opt.QvQuality())
|
||||||
|
assert.Equal(t, "25", opt.GlobalQuality())
|
||||||
|
assert.Equal(t, "25", opt.CrfQuality())
|
||||||
|
assert.Equal(t, "25", opt.QpQuality())
|
||||||
|
assert.Equal(t, "25", opt.CqQuality())
|
||||||
|
assert.Equal(t, PresetFast, opt.Preset)
|
||||||
|
assert.Equal(t, "", opt.Device)
|
||||||
assert.Equal(t, "0:v:0", opt.MapVideo)
|
assert.Equal(t, "0:v:0", opt.MapVideo)
|
||||||
assert.Equal(t, "0:a:0?", opt.MapAudio)
|
assert.Equal(t, "0:a:0?", opt.MapAudio)
|
||||||
assert.Equal(t, MapVideo, opt.MapVideo)
|
assert.Equal(t, DefaultMapVideo, opt.MapVideo)
|
||||||
assert.Equal(t, MapAudio, opt.MapAudio)
|
assert.Equal(t, DefaultMapAudio, opt.MapAudio)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOptions_VideoFilter(t *testing.T) {
|
func TestOptions_VideoFilter(t *testing.T) {
|
||||||
opt := &Options{
|
opt := &Options{
|
||||||
Bin: "",
|
Bin: "",
|
||||||
Encoder: "intel",
|
Encoder: "intel",
|
||||||
SizeLimit: 1500,
|
SizeLimit: 1500,
|
||||||
BitrateLimit: "60M",
|
MapVideo: "",
|
||||||
MapVideo: "",
|
MapAudio: "",
|
||||||
MapAudio: "",
|
MovFlags: "",
|
||||||
MovFlags: "",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Empty", func(t *testing.T) {
|
t.Run("Empty", func(t *testing.T) {
|
||||||
|
|||||||
15
internal/ffmpeg/encode/preset.go
Normal file
15
internal/ffmpeg/encode/preset.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package encode
|
||||||
|
|
||||||
|
// FFmpeg encoding preset names from fastest to slowest,
|
||||||
|
// see https://trac.ffmpeg.org/wiki/Encode/H.264#Preset.
|
||||||
|
const (
|
||||||
|
PresetUltraFast = "ultrafast"
|
||||||
|
PresetSuperFast = "superfast"
|
||||||
|
PresetVeryFast = "veryfast"
|
||||||
|
PresetFaster = "faster"
|
||||||
|
PresetFast = "fast"
|
||||||
|
PresetMedium = "medium"
|
||||||
|
PresetSlow = "slow"
|
||||||
|
PresetSlower = "slower"
|
||||||
|
PresetVerySlow = "veryslow"
|
||||||
|
)
|
||||||
107
internal/ffmpeg/encode/quality.go
Normal file
107
internal/ffmpeg/encode/quality.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package encode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encoding quality min, max, and default settings,
|
||||||
|
// where 100 is almost lossless.
|
||||||
|
const (
|
||||||
|
BestQuality = 100
|
||||||
|
DefaultQuality = 50
|
||||||
|
WorstQuality = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// QvQuality returns the video encoding quality as "-q:v" parameter string.
|
||||||
|
func QvQuality(q int) string {
|
||||||
|
switch {
|
||||||
|
case q < 0:
|
||||||
|
return "50"
|
||||||
|
case q < 1:
|
||||||
|
return "1"
|
||||||
|
case q > 100:
|
||||||
|
return "100"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d", q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalQuality returns the video encoding quality as "-global_quality" parameter string.
|
||||||
|
func GlobalQuality(q int) string {
|
||||||
|
if q <= 0 {
|
||||||
|
q = DefaultQuality
|
||||||
|
} else if q > BestQuality {
|
||||||
|
q = BestQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
result := (100 - q) / 2
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case result < 1:
|
||||||
|
return "1"
|
||||||
|
case result > 50:
|
||||||
|
return "50"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrfQuality returns the video encoding quality as "-crf" parameter string.
|
||||||
|
func CrfQuality(q int) string {
|
||||||
|
if q <= 0 {
|
||||||
|
q = DefaultQuality
|
||||||
|
} else if q > BestQuality {
|
||||||
|
q = BestQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
result := (100 - q) / 2
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case result < 1:
|
||||||
|
return "0"
|
||||||
|
case result > 50:
|
||||||
|
return "51"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QpQuality returns the video encoding quality as "-qp" parameter string.
|
||||||
|
func QpQuality(q int) string {
|
||||||
|
if q <= 0 {
|
||||||
|
q = DefaultQuality
|
||||||
|
} else if q > BestQuality {
|
||||||
|
q = BestQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
result := (100 - q) / 2
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case result < 1:
|
||||||
|
return "0"
|
||||||
|
case result > 50:
|
||||||
|
return "51"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CqQuality returns the video encoding quality as "-cq" parameter string.
|
||||||
|
func CqQuality(q int) string {
|
||||||
|
if q <= 0 {
|
||||||
|
q = DefaultQuality
|
||||||
|
} else if q > BestQuality {
|
||||||
|
q = BestQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
result := (100 - q) / 2
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case result < 1:
|
||||||
|
return "1"
|
||||||
|
case result > 50:
|
||||||
|
return "50"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
internal/ffmpeg/encode/quality_test.go
Normal file
47
internal/ffmpeg/encode/quality_test.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package encode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConstantQuality(t *testing.T) {
|
||||||
|
t.Run("Defaults", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "100", QvQuality(BestQuality))
|
||||||
|
assert.Equal(t, "50", QvQuality(DefaultQuality))
|
||||||
|
assert.Equal(t, "1", QvQuality(WorstQuality))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalQuality(t *testing.T) {
|
||||||
|
t.Run("Defaults", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "1", GlobalQuality(BestQuality))
|
||||||
|
assert.Equal(t, "25", GlobalQuality(DefaultQuality))
|
||||||
|
assert.Equal(t, "49", GlobalQuality(WorstQuality))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCrfQuality(t *testing.T) {
|
||||||
|
t.Run("Defaults", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "0", CrfQuality(BestQuality))
|
||||||
|
assert.Equal(t, "25", CrfQuality(DefaultQuality))
|
||||||
|
assert.Equal(t, "49", CrfQuality(WorstQuality))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQpQuality(t *testing.T) {
|
||||||
|
t.Run("Defaults", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "0", QpQuality(BestQuality))
|
||||||
|
assert.Equal(t, "25", QpQuality(DefaultQuality))
|
||||||
|
assert.Equal(t, "49", QpQuality(WorstQuality))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCqQuality(t *testing.T) {
|
||||||
|
t.Run("Defaults", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "1", CqQuality(BestQuality))
|
||||||
|
assert.Equal(t, "25", CqQuality(DefaultQuality))
|
||||||
|
assert.Equal(t, "49", CqQuality(WorstQuality))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -26,10 +26,10 @@ func ExtractJpegImageCmd(videoName, imageName string, opt *encode.Options) *exec
|
|||||||
// see https://github.com/photoprism/photoprism/issues/4488.
|
// see https://github.com/photoprism/photoprism/issues/4488.
|
||||||
// Unfortunately, this filter would render thumbnails of non-HDR videos too dark:
|
// Unfortunately, this filter would render thumbnails of non-HDR videos too dark:
|
||||||
// "-vf", "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=gamma:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p",
|
// "-vf", "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=gamma:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p",
|
||||||
return exec.Command(opt.Bin, "-y", "-strict", "-2", "-ss", opt.TimeOffset, "-i", videoName, "-vframes", "1", imageName)
|
return exec.Command(opt.Bin, "-hide_banner", "-y", "-strict", "-2", "-ss", opt.TimeOffset, "-i", videoName, "-vframes", "1", imageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractPngImageCmd extracts a PNG still image from the specified source video file.
|
// ExtractPngImageCmd extracts a PNG still image from the specified source video file.
|
||||||
func ExtractPngImageCmd(videoName, imageName string, opt *encode.Options) *exec.Cmd {
|
func ExtractPngImageCmd(videoName, imageName string, opt *encode.Options) *exec.Cmd {
|
||||||
return exec.Command(opt.Bin, "-y", "-strict", "-2", "-ss", opt.TimeOffset, "-i", videoName, "-vframes", "1", imageName)
|
return exec.Command(opt.Bin, "-hide_banner", "-y", "-strict", "-2", "-ss", opt.TimeOffset, "-i", videoName, "-vframes", "1", imageName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestExtractImageCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
||||||
|
|
||||||
RunCommandTest(t, "jpg", srcName, destName, cmd, true)
|
RunCommandTest(t, "jpg", srcName, destName, cmd, true)
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ func TestExtractJpegImageCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
||||||
|
|
||||||
RunCommandTest(t, "jpeg", srcName, destName, cmd, true)
|
RunCommandTest(t, "jpeg", srcName, destName, cmd, true)
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func TestExtractPngImageCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
||||||
|
|
||||||
RunCommandTest(t, "png", srcName, destName, cmd, true)
|
RunCommandTest(t, "png", srcName, destName, cmd, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,46 @@ import (
|
|||||||
// TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC.
|
// TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC.
|
||||||
func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||||
// ffmpeg -hide_banner -h encoder=h264_qsv
|
// ffmpeg -hide_banner -h encoder=h264_qsv
|
||||||
return exec.Command(
|
if opt.Device != "" {
|
||||||
opt.Bin,
|
return exec.Command(
|
||||||
"-y",
|
opt.Bin,
|
||||||
"-strict", "-2",
|
"-hide_banner", "-y",
|
||||||
"-hwaccel", "qsv",
|
"-strict", "-2",
|
||||||
"-hwaccel_output_format", "qsv",
|
"-hwaccel", "qsv",
|
||||||
"-i", srcName,
|
"-hwaccel_device", opt.Device,
|
||||||
"-c:a", "aac",
|
"-hwaccel_output_format", "qsv",
|
||||||
"-vf", opt.VideoFilter(encode.FormatQSV),
|
"-i", srcName,
|
||||||
"-c:v", opt.Encoder.String(),
|
"-c:a", "aac",
|
||||||
"-map", opt.MapVideo,
|
"-vf", opt.VideoFilter(encode.FormatQSV),
|
||||||
"-map", opt.MapAudio,
|
"-c:v", opt.Encoder.String(),
|
||||||
"-r", "30",
|
"-map", opt.MapVideo,
|
||||||
"-b:v", opt.BitrateLimit,
|
"-map", opt.MapAudio,
|
||||||
"-bitrate", opt.BitrateLimit,
|
"-preset", opt.Preset,
|
||||||
"-f", "mp4",
|
"-r", "30",
|
||||||
"-movflags", opt.MovFlags,
|
"-global_quality", opt.GlobalQuality(),
|
||||||
destName,
|
"-f", "mp4",
|
||||||
)
|
"-movflags", opt.MovFlags,
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return exec.Command(
|
||||||
|
opt.Bin,
|
||||||
|
"-hide_banner", "-y",
|
||||||
|
"-strict", "-2",
|
||||||
|
"-hwaccel", "qsv",
|
||||||
|
"-hwaccel_output_format", "qsv",
|
||||||
|
"-i", srcName,
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", opt.VideoFilter(encode.FormatQSV),
|
||||||
|
"-c:v", opt.Encoder.String(),
|
||||||
|
"-map", opt.MapVideo,
|
||||||
|
"-map", opt.MapAudio,
|
||||||
|
"-preset", opt.Preset,
|
||||||
|
"-r", "30",
|
||||||
|
"-global_quality", opt.GlobalQuality(),
|
||||||
|
"-f", "mp4",
|
||||||
|
"-movflags", opt.MovFlags,
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
|||||||
// ffmpeg -hide_banner -h encoder=h264_nvenc
|
// ffmpeg -hide_banner -h encoder=h264_nvenc
|
||||||
return exec.Command(
|
return exec.Command(
|
||||||
opt.Bin,
|
opt.Bin,
|
||||||
"-y",
|
"-hide_banner", "-y",
|
||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
"-hwaccel", "auto",
|
"-hwaccel", "auto",
|
||||||
"-i", srcName,
|
"-i", srcName,
|
||||||
@@ -20,15 +20,14 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
|||||||
"-map", opt.MapVideo,
|
"-map", opt.MapVideo,
|
||||||
"-map", opt.MapAudio,
|
"-map", opt.MapAudio,
|
||||||
"-c:a", "aac",
|
"-c:a", "aac",
|
||||||
"-preset", "15",
|
"-preset", opt.Preset,
|
||||||
"-pixel_format", "yuv420p",
|
"-pixel_format", "yuv420p",
|
||||||
"-gpu", "any",
|
"-gpu", "any",
|
||||||
"-vf", opt.VideoFilter(encode.FormatYUV420P),
|
"-vf", opt.VideoFilter(encode.FormatYUV420P),
|
||||||
"-rc:v", "constqp",
|
"-rc:v", "constqp",
|
||||||
"-cq", "0",
|
"-cq", opt.CqQuality(),
|
||||||
"-tune", "2",
|
"-tune", "2",
|
||||||
"-r", "30",
|
"-r", "30",
|
||||||
"-b:v", opt.BitrateLimit,
|
|
||||||
"-profile:v", "1",
|
"-profile:v", "1",
|
||||||
"-level:v", "auto",
|
"-level:v", "auto",
|
||||||
"-coder:v", "1",
|
"-coder:v", "1",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TranscodeCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd,
|
|||||||
if fs.TypeAnimated[fs.FileType(srcName)] != "" {
|
if fs.TypeAnimated[fs.FileType(srcName)] != "" {
|
||||||
cmd = exec.Command(
|
cmd = exec.Command(
|
||||||
opt.Bin,
|
opt.Bin,
|
||||||
"-y",
|
"-hide_banner", "-y",
|
||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
"-i", srcName,
|
"-i", srcName,
|
||||||
"-pix_fmt", encode.FormatYUV420P.String(),
|
"-pix_fmt", encode.FormatYUV420P.String(),
|
||||||
|
|||||||
@@ -15,29 +15,29 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
ffmpegBin := "/usr/bin/ffmpeg"
|
ffmpegBin := "/usr/bin/ffmpeg"
|
||||||
|
|
||||||
t.Run("NoSource", func(t *testing.T) {
|
t.Run("NoSource", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
_, _, err := TranscodeCmd("", "", opt)
|
_, _, err := TranscodeCmd("", "", opt)
|
||||||
|
|
||||||
assert.Equal(t, "empty source filename", err.Error())
|
assert.Equal(t, "empty source filename", err.Error())
|
||||||
})
|
})
|
||||||
t.Run("NoDestination", func(t *testing.T) {
|
t.Run("NoDestination", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
_, _, err := TranscodeCmd("VID123.mov", "", opt)
|
_, _, err := TranscodeCmd("VID123.mov", "", opt)
|
||||||
|
|
||||||
assert.Equal(t, "empty destination filename", err.Error())
|
assert.Equal(t, "empty destination filename", err.Error())
|
||||||
})
|
})
|
||||||
t.Run("Animation", func(t *testing.T) {
|
t.Run("Animation", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
r, _, err := TranscodeCmd("VID123.gif", "VID123.gif.avc", opt)
|
r, _, err := TranscodeCmd("VID123.gif", "VID123.gif.avc", opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -i VID123.gif -pix_fmt yuv420p -vf scale='trunc(iw/2)*2:trunc(ih/2)*2' -f mp4 -movflags +faststart VID123.gif.avc")
|
assert.Contains(t, r.String(), "bin/ffmpeg -hide_banner -y -strict -2 -i VID123.gif -pix_fmt yuv420p -vf scale='trunc(iw/2)*2:trunc(ih/2)*2' -f mp4 -movflags +faststart VID123.gif.avc")
|
||||||
})
|
})
|
||||||
t.Run("VP9toAVC", func(t *testing.T) {
|
t.Run("VP9toAVC", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions(ffmpegBin, encode.SoftwareAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions(ffmpegBin, encode.SoftwareAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
|
|
||||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||||
destName := fs.Abs("./testdata/25fps.avc")
|
destName := fs.Abs("./testdata/25fps.avc")
|
||||||
@@ -52,13 +52,13 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -i SRC -c:v libx264 -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -max_muxing_queue_size 1024 -crf 23 -r 30 -b:v 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -i SRC -c:v libx264 -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -max_muxing_queue_size 1024 -r 30 -crf 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||||
|
|
||||||
// Run generated command to test software transcoding.
|
// Run generated command to test software transcoding.
|
||||||
RunCommandTest(t, opt.Encoder, srcName, destName, cmd, true)
|
RunCommandTest(t, opt.Encoder, srcName, destName, cmd, true)
|
||||||
})
|
})
|
||||||
t.Run("Vaapi", func(t *testing.T) {
|
t.Run("Vaapi", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions(ffmpegBin, encode.VaapiAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions(ffmpegBin, encode.VaapiAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
|
|
||||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||||
destName := fs.Abs("./testdata/25fps.vaapi.avc")
|
destName := fs.Abs("./testdata/25fps.vaapi.avc")
|
||||||
@@ -73,7 +73,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel vaapi -i SRC -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=nv12,hwupload -c:v h264_vaapi -map 0:v:0 -map 0:a:0? -r 30 -b:v 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel vaapi -i SRC -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=nv12,hwupload -c:v h264_vaapi -map 0:v:0 -map 0:a:0? -r 30 -qp 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||||
|
|
||||||
// This transcoding test requires a supported hardware device that is properly configured:
|
// This transcoding test requires a supported hardware device that is properly configured:
|
||||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "vaapi" {
|
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "vaapi" {
|
||||||
@@ -81,7 +81,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("IntelHvc", func(t *testing.T) {
|
t.Run("IntelHvc", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions(ffmpegBin, encode.IntelAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions(ffmpegBin, encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "/dev/dri/renderD128", "", "")
|
||||||
|
|
||||||
// QuickTime MOV container with HVC1 (HEVC) codec.
|
// QuickTime MOV container with HVC1 (HEVC) codec.
|
||||||
srcName := fs.Abs("./testdata/30fps.mov")
|
srcName := fs.Abs("./testdata/30fps.mov")
|
||||||
@@ -97,7 +97,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel qsv -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -r 30 -b:v 60M -bitrate 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -preset fast -r 30 -global_quality 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||||
|
|
||||||
// This transcoding test requires a supported hardware device that is properly configured:
|
// This transcoding test requires a supported hardware device that is properly configured:
|
||||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" {
|
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" {
|
||||||
@@ -105,7 +105,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("IntelVp9", func(t *testing.T) {
|
t.Run("IntelVp9", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions(ffmpegBin, encode.IntelAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions(ffmpegBin, encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "/dev/dri/renderD128", "", "")
|
||||||
|
|
||||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||||
destName := fs.Abs("./testdata/25fps.intel.avc")
|
destName := fs.Abs("./testdata/25fps.intel.avc")
|
||||||
@@ -120,7 +120,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel qsv -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -r 30 -b:v 60M -bitrate 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -preset fast -r 30 -global_quality 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||||
|
|
||||||
// This transcoding test requires a supported hardware device that is properly configured:
|
// This transcoding test requires a supported hardware device that is properly configured:
|
||||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" {
|
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" {
|
||||||
@@ -128,7 +128,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("NvidiaHvc", func(t *testing.T) {
|
t.Run("NvidiaHvc", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions(ffmpegBin, encode.NvidiaAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions(ffmpegBin, encode.NvidiaAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
|
|
||||||
// QuickTime MOV container with HVC1 (HEVC) codec.
|
// QuickTime MOV container with HVC1 (HEVC) codec.
|
||||||
srcName := fs.Abs("./testdata/30fps.mov")
|
srcName := fs.Abs("./testdata/30fps.mov")
|
||||||
@@ -144,7 +144,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset 15 -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 0 -tune 2 -r 30 -b:v 60M -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 25 -tune 2 -r 30 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||||
|
|
||||||
// This transcoding test requires a supported hardware device that is properly configured:
|
// This transcoding test requires a supported hardware device that is properly configured:
|
||||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" {
|
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" {
|
||||||
@@ -152,7 +152,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("NvidiaVp9", func(t *testing.T) {
|
t.Run("NvidiaVp9", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions(ffmpegBin, encode.NvidiaAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions(ffmpegBin, encode.NvidiaAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
|
|
||||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||||
destName := fs.Abs("./testdata/25fps.nvidia.avc")
|
destName := fs.Abs("./testdata/25fps.nvidia.avc")
|
||||||
@@ -167,7 +167,7 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||||
|
|
||||||
assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset 15 -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 0 -tune 2 -r 30 -b:v 60M -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 25 -tune 2 -r 30 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||||
|
|
||||||
// This transcoding test requires a supported hardware device that is properly configured:
|
// This transcoding test requires a supported hardware device that is properly configured:
|
||||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" {
|
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" {
|
||||||
@@ -175,23 +175,23 @@ func TestTranscodeCmd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("Apple", func(t *testing.T) {
|
t.Run("Apple", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions("", encode.AppleAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions("", encode.AppleAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)
|
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -i VID123.mov -c:v h264_videotoolbox -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -profile high -level 51 -r 30 -b:v 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc")
|
assert.Contains(t, r.String(), "ffmpeg -hide_banner -y -strict -2 -i VID123.mov -c:v h264_videotoolbox -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -profile high -level 51 -r 30 -q:v 50 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc")
|
||||||
})
|
})
|
||||||
t.Run("Video4Linux", func(t *testing.T) {
|
t.Run("Video4Linux", func(t *testing.T) {
|
||||||
opt := encode.NewVideoOptions("", encode.V4LAvc, 1500, "60M", "", "")
|
opt := encode.NewVideoOptions("", encode.V4LAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||||
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)
|
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -i VID123.mov -c:v h264_v4l2m2m -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -num_output_buffers 72 -num_capture_buffers 64 -max_muxing_queue_size 1024 -crf 23 -r 30 -b:v 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc")
|
assert.Contains(t, r.String(), "ffmpeg -hide_banner -y -strict -2 -i VID123.mov -c:v h264_v4l2m2m -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -num_output_buffers 72 -num_capture_buffers 64 -max_muxing_queue_size 1024 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
|||||||
// ffmpeg -hide_banner -h encoder=h264_v4l2m2m
|
// ffmpeg -hide_banner -h encoder=h264_v4l2m2m
|
||||||
return exec.Command(
|
return exec.Command(
|
||||||
opt.Bin,
|
opt.Bin,
|
||||||
"-y",
|
"-hide_banner", "-y",
|
||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
"-i", srcName,
|
"-i", srcName,
|
||||||
"-c:v", opt.Encoder.String(),
|
"-c:v", opt.Encoder.String(),
|
||||||
@@ -22,9 +22,6 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
|||||||
"-num_output_buffers", "72",
|
"-num_output_buffers", "72",
|
||||||
"-num_capture_buffers", "64",
|
"-num_capture_buffers", "64",
|
||||||
"-max_muxing_queue_size", "1024",
|
"-max_muxing_queue_size", "1024",
|
||||||
"-crf", "23",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", opt.BitrateLimit,
|
|
||||||
"-f", "mp4",
|
"-f", "mp4",
|
||||||
"-movflags", opt.MovFlags,
|
"-movflags", opt.MovFlags,
|
||||||
destName,
|
destName,
|
||||||
|
|||||||
@@ -8,21 +8,42 @@ import (
|
|||||||
|
|
||||||
// TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC.
|
// TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC.
|
||||||
func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||||
return exec.Command(
|
if opt.Device != "" {
|
||||||
opt.Bin,
|
return exec.Command(
|
||||||
"-y",
|
opt.Bin,
|
||||||
"-strict", "-2",
|
"-hide_banner", "-y",
|
||||||
"-hwaccel", "vaapi",
|
"-strict", "-2",
|
||||||
"-i", srcName,
|
"-hwaccel", "vaapi",
|
||||||
"-c:a", "aac",
|
"-hwaccel_device", opt.Device,
|
||||||
"-vf", opt.VideoFilter(encode.FormatNV12),
|
"-i", srcName,
|
||||||
"-c:v", opt.Encoder.String(),
|
"-c:a", "aac",
|
||||||
"-map", opt.MapVideo,
|
"-vf", opt.VideoFilter(encode.FormatNV12),
|
||||||
"-map", opt.MapAudio,
|
"-c:v", opt.Encoder.String(),
|
||||||
"-r", "30",
|
"-map", opt.MapVideo,
|
||||||
"-b:v", opt.BitrateLimit,
|
"-map", opt.MapAudio,
|
||||||
"-f", "mp4",
|
"-r", "30",
|
||||||
"-movflags", opt.MovFlags,
|
"-qp", opt.QpQuality(),
|
||||||
destName,
|
"-f", "mp4",
|
||||||
)
|
"-movflags", opt.MovFlags,
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return exec.Command(
|
||||||
|
opt.Bin,
|
||||||
|
"-hide_banner", "-y",
|
||||||
|
"-strict", "-2",
|
||||||
|
"-hwaccel", "vaapi",
|
||||||
|
"-i", srcName,
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", opt.VideoFilter(encode.FormatNV12),
|
||||||
|
"-c:v", opt.Encoder.String(),
|
||||||
|
"-map", opt.MapVideo,
|
||||||
|
"-map", opt.MapAudio,
|
||||||
|
"-r", "30",
|
||||||
|
"-qp", opt.QpQuality(),
|
||||||
|
"-f", "mp4",
|
||||||
|
"-movflags", opt.MovFlags,
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
scripts/dist/install-gpu.sh
vendored
1
scripts/dist/install-gpu.sh
vendored
@@ -62,7 +62,6 @@ for t in ${GPU_DETECTED[@]}; do
|
|||||||
echo "Installing AMD VA-API GPU Drivers..."
|
echo "Installing AMD VA-API GPU Drivers..."
|
||||||
apt-get -qq install mesa-va-drivers vainfo libva2
|
apt-get -qq install mesa-va-drivers vainfo libva2
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|
||||||
"null")
|
"null")
|
||||||
# ignore
|
# ignore
|
||||||
|
|||||||
Reference in New Issue
Block a user