Config: Add preset, quality, and device options for FFmpeg #4969

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-04-27 14:36:36 +02:00
parent 27ec7a1282
commit eb4fc032ff
23 changed files with 490 additions and 143 deletions

View File

@@ -5,6 +5,8 @@ import (
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
"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.
@@ -33,13 +35,31 @@ func (c *Config) FFmpegSize() int {
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 {
switch {
case c.options.FFmpegBitrate <= 0:
return 60
case c.options.FFmpegBitrate >= 960:
return 960
return encode.DefaultBitrateLimit
case c.options.FFmpegBitrate < encode.MinBitrateLimit:
return encode.MinBitrateLimit
case c.options.FFmpegBitrate >= encode.MaxBitrateLimit:
return encode.MaxBitrateLimit
default:
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.
func (c *Config) FFmpegMapVideo() string {
if c.options.FFmpegMapVideo == "" {
return encode.MapVideo
return encode.DefaultMapVideo
}
return c.options.FFmpegMapVideo
@@ -68,7 +111,7 @@ func (c *Config) FFmpegMapVideo() string {
// FFmpegMapAudio returns the audio streams to be transcoded as string.
func (c *Config) FFmpegMapAudio() string {
if c.options.FFmpegMapAudio == "" {
return encode.MapAudio
return encode.DefaultMapAudio
}
return c.options.FFmpegMapAudio
@@ -77,7 +120,7 @@ func (c *Config) FFmpegMapAudio() string {
// FFmpegOptions returns the FFmpeg options to use for video transcoding.
func (c *Config) FFmpegOptions(encoder encode.Encoder, bitrate string) (encode.Options, error) {
// 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.
if opt.Bin == "" {

View File

@@ -32,13 +32,16 @@ func TestConfig_FFmpegEnabled(t *testing.T) {
func TestConfig_FFmpegBitrate(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 60, c.FFmpegBitrate())
assert.Equal(t, encode.DefaultBitrateLimit, c.FFmpegBitrate())
c.options.FFmpegBitrate = 1000
assert.Equal(t, 960, c.FFmpegBitrate())
assert.Equal(t, encode.MaxBitrateLimit, c.FFmpegBitrate())
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
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())
}
func TestConfig_FFmpegQuality(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, encode.DefaultQuality, c.FFmpegQuality())
}
func TestConfig_FFmpegBitrateExceeded(t *testing.T) {
c := NewConfig(CliTestContext())
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(1.0))
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
assert.False(t, c.FFmpegBitrateExceeded(0.95))
assert.False(t, c.FFmpegBitrateExceeded(1.05))
@@ -91,14 +99,28 @@ func TestConfig_FFmpegBitrateExceeded(t *testing.T) {
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) {
c := NewConfig(CliTestContext())
assert.Equal(t, encode.MapVideo, c.FFmpegMapVideo())
assert.Equal(t, encode.DefaultMapVideo, c.FFmpegMapVideo())
}
func TestConfig_FFmpegMapAudio(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, encode.MapAudio, c.FFmpegMapAudio())
assert.Equal(t, encode.DefaultMapAudio, c.FFmpegMapAudio())
}
func TestConfig_FFmpegOptions(t *testing.T) {
@@ -108,9 +130,8 @@ func TestConfig_FFmpegOptions(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, c.FFmpegBin(), opt.Bin)
assert.Equal(t, encode.SoftwareAvc, opt.Encoder)
assert.Equal(t, bitrate, opt.BitrateLimit)
assert.Equal(t, encode.MapVideo, opt.MapVideo)
assert.Equal(t, encode.MapAudio, opt.MapAudio)
assert.Equal(t, encode.DefaultMapVideo, opt.MapVideo)
assert.Equal(t, encode.DefaultMapAudio, opt.MapAudio)
assert.Equal(t, c.FFmpegMapVideo(), opt.MapVideo)
assert.Equal(t, c.FFmpegMapAudio(), opt.MapAudio)
}

View File

@@ -794,36 +794,51 @@ var Flags = CliFlags{
Flag: &cli.StringFlag{
Name: "ffmpeg-encoder",
Aliases: []string{"vc"},
Usage: "FFmpeg AVC encoder `NAME`",
Usage: "FFmpeg AVC video encoder `NAME`",
Value: "libx264",
EnvVars: EnvVars("FFMPEG_ENCODER"),
}}, {
Flag: &cli.IntFlag{
Name: "ffmpeg-size",
Aliases: []string{"vs"},
Usage: "maximum video size in `PIXELS` (720-7680)",
Usage: "encoding resolution limit in `PIXELS` (720-7680)",
Value: thumb.Sizes[thumb.Fit4096].Width,
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{
Name: "ffmpeg-bitrate",
Aliases: []string{"vb"},
Usage: "maximum video `BITRATE` in Mbps",
Value: 60,
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),
Value: encode.DefaultBitrateLimit,
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{
Name: "ffmpeg-map-video",
Usage: "video `STREAMS` that should be transcoded",
Value: encode.MapVideo,
Usage: "transcoding video stream `MAP`",
Value: encode.DefaultMapVideo,
EnvVars: EnvVars("FFMPEG_MAP_VIDEO"),
}, DocDefault: fmt.Sprintf("`%s`", encode.MapVideo)}, {
}, DocDefault: fmt.Sprintf("`%s`", encode.DefaultMapVideo)}, {
Flag: &cli.StringFlag{
Name: "ffmpeg-map-audio",
Usage: "audio `STREAMS` that should be transcoded",
Value: encode.MapAudio,
Usage: "transcoding audio stream `MAP`",
Value: encode.DefaultMapAudio,
EnvVars: EnvVars("FFMPEG_MAP_AUDIO"),
}, DocDefault: fmt.Sprintf("`%s`", encode.MapAudio)}, {
}, DocDefault: fmt.Sprintf("`%s`", encode.DefaultMapAudio)}, {
Flag: &cli.StringFlag{
Name: "exiftool-bin",
Usage: "ExifTool `COMMAND` for extracting metadata",

View File

@@ -167,7 +167,10 @@ type Options struct {
FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"`
FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"`
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"`
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"`
FFmpegMapAudio string `yaml:"FFmpegMapAudio" json:"FFmpegMapAudio" flag:"ffmpeg-map-audio"`
ExifToolBin string `yaml:"ExifToolBin" json:"-" flag:"exiftool-bin"`

View File

@@ -211,7 +211,10 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"ffmpeg-bin", c.FFmpegBin()},
{"ffmpeg-encoder", c.FFmpegEncoder().String()},
{"ffmpeg-size", fmt.Sprintf("%d", c.FFmpegSize())},
{"ffmpeg-quality", fmt.Sprintf("%d", c.FFmpegQuality())},
{"ffmpeg-bitrate", fmt.Sprintf("%d", c.FFmpegBitrate())},
{"ffmpeg-preset", c.FFmpegPreset()},
{"ffmpeg-device", c.FFmpegDevice()},
{"ffmpeg-map-video", c.FFmpegMapVideo()},
{"ffmpeg-map-audio", c.FFmpegMapAudio()},
{"exiftool-bin", c.ExifToolBin()},

View File

@@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
// ffmpeg -hide_banner -h encoder=h264_videotoolbox
return exec.Command(
opt.Bin,
"-y",
"-hide_banner", "-y",
"-strict", "-2",
"-i", srcName,
"-c:v", opt.Encoder.String(),
@@ -22,7 +22,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
"-profile", "high",
"-level", "51",
"-r", "30",
"-b:v", opt.BitrateLimit,
"-q:v", opt.QvQuality(),
"-f", "mp4",
"-movflags", opt.MovFlags,
destName,

View File

@@ -6,18 +6,18 @@ import "os/exec"
func TranscodeToAvcCmd(srcName, destName string, opt Options) *exec.Cmd {
return exec.Command(
opt.Bin,
"-y",
"-hide_banner", "-y",
"-strict", "-2",
"-i", srcName,
"-c:v", opt.Encoder.String(),
"-map", opt.MapVideo,
"-map", opt.MapAudio,
"-c:a", "aac",
"-preset", opt.Preset,
"-vf", opt.VideoFilter(FormatYUV420P),
"-max_muxing_queue_size", "1024",
"-crf", "23",
"-r", "30",
"-b:v", opt.BitrateLimit,
"-crf", opt.CrfQuality(),
"-f", "mp4",
"-movflags", opt.MovFlags,
destName,

View 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?"
)

View File

@@ -7,9 +7,3 @@ package encode
// - 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
var MovFlags = "frag_keyframe+empty_moov+default_base_moof+faststart"
const (
FFmpegBin = "ffmpeg"
MapVideo = "0:v:0"
MapAudio = "0:a:0?"
)

View File

@@ -7,19 +7,21 @@ import (
// Options represents FFmpeg encoding options.
type Options struct {
Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg.
Encoder Encoder // Supported FFmpeg output Encoder.
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.
MapVideo string // See https://trac.ffmpeg.org/wiki/Map#Videostreamsonly.
MapAudio string // See https://trac.ffmpeg.org/wiki/Map#Audiostreamsonly.
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
Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg
Encoder Encoder // Supported FFmpeg output Encoder
SizeLimit int // Maximum width and height of the output video file in pixels.
Quality int // See https://ffmpeg.org/ffmpeg-codecs.html
Preset string // See https://trac.ffmpeg.org/wiki/Encode/H.264#Preset
Device string // See https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
MapVideo string // See https://trac.ffmpeg.org/wiki/Map#Videostreamsonly
MapAudio string // See https://trac.ffmpeg.org/wiki/Map#Audiostreamsonly
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.
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 == "" {
ffmpegBin = FFmpegBin
}
@@ -34,26 +36,36 @@ func NewVideoOptions(ffmpegBin string, encoder Encoder, sizeLimit int, bitrateLi
sizeLimit = 15360
}
if bitrateLimit == "" {
bitrateLimit = "60M"
if quality <= 0 {
quality = DefaultQuality
} else if quality < WorstQuality {
quality = WorstQuality
} else if quality >= BestQuality {
quality = BestQuality
}
if preset == "" {
preset = PresetFast
}
if mapVideo == "" {
mapVideo = MapVideo
mapVideo = DefaultMapVideo
}
if mapAudio == "" {
mapAudio = MapAudio
mapAudio = DefaultMapAudio
}
return Options{
Bin: ffmpegBin,
Encoder: encoder,
SizeLimit: sizeLimit,
BitrateLimit: bitrateLimit,
MapVideo: mapVideo,
MapAudio: mapAudio,
MovFlags: MovFlags,
Bin: ffmpegBin,
Encoder: encoder,
SizeLimit: sizeLimit,
Quality: quality,
Preset: preset,
Device: device,
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)
}
}
// 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)
}

View File

@@ -8,29 +8,34 @@ import (
func TestNewOptions(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, FFmpegBin, opt.Bin)
assert.Equal(t, DefaultAvcEncoder(), opt.Encoder)
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:a:0?", opt.MapAudio)
assert.Equal(t, MapVideo, opt.MapVideo)
assert.Equal(t, MapAudio, opt.MapAudio)
assert.Equal(t, DefaultMapVideo, opt.MapVideo)
assert.Equal(t, DefaultMapAudio, opt.MapAudio)
})
}
func TestOptions_VideoFilter(t *testing.T) {
opt := &Options{
Bin: "",
Encoder: "intel",
SizeLimit: 1500,
BitrateLimit: "60M",
MapVideo: "",
MapAudio: "",
MovFlags: "",
Bin: "",
Encoder: "intel",
SizeLimit: 1500,
MapVideo: "",
MapAudio: "",
MovFlags: "",
}
t.Run("Empty", func(t *testing.T) {

View 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"
)

View 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)
}
}

View 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))
})
}

View File

@@ -26,10 +26,10 @@ func ExtractJpegImageCmd(videoName, imageName string, opt *encode.Options) *exec
// see https://github.com/photoprism/photoprism/issues/4488.
// 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",
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.
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)
}

View File

@@ -23,7 +23,7 @@ func TestExtractImageCmd(t *testing.T) {
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 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)
}
@@ -40,7 +40,7 @@ func TestExtractJpegImageCmd(t *testing.T) {
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 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)
}
@@ -57,7 +57,7 @@ func TestExtractPngImageCmd(t *testing.T) {
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 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)
}

View File

@@ -9,23 +9,46 @@ import (
// TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC.
func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
// ffmpeg -hide_banner -h encoder=h264_qsv
return exec.Command(
opt.Bin,
"-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,
"-r", "30",
"-b:v", opt.BitrateLimit,
"-bitrate", opt.BitrateLimit,
"-f", "mp4",
"-movflags", opt.MovFlags,
destName,
)
if opt.Device != "" {
return exec.Command(
opt.Bin,
"-hide_banner", "-y",
"-strict", "-2",
"-hwaccel", "qsv",
"-hwaccel_device", opt.Device,
"-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,
)
} 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,
)
}
}

View File

@@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
// ffmpeg -hide_banner -h encoder=h264_nvenc
return exec.Command(
opt.Bin,
"-y",
"-hide_banner", "-y",
"-strict", "-2",
"-hwaccel", "auto",
"-i", srcName,
@@ -20,15 +20,14 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
"-map", opt.MapVideo,
"-map", opt.MapAudio,
"-c:a", "aac",
"-preset", "15",
"-preset", opt.Preset,
"-pixel_format", "yuv420p",
"-gpu", "any",
"-vf", opt.VideoFilter(encode.FormatYUV420P),
"-rc:v", "constqp",
"-cq", "0",
"-cq", opt.CqQuality(),
"-tune", "2",
"-r", "30",
"-b:v", opt.BitrateLimit,
"-profile:v", "1",
"-level:v", "auto",
"-coder:v", "1",

View File

@@ -33,7 +33,7 @@ func TranscodeCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd,
if fs.TypeAnimated[fs.FileType(srcName)] != "" {
cmd = exec.Command(
opt.Bin,
"-y",
"-hide_banner", "-y",
"-strict", "-2",
"-i", srcName,
"-pix_fmt", encode.FormatYUV420P.String(),

View File

@@ -15,29 +15,29 @@ func TestTranscodeCmd(t *testing.T) {
ffmpegBin := "/usr/bin/ffmpeg"
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)
assert.Equal(t, "empty source filename", err.Error())
})
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)
assert.Equal(t, "empty destination filename", err.Error())
})
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)
if err != nil {
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) {
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")
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, 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.
RunCommandTest(t, opt.Encoder, srcName, destName, cmd, true)
})
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")
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, 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:
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "vaapi" {
@@ -81,7 +81,7 @@ func TestTranscodeCmd(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.
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, 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:
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" {
@@ -105,7 +105,7 @@ func TestTranscodeCmd(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")
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, 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:
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" {
@@ -128,7 +128,7 @@ func TestTranscodeCmd(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.
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, 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:
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" {
@@ -152,7 +152,7 @@ func TestTranscodeCmd(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")
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, 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:
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" {
@@ -175,23 +175,23 @@ func TestTranscodeCmd(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)
if err != nil {
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) {
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)
if err != nil {
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")
})
}

View File

@@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
// ffmpeg -hide_banner -h encoder=h264_v4l2m2m
return exec.Command(
opt.Bin,
"-y",
"-hide_banner", "-y",
"-strict", "-2",
"-i", srcName,
"-c:v", opt.Encoder.String(),
@@ -22,9 +22,6 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
"-num_output_buffers", "72",
"-num_capture_buffers", "64",
"-max_muxing_queue_size", "1024",
"-crf", "23",
"-r", "30",
"-b:v", opt.BitrateLimit,
"-f", "mp4",
"-movflags", opt.MovFlags,
destName,

View File

@@ -8,21 +8,42 @@ import (
// TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC.
func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
return exec.Command(
opt.Bin,
"-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",
"-b:v", opt.BitrateLimit,
"-f", "mp4",
"-movflags", opt.MovFlags,
destName,
)
if opt.Device != "" {
return exec.Command(
opt.Bin,
"-hide_banner", "-y",
"-strict", "-2",
"-hwaccel", "vaapi",
"-hwaccel_device", opt.Device,
"-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,
)
} 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,
)
}
}

View File

@@ -62,7 +62,6 @@ for t in ${GPU_DETECTED[@]}; do
echo "Installing AMD VA-API GPU Drivers..."
apt-get -qq install mesa-va-drivers vainfo libva2
;;
"null")
# ignore