mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
FFmpeg: Add tests, refactor package, and split into sub-packages #4604
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,12 +19,12 @@ func (c *Config) FFmpegEnabled() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FFmpegEncoder returns the FFmpeg AVC encoder name.
|
// FFmpegEncoder returns the FFmpeg AVC encoder name.
|
||||||
func (c *Config) FFmpegEncoder() ffmpeg.AvcEncoder {
|
func (c *Config) FFmpegEncoder() encode.Encoder {
|
||||||
if c.options.FFmpegEncoder == "" || c.options.FFmpegEncoder == ffmpeg.SoftwareEncoder.String() {
|
if c.options.FFmpegEncoder == "" || c.options.FFmpegEncoder == encode.SoftwareAvc.String() {
|
||||||
return ffmpeg.SoftwareEncoder
|
return encode.SoftwareAvc
|
||||||
}
|
}
|
||||||
|
|
||||||
return ffmpeg.FindEncoder(c.options.FFmpegEncoder)
|
return encode.FindEncoder(c.options.FFmpegEncoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FFmpegSize returns the maximum ffmpeg video encoding size in pixels (720-7680).
|
// FFmpegSize returns the maximum ffmpeg video encoding size in pixels (720-7680).
|
||||||
@@ -73,9 +74,9 @@ func (c *Config) FFmpegMapAudio() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FFmpegOptions returns the FFmpeg transcoding options.
|
// FFmpegOptions returns the FFmpeg transcoding options.
|
||||||
func (c *Config) FFmpegOptions(encoder ffmpeg.AvcEncoder, bitrate string) (ffmpeg.Options, error) {
|
func (c *Config) FFmpegOptions(encoder encode.Encoder, bitrate string) (encode.Options, error) {
|
||||||
// Transcode all other formats with FFmpeg.
|
// Transcode all other formats with FFmpeg.
|
||||||
opt := ffmpeg.Options{
|
opt := encode.Options{
|
||||||
Bin: c.FFmpegBin(),
|
Bin: c.FFmpegBin(),
|
||||||
Encoder: encoder,
|
Encoder: encoder,
|
||||||
Size: c.FFmpegSize(),
|
Size: c.FFmpegSize(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -11,15 +12,15 @@ import (
|
|||||||
|
|
||||||
func TestConfig_FFmpegEncoder(t *testing.T) {
|
func TestConfig_FFmpegEncoder(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder())
|
assert.Equal(t, encode.SoftwareAvc, c.FFmpegEncoder())
|
||||||
c.options.FFmpegEncoder = "nvidia"
|
c.options.FFmpegEncoder = "nvidia"
|
||||||
assert.Equal(t, ffmpeg.NvidiaEncoder, c.FFmpegEncoder())
|
assert.Equal(t, encode.NvidiaAvc, c.FFmpegEncoder())
|
||||||
c.options.FFmpegEncoder = "intel"
|
c.options.FFmpegEncoder = "intel"
|
||||||
assert.Equal(t, ffmpeg.IntelEncoder, c.FFmpegEncoder())
|
assert.Equal(t, encode.IntelAvc, c.FFmpegEncoder())
|
||||||
c.options.FFmpegEncoder = "xxx"
|
c.options.FFmpegEncoder = "xxx"
|
||||||
assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder())
|
assert.Equal(t, encode.SoftwareAvc, c.FFmpegEncoder())
|
||||||
c.options.FFmpegEncoder = ""
|
c.options.FFmpegEncoder = ""
|
||||||
assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder())
|
assert.Equal(t, encode.SoftwareAvc, c.FFmpegEncoder())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_FFmpegEnabled(t *testing.T) {
|
func TestConfig_FFmpegEnabled(t *testing.T) {
|
||||||
@@ -104,10 +105,10 @@ func TestConfig_FFmpegMapAudio(t *testing.T) {
|
|||||||
func TestConfig_FFmpegOptions(t *testing.T) {
|
func TestConfig_FFmpegOptions(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
bitrate := "25M"
|
bitrate := "25M"
|
||||||
opt, err := c.FFmpegOptions(ffmpeg.SoftwareEncoder, bitrate)
|
opt, err := c.FFmpegOptions(encode.SoftwareAvc, bitrate)
|
||||||
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, ffmpeg.SoftwareEncoder, opt.Encoder)
|
assert.Equal(t, encode.SoftwareAvc, opt.Encoder)
|
||||||
assert.Equal(t, bitrate, opt.Bitrate)
|
assert.Equal(t, bitrate, opt.Bitrate)
|
||||||
assert.Equal(t, ffmpeg.MapVideoDefault, opt.MapVideo)
|
assert.Equal(t, ffmpeg.MapVideoDefault, opt.MapVideo)
|
||||||
assert.Equal(t, ffmpeg.MapAudioDefault, opt.MapAudio)
|
assert.Equal(t, ffmpeg.MapAudioDefault, opt.MapAudio)
|
||||||
|
|||||||
30
internal/ffmpeg/apple/avc.go
Normal file
30
internal/ffmpeg/apple/avc.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package apple
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC.
|
||||||
|
func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||||
|
// ffmpeg -hide_banner -h encoder=h264_videotoolbox
|
||||||
|
return exec.Command(
|
||||||
|
opt.Bin,
|
||||||
|
"-y",
|
||||||
|
"-strict", "-2",
|
||||||
|
"-i", srcName,
|
||||||
|
"-c:v", opt.Encoder.String(),
|
||||||
|
"-map", opt.MapVideo,
|
||||||
|
"-map", opt.MapAudio,
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", opt.VideoFilter(encode.FormatYUV420P),
|
||||||
|
"-profile", "high",
|
||||||
|
"-level", "51",
|
||||||
|
"-r", "30",
|
||||||
|
"-b:v", opt.Bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-movflags", "+faststart",
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,187 +4,72 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/apple"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/intel"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/nvidia"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/v4l"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/vaapi"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AvcConvertCommand returns the command for converting video files to MPEG-4 AVC.
|
// AvcConvertCmd returns the command for converting video files to MPEG-4 AVC.
|
||||||
func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, useMutex bool, err error) {
|
func AvcConvertCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd, useMutex bool, err error) {
|
||||||
if fileName == "" {
|
if srcName == "" {
|
||||||
return nil, false, fmt.Errorf("empty input filename")
|
return nil, false, fmt.Errorf("empty source filename")
|
||||||
} else if avcName == "" {
|
} else if destName == "" {
|
||||||
return nil, false, fmt.Errorf("empty output filename")
|
return nil, false, fmt.Errorf("empty destination filename")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't transcode more than one video at the same time.
|
// Don't transcode more than one video at the same time.
|
||||||
useMutex = true
|
useMutex = true
|
||||||
|
|
||||||
// Get configured ffmpeg command name.
|
// Use default ffmpeg command name.
|
||||||
ffmpeg := opt.Bin
|
if opt.Bin == "" {
|
||||||
|
opt.Bin = DefaultBin
|
||||||
// Use default ffmpeg command name?
|
|
||||||
if ffmpeg == "" {
|
|
||||||
ffmpeg = DefaultBin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't use hardware transcoding for animated images.
|
// Don't use hardware transcoding for animated images.
|
||||||
if fs.TypeAnimated[fs.FileType(fileName)] != "" {
|
if fs.TypeAnimated[fs.FileType(srcName)] != "" {
|
||||||
result = exec.Command(
|
cmd = exec.Command(
|
||||||
ffmpeg,
|
opt.Bin,
|
||||||
"-y",
|
"-y",
|
||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
"-i", fileName,
|
"-i", srcName,
|
||||||
"-pix_fmt", FormatYUV420P.String(),
|
"-pix_fmt", encode.FormatYUV420P.String(),
|
||||||
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
"-vf", "scale='trunc(iw/2)*2:trunc(ih/2)*2'",
|
||||||
"-f", "mp4",
|
"-f", "mp4",
|
||||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||||
avcName,
|
destName,
|
||||||
)
|
)
|
||||||
|
|
||||||
return result, useMutex, nil
|
return cmd, useMutex, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display encoder info.
|
// Display encoder info.
|
||||||
if opt.Encoder != SoftwareEncoder {
|
if opt.Encoder != encode.SoftwareAvc {
|
||||||
log.Infof("convert: ffmpeg encoder %s selected", opt.Encoder.String())
|
log.Infof("convert: ffmpeg encoder %s selected", opt.Encoder.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
switch opt.Encoder {
|
switch opt.Encoder {
|
||||||
case IntelEncoder:
|
case encode.IntelAvc:
|
||||||
// ffmpeg -hide_banner -h encoder=h264_qsv
|
cmd = intel.AvcConvertCmd(srcName, destName, opt)
|
||||||
result = exec.Command(
|
|
||||||
ffmpeg,
|
|
||||||
"-y",
|
|
||||||
"-strict", "-2",
|
|
||||||
"-hwaccel", "qsv",
|
|
||||||
"-hwaccel_output_format", "qsv",
|
|
||||||
"-qsv_device", "/dev/dri/renderD128",
|
|
||||||
"-i", fileName,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", opt.VideoFilter(FormatQSV),
|
|
||||||
"-c:v", opt.Encoder.String(),
|
|
||||||
"-map", opt.MapVideo,
|
|
||||||
"-map", opt.MapAudio,
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", opt.Bitrate,
|
|
||||||
"-bitrate", opt.Bitrate,
|
|
||||||
"-f", "mp4",
|
|
||||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
|
|
||||||
case AppleEncoder:
|
case encode.AppleAvc:
|
||||||
// ffmpeg -hide_banner -h encoder=h264_videotoolbox
|
cmd = apple.AvcConvertCmd(srcName, destName, opt)
|
||||||
result = exec.Command(
|
|
||||||
ffmpeg,
|
|
||||||
"-y",
|
|
||||||
"-strict", "-2",
|
|
||||||
"-i", fileName,
|
|
||||||
"-c:v", opt.Encoder.String(),
|
|
||||||
"-map", opt.MapVideo,
|
|
||||||
"-map", opt.MapAudio,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", opt.VideoFilter(FormatYUV420P),
|
|
||||||
"-profile", "high",
|
|
||||||
"-level", "51",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", opt.Bitrate,
|
|
||||||
"-f", "mp4",
|
|
||||||
"-movflags", "+faststart",
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
|
|
||||||
case VAAPIEncoder:
|
case encode.VaapiAvc:
|
||||||
result = exec.Command(
|
cmd = vaapi.AvcConvertCmd(srcName, destName, opt)
|
||||||
ffmpeg,
|
|
||||||
"-y",
|
|
||||||
"-strict", "-2",
|
|
||||||
"-hwaccel", "vaapi",
|
|
||||||
"-i", fileName,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", opt.VideoFilter(FormatNV12),
|
|
||||||
"-c:v", opt.Encoder.String(),
|
|
||||||
"-map", opt.MapVideo,
|
|
||||||
"-map", opt.MapAudio,
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", opt.Bitrate,
|
|
||||||
"-f", "mp4",
|
|
||||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
|
|
||||||
case NvidiaEncoder:
|
case encode.NvidiaAvc:
|
||||||
// ffmpeg -hide_banner -h encoder=h264_nvenc
|
cmd = nvidia.AvcConvertCmd(srcName, destName, opt)
|
||||||
result = exec.Command(
|
|
||||||
ffmpeg,
|
|
||||||
"-y",
|
|
||||||
"-strict", "-2",
|
|
||||||
"-hwaccel", "auto",
|
|
||||||
"-i", fileName,
|
|
||||||
"-pix_fmt", FormatYUV420P.String(),
|
|
||||||
"-c:v", opt.Encoder.String(),
|
|
||||||
"-map", opt.MapVideo,
|
|
||||||
"-map", opt.MapAudio,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-preset", "15",
|
|
||||||
"-pixel_format", "yuv420p",
|
|
||||||
"-gpu", "any",
|
|
||||||
"-vf", opt.VideoFilter(FormatYUV420P),
|
|
||||||
"-rc:v", "constqp",
|
|
||||||
"-cq", "0",
|
|
||||||
"-tune", "2",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", opt.Bitrate,
|
|
||||||
"-profile:v", "1",
|
|
||||||
"-level:v", "auto",
|
|
||||||
"-coder:v", "1",
|
|
||||||
"-f", "mp4",
|
|
||||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
|
|
||||||
case Video4LinuxEncoder:
|
case encode.V4LAvc:
|
||||||
// ffmpeg -hide_banner -h encoder=h264_v4l2m2m
|
cmd = v4l.AvcConvertCmd(srcName, destName, opt)
|
||||||
result = exec.Command(
|
|
||||||
ffmpeg,
|
|
||||||
"-y",
|
|
||||||
"-strict", "-2",
|
|
||||||
"-i", fileName,
|
|
||||||
"-c:v", opt.Encoder.String(),
|
|
||||||
"-map", opt.MapVideo,
|
|
||||||
"-map", opt.MapAudio,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", opt.VideoFilter(FormatYUV420P),
|
|
||||||
"-num_output_buffers", "72",
|
|
||||||
"-num_capture_buffers", "64",
|
|
||||||
"-max_muxing_queue_size", "1024",
|
|
||||||
"-crf", "23",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", opt.Bitrate,
|
|
||||||
"-f", "mp4",
|
|
||||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
result = exec.Command(
|
cmd = encode.AvcConvertCmd(srcName, destName, opt)
|
||||||
ffmpeg,
|
|
||||||
"-y",
|
|
||||||
"-strict", "-2",
|
|
||||||
"-i", fileName,
|
|
||||||
"-c:v", opt.Encoder.String(),
|
|
||||||
"-map", opt.MapVideo,
|
|
||||||
"-map", opt.MapAudio,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", opt.VideoFilter(FormatYUV420P),
|
|
||||||
"-max_muxing_queue_size", "1024",
|
|
||||||
"-crf", "23",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", opt.Bitrate,
|
|
||||||
"-f", "mp4",
|
|
||||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, useMutex, nil
|
return cmd, useMutex, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,62 @@
|
|||||||
package ffmpeg
|
package ffmpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAvcConvertCommand(t *testing.T) {
|
func TestAvcConvertCmd(t *testing.T) {
|
||||||
t.Run("empty filename", func(t *testing.T) {
|
runCmd := func(t *testing.T, encoder encode.Encoder, srcName, destName string, cmd *exec.Cmd) {
|
||||||
opt := Options{
|
var out bytes.Buffer
|
||||||
Bin: "",
|
var stderr bytes.Buffer
|
||||||
Encoder: "intel",
|
cmd.Stdout = &out
|
||||||
Size: 1500,
|
cmd.Stderr = &stderr
|
||||||
Bitrate: "50M",
|
cmd.Env = append(cmd.Env, []string{
|
||||||
MapVideo: MapVideoDefault,
|
fmt.Sprintf("HOME=%s", fs.Abs("./testdata")),
|
||||||
MapAudio: MapAudioDefault,
|
}...)
|
||||||
}
|
|
||||||
_, _, err := AvcConvertCommand("", "", opt)
|
|
||||||
|
|
||||||
assert.Equal(t, err.Error(), "empty input filename")
|
// Transcode source media file to AVC.
|
||||||
})
|
start := time.Now()
|
||||||
t.Run("avc name empty", func(t *testing.T) {
|
if err := cmd.Run(); err != nil {
|
||||||
opt := Options{
|
if stderr.String() != "" {
|
||||||
Bin: "",
|
err = errors.New(stderr.String())
|
||||||
Encoder: "intel",
|
}
|
||||||
Size: 1500,
|
|
||||||
Bitrate: "50M",
|
|
||||||
MapVideo: MapVideoDefault,
|
|
||||||
MapAudio: MapAudioDefault,
|
|
||||||
}
|
|
||||||
_, _, err := AvcConvertCommand("VID123.mov", "", opt)
|
|
||||||
|
|
||||||
assert.Equal(t, err.Error(), "empty output filename")
|
// Remove broken video file.
|
||||||
})
|
if !fs.FileExists(destName) {
|
||||||
t.Run("animated file", func(t *testing.T) {
|
// Do nothing.
|
||||||
opt := Options{
|
} else if removeErr := os.Remove(destName); removeErr != nil {
|
||||||
|
t.Logf("%s: failed to remove %s after error (%s)", encoder, srcName, removeErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log ffmpeg output for debugging.
|
||||||
|
if err.Error() != "" {
|
||||||
|
t.Error(err)
|
||||||
|
t.Fatalf("%s: failed to transcode %s [%s]", encoder, srcName, time.Since(start))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log filename and transcoding time.
|
||||||
|
t.Logf("%s: created %s [%s]", encoder, destName, time.Since(start))
|
||||||
|
|
||||||
|
if removeErr := os.Remove(destName); removeErr != nil {
|
||||||
|
t.Fatalf("%s: failed to remove %s after successful test (%s)", encoder, srcName, removeErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("NoSource", func(t *testing.T) {
|
||||||
|
opt := encode.Options{
|
||||||
Bin: "",
|
Bin: "",
|
||||||
Encoder: "intel",
|
Encoder: "intel",
|
||||||
Size: 1500,
|
Size: 1500,
|
||||||
@@ -42,33 +64,101 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||||||
MapVideo: MapVideoDefault,
|
MapVideo: MapVideoDefault,
|
||||||
MapAudio: MapAudioDefault,
|
MapAudio: MapAudioDefault,
|
||||||
}
|
}
|
||||||
r, _, err := AvcConvertCommand("VID123.gif", "VID123.gif.avc", opt)
|
_, _, err := AvcConvertCmd("", "", opt)
|
||||||
|
|
||||||
|
assert.Equal(t, "empty source filename", err.Error())
|
||||||
|
})
|
||||||
|
t.Run("NoDestination", func(t *testing.T) {
|
||||||
|
opt := encode.Options{
|
||||||
|
Bin: "",
|
||||||
|
Encoder: "intel",
|
||||||
|
Size: 1500,
|
||||||
|
Bitrate: "50M",
|
||||||
|
MapVideo: MapVideoDefault,
|
||||||
|
MapAudio: MapAudioDefault,
|
||||||
|
}
|
||||||
|
_, _, err := AvcConvertCmd("VID123.mov", "", opt)
|
||||||
|
|
||||||
|
assert.Equal(t, "empty destination filename", err.Error())
|
||||||
|
})
|
||||||
|
t.Run("Animation", func(t *testing.T) {
|
||||||
|
opt := encode.Options{
|
||||||
|
Bin: "",
|
||||||
|
Encoder: "intel",
|
||||||
|
Size: 1500,
|
||||||
|
Bitrate: "50M",
|
||||||
|
MapVideo: MapVideoDefault,
|
||||||
|
MapAudio: MapAudioDefault,
|
||||||
|
}
|
||||||
|
r, _, err := AvcConvertCmd("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 -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("libx264", func(t *testing.T) {
|
t.Run("VP9", func(t *testing.T) {
|
||||||
opt := Options{
|
encoder := encode.SoftwareAvc
|
||||||
Bin: "",
|
|
||||||
Encoder: "libx264",
|
opt := encode.Options{
|
||||||
|
Bin: "/usr/bin/ffmpeg",
|
||||||
|
Encoder: encoder,
|
||||||
Size: 1500,
|
Size: 1500,
|
||||||
Bitrate: "50M",
|
Bitrate: "50M",
|
||||||
MapVideo: MapVideoDefault,
|
MapVideo: MapVideoDefault,
|
||||||
MapAudio: MapAudioDefault,
|
MapAudio: MapAudioDefault,
|
||||||
}
|
}
|
||||||
r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt)
|
|
||||||
|
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||||
|
destName := fs.Abs("./testdata/25fps.avc")
|
||||||
|
|
||||||
|
cmd, _, err := AvcConvertCmd(srcName, destName, 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 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 50M -f mp4 -movflags +faststart VID123.mov.avc")
|
cmdStr := cmd.String()
|
||||||
|
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 50M -f mp4 -movflags +faststart DEST", cmdStr)
|
||||||
|
|
||||||
|
// Performs software transcoding.
|
||||||
|
runCmd(t, encoder, srcName, destName, cmd)
|
||||||
})
|
})
|
||||||
t.Run("h264_qsv", func(t *testing.T) {
|
t.Run("Vaapi", func(t *testing.T) {
|
||||||
opt := Options{
|
encoder := encode.VaapiAvc
|
||||||
|
opt := encode.Options{
|
||||||
|
Bin: "/usr/bin/ffmpeg",
|
||||||
|
Encoder: encoder,
|
||||||
|
Size: 1500,
|
||||||
|
Bitrate: "50M",
|
||||||
|
MapVideo: MapVideoDefault,
|
||||||
|
MapAudio: MapAudioDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||||
|
destName := fs.Abs("./testdata/25fps.vaapi.avc")
|
||||||
|
|
||||||
|
cmd, _, err := AvcConvertCmd(srcName, destName, opt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdStr := cmd.String()
|
||||||
|
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 50M -f mp4 -movflags +faststart DEST", cmdStr)
|
||||||
|
|
||||||
|
// Requires Video4Linux transcoding device.
|
||||||
|
// runCmd(t, encoder, srcName, destName, cmd)
|
||||||
|
})
|
||||||
|
t.Run("QSV", func(t *testing.T) {
|
||||||
|
opt := encode.Options{
|
||||||
Bin: "",
|
Bin: "",
|
||||||
Encoder: "h264_qsv",
|
Encoder: "h264_qsv",
|
||||||
Size: 1500,
|
Size: 1500,
|
||||||
@@ -76,7 +166,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||||||
MapVideo: MapVideoDefault,
|
MapVideo: MapVideoDefault,
|
||||||
MapAudio: MapAudioDefault,
|
MapAudio: MapAudioDefault,
|
||||||
}
|
}
|
||||||
r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt)
|
r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -84,8 +174,8 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||||||
|
|
||||||
assert.Contains(t, r.String(), "/bin/ffmpeg -y -strict -2 -hwaccel qsv -hwaccel_output_format qsv -qsv_device /dev/dri/renderD128 -i VID123.mov -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 50M -bitrate 50M -f mp4 -movflags +faststart VID123.mov.avc")
|
assert.Contains(t, r.String(), "/bin/ffmpeg -y -strict -2 -hwaccel qsv -hwaccel_output_format qsv -qsv_device /dev/dri/renderD128 -i VID123.mov -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 50M -bitrate 50M -f mp4 -movflags +faststart VID123.mov.avc")
|
||||||
})
|
})
|
||||||
t.Run("h264_videotoolbox", func(t *testing.T) {
|
t.Run("Apple", func(t *testing.T) {
|
||||||
opt := Options{
|
opt := encode.Options{
|
||||||
Bin: "",
|
Bin: "",
|
||||||
Encoder: "h264_videotoolbox",
|
Encoder: "h264_videotoolbox",
|
||||||
Size: 1500,
|
Size: 1500,
|
||||||
@@ -93,7 +183,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||||||
MapVideo: MapVideoDefault,
|
MapVideo: MapVideoDefault,
|
||||||
MapAudio: MapAudioDefault,
|
MapAudio: MapAudioDefault,
|
||||||
}
|
}
|
||||||
r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt)
|
r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -101,25 +191,8 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||||||
|
|
||||||
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 50M -f mp4 -movflags +faststart VID123.mov.avc")
|
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 50M -f mp4 -movflags +faststart VID123.mov.avc")
|
||||||
})
|
})
|
||||||
t.Run("h264_vaapi", func(t *testing.T) {
|
t.Run("Nvidia", func(t *testing.T) {
|
||||||
opt := Options{
|
opt := encode.Options{
|
||||||
Bin: "",
|
|
||||||
Encoder: "h264_vaapi",
|
|
||||||
Size: 1500,
|
|
||||||
Bitrate: "50M",
|
|
||||||
MapVideo: MapVideoDefault,
|
|
||||||
MapAudio: MapAudioDefault,
|
|
||||||
}
|
|
||||||
r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -hwaccel vaapi -i VID123.mov -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 50M -f mp4 -movflags +faststart VID123.mov.avc")
|
|
||||||
})
|
|
||||||
t.Run("h264_nvenc", func(t *testing.T) {
|
|
||||||
opt := Options{
|
|
||||||
Bin: "",
|
Bin: "",
|
||||||
Encoder: "h264_nvenc",
|
Encoder: "h264_nvenc",
|
||||||
Size: 1500,
|
Size: 1500,
|
||||||
@@ -127,7 +200,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||||||
MapVideo: MapVideoDefault,
|
MapVideo: MapVideoDefault,
|
||||||
MapAudio: MapAudioDefault,
|
MapAudio: MapAudioDefault,
|
||||||
}
|
}
|
||||||
r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt)
|
r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -135,8 +208,8 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||||||
|
|
||||||
assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -hwaccel auto -i VID123.mov -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 50M -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags +faststart VID123.mov.avc")
|
assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -hwaccel auto -i VID123.mov -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 50M -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags +faststart VID123.mov.avc")
|
||||||
})
|
})
|
||||||
t.Run("h264_v4l2m2m", func(t *testing.T) {
|
t.Run("Video4Linux", func(t *testing.T) {
|
||||||
opt := Options{
|
opt := encode.Options{
|
||||||
Bin: "",
|
Bin: "",
|
||||||
Encoder: "h264_v4l2m2m",
|
Encoder: "h264_v4l2m2m",
|
||||||
Size: 1500,
|
Size: 1500,
|
||||||
@@ -144,7 +217,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||||||
MapVideo: MapVideoDefault,
|
MapVideo: MapVideoDefault,
|
||||||
MapAudio: MapAudioDefault,
|
MapAudio: MapAudioDefault,
|
||||||
}
|
}
|
||||||
r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt)
|
r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
25
internal/ffmpeg/encode/avc.go
Normal file
25
internal/ffmpeg/encode/avc.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package encode
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
// AvcConvertCmd returns the command for software transcoding of video files to MPEG-4 AVC.
|
||||||
|
func AvcConvertCmd(srcName, destName string, opt Options) *exec.Cmd {
|
||||||
|
return exec.Command(
|
||||||
|
opt.Bin,
|
||||||
|
"-y",
|
||||||
|
"-strict", "-2",
|
||||||
|
"-i", srcName,
|
||||||
|
"-c:v", opt.Encoder.String(),
|
||||||
|
"-map", opt.MapVideo,
|
||||||
|
"-map", opt.MapAudio,
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", opt.VideoFilter(FormatYUV420P),
|
||||||
|
"-max_muxing_queue_size", "1024",
|
||||||
|
"-crf", "23",
|
||||||
|
"-r", "30",
|
||||||
|
"-b:v", opt.Bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
}
|
||||||
31
internal/ffmpeg/encode/encode.go
Normal file
31
internal/ffmpeg/encode/encode.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Package encode provides FFmpeg video encoder related types and functions.
|
||||||
|
|
||||||
|
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||||
|
<https://docs.photoprism.app/license/agpl>
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||||
|
which describe how our Brand Assets may be used:
|
||||||
|
<https://www.photoprism.app/trademark>
|
||||||
|
|
||||||
|
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||||
|
want to support our work, or just want to say hello.
|
||||||
|
|
||||||
|
Additional information can be found in our Developer Guide:
|
||||||
|
<https://docs.photoprism.app/developer-guide/>
|
||||||
|
*/
|
||||||
|
package encode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = event.Log
|
||||||
63
internal/ffmpeg/encode/encoder.go
Normal file
63
internal/ffmpeg/encode/encoder.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package encode
|
||||||
|
|
||||||
|
import "github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
|
||||||
|
// Encoder represents a supported FFmpeg AVC encoder name.
|
||||||
|
type Encoder string
|
||||||
|
|
||||||
|
// String returns the FFmpeg AVC encoder name as string.
|
||||||
|
func (name Encoder) String() string {
|
||||||
|
return string(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported FFmpeg AVC encoders.
|
||||||
|
const (
|
||||||
|
SoftwareAvc Encoder = "libx264" // SoftwareAvc see https://trac.ffmpeg.org/wiki/HWAccelIntro.
|
||||||
|
IntelAvc Encoder = "h264_qsv" // IntelAvc is the Intel Quick Sync H.264 encoder.
|
||||||
|
AppleAvc Encoder = "h264_videotoolbox" // AppleAvc is the Apple Video Toolbox H.264 encoder.
|
||||||
|
VaapiAvc Encoder = "h264_vaapi" // VaapiAvc is the Video Acceleration API H.264 encoder.
|
||||||
|
NvidiaAvc Encoder = "h264_nvenc" // NvidiaAvc is the NVIDIA H.264 encoder.
|
||||||
|
V4LAvc Encoder = "h264_v4l2m2m" // V4LAvc is the Video4Linux H.264 encoder.
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvcEncoders is the list of supported H.264 encoders with aliases.
|
||||||
|
var AvcEncoders = map[string]Encoder{
|
||||||
|
"": SoftwareAvc,
|
||||||
|
"default": SoftwareAvc,
|
||||||
|
"software": SoftwareAvc,
|
||||||
|
string(SoftwareAvc): SoftwareAvc,
|
||||||
|
"intel": IntelAvc,
|
||||||
|
"qsv": IntelAvc,
|
||||||
|
string(IntelAvc): IntelAvc,
|
||||||
|
"apple": AppleAvc,
|
||||||
|
"osx": AppleAvc,
|
||||||
|
"mac": AppleAvc,
|
||||||
|
"macos": AppleAvc,
|
||||||
|
"darwin": AppleAvc,
|
||||||
|
string(AppleAvc): AppleAvc,
|
||||||
|
"vaapi": VaapiAvc,
|
||||||
|
"libva": VaapiAvc,
|
||||||
|
string(VaapiAvc): VaapiAvc,
|
||||||
|
"nvidia": NvidiaAvc,
|
||||||
|
"nvenc": NvidiaAvc,
|
||||||
|
"cuda": NvidiaAvc,
|
||||||
|
string(NvidiaAvc): NvidiaAvc,
|
||||||
|
"v4l2": V4LAvc,
|
||||||
|
"v4l": V4LAvc,
|
||||||
|
"video4linux": V4LAvc,
|
||||||
|
"rp4": V4LAvc,
|
||||||
|
"raspberry": V4LAvc,
|
||||||
|
"raspberrypi": V4LAvc,
|
||||||
|
string(V4LAvc): V4LAvc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindEncoder finds an FFmpeg encoder by name.
|
||||||
|
func FindEncoder(s string) Encoder {
|
||||||
|
if encoder, ok := AvcEncoders[s]; ok {
|
||||||
|
return encoder
|
||||||
|
} else {
|
||||||
|
log.Warnf("ffmpeg: unsupported encoder %s", clean.Log(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
return SoftwareAvc
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package ffmpeg
|
package encode
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package ffmpeg
|
package encode
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// Options represents transcoding options.
|
// Options represents transcoding options.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Bin string
|
Bin string
|
||||||
Encoder AvcEncoder
|
Encoder Encoder
|
||||||
Size int
|
Size int
|
||||||
Bitrate string
|
Bitrate string
|
||||||
MapVideo string
|
MapVideo string
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package ffmpeg
|
package encode
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package ffmpeg
|
package encode
|
||||||
|
|
||||||
// PixelFormat represents a standard pixel format.
|
// PixelFormat represents a standard pixel format.
|
||||||
type PixelFormat string
|
type PixelFormat string
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
import "github.com/photoprism/photoprism/pkg/clean"
|
|
||||||
|
|
||||||
// AvcEncoder represents a supported FFmpeg AVC encoder name.
|
|
||||||
type AvcEncoder string
|
|
||||||
|
|
||||||
// String returns the FFmpeg AVC encoder name as string.
|
|
||||||
func (name AvcEncoder) String() string {
|
|
||||||
return string(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supported FFmpeg AVC encoders.
|
|
||||||
const (
|
|
||||||
SoftwareEncoder AvcEncoder = "libx264" // SoftwareEncoder see https://trac.ffmpeg.org/wiki/HWAccelIntro.
|
|
||||||
IntelEncoder AvcEncoder = "h264_qsv" // IntelEncoder is the Intel Quick Sync H.264 encoder.
|
|
||||||
AppleEncoder AvcEncoder = "h264_videotoolbox" // AppleEncoder is the Apple Video Toolbox H.264 encoder.
|
|
||||||
VAAPIEncoder AvcEncoder = "h264_vaapi" // VAAPIEncoder is the Video Acceleration API H.264 encoder.
|
|
||||||
NvidiaEncoder AvcEncoder = "h264_nvenc" // NvidiaEncoder is the NVIDIA H.264 encoder.
|
|
||||||
Video4LinuxEncoder AvcEncoder = "h264_v4l2m2m" // Video4LinuxEncoder is the Video4Linux H.264 encoder.
|
|
||||||
)
|
|
||||||
|
|
||||||
// AvcEncoders is the list of supported H.264 encoders with aliases.
|
|
||||||
var AvcEncoders = map[string]AvcEncoder{
|
|
||||||
"": SoftwareEncoder,
|
|
||||||
"default": SoftwareEncoder,
|
|
||||||
"software": SoftwareEncoder,
|
|
||||||
string(SoftwareEncoder): SoftwareEncoder,
|
|
||||||
"intel": IntelEncoder,
|
|
||||||
"qsv": IntelEncoder,
|
|
||||||
string(IntelEncoder): IntelEncoder,
|
|
||||||
"apple": AppleEncoder,
|
|
||||||
"osx": AppleEncoder,
|
|
||||||
"mac": AppleEncoder,
|
|
||||||
"macos": AppleEncoder,
|
|
||||||
"darwin": AppleEncoder,
|
|
||||||
string(AppleEncoder): AppleEncoder,
|
|
||||||
"vaapi": VAAPIEncoder,
|
|
||||||
"libva": VAAPIEncoder,
|
|
||||||
string(VAAPIEncoder): VAAPIEncoder,
|
|
||||||
"nvidia": NvidiaEncoder,
|
|
||||||
"nvenc": NvidiaEncoder,
|
|
||||||
"cuda": NvidiaEncoder,
|
|
||||||
string(NvidiaEncoder): NvidiaEncoder,
|
|
||||||
"v4l2": Video4LinuxEncoder,
|
|
||||||
"v4l": Video4LinuxEncoder,
|
|
||||||
"video4linux": Video4LinuxEncoder,
|
|
||||||
"rp4": Video4LinuxEncoder,
|
|
||||||
"raspberry": Video4LinuxEncoder,
|
|
||||||
"raspberrypi": Video4LinuxEncoder,
|
|
||||||
string(Video4LinuxEncoder): Video4LinuxEncoder,
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindEncoder finds an FFmpeg encoder by name.
|
|
||||||
func FindEncoder(s string) AvcEncoder {
|
|
||||||
if encoder, ok := AvcEncoders[s]; ok {
|
|
||||||
return encoder
|
|
||||||
} else {
|
|
||||||
log.Warnf("ffmpeg: unsupported encoder %s", clean.Log(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
return SoftwareEncoder
|
|
||||||
}
|
|
||||||
32
internal/ffmpeg/intel/avc.go
Normal file
32
internal/ffmpeg/intel/avc.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package intel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC.
|
||||||
|
func AvcConvertCmd(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",
|
||||||
|
"-qsv_device", "/dev/dri/renderD128",
|
||||||
|
"-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.Bitrate,
|
||||||
|
"-bitrate", opt.Bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
}
|
||||||
39
internal/ffmpeg/nvidia/avc.go
Normal file
39
internal/ffmpeg/nvidia/avc.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package nvidia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC.
|
||||||
|
func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||||
|
// ffmpeg -hide_banner -h encoder=h264_nvenc
|
||||||
|
return exec.Command(
|
||||||
|
opt.Bin,
|
||||||
|
"-y",
|
||||||
|
"-strict", "-2",
|
||||||
|
"-hwaccel", "auto",
|
||||||
|
"-i", srcName,
|
||||||
|
"-pix_fmt", encode.FormatYUV420P.String(),
|
||||||
|
"-c:v", opt.Encoder.String(),
|
||||||
|
"-map", opt.MapVideo,
|
||||||
|
"-map", opt.MapAudio,
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-preset", "15",
|
||||||
|
"-pixel_format", "yuv420p",
|
||||||
|
"-gpu", "any",
|
||||||
|
"-vf", opt.VideoFilter(encode.FormatYUV420P),
|
||||||
|
"-rc:v", "constqp",
|
||||||
|
"-cq", "0",
|
||||||
|
"-tune", "2",
|
||||||
|
"-r", "30",
|
||||||
|
"-b:v", opt.Bitrate,
|
||||||
|
"-profile:v", "1",
|
||||||
|
"-level:v", "auto",
|
||||||
|
"-coder:v", "1",
|
||||||
|
"-f", "mp4",
|
||||||
|
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
}
|
||||||
BIN
internal/ffmpeg/testdata/25fps.vp9
vendored
Normal file
BIN
internal/ffmpeg/testdata/25fps.vp9
vendored
Normal file
Binary file not shown.
32
internal/ffmpeg/v4l/avc.go
Normal file
32
internal/ffmpeg/v4l/avc.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package v4l
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC.
|
||||||
|
func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||||
|
// ffmpeg -hide_banner -h encoder=h264_v4l2m2m
|
||||||
|
return exec.Command(
|
||||||
|
opt.Bin,
|
||||||
|
"-y",
|
||||||
|
"-strict", "-2",
|
||||||
|
"-i", srcName,
|
||||||
|
"-c:v", opt.Encoder.String(),
|
||||||
|
"-map", opt.MapVideo,
|
||||||
|
"-map", opt.MapAudio,
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", opt.VideoFilter(encode.FormatYUV420P),
|
||||||
|
"-num_output_buffers", "72",
|
||||||
|
"-num_capture_buffers", "64",
|
||||||
|
"-max_muxing_queue_size", "1024",
|
||||||
|
"-crf", "23",
|
||||||
|
"-r", "30",
|
||||||
|
"-b:v", opt.Bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
}
|
||||||
28
internal/ffmpeg/vaapi/avc.go
Normal file
28
internal/ffmpeg/vaapi/avc.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package vaapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC.
|
||||||
|
func AvcConvertCmd(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.Bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||||
|
destName,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,15 +6,15 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/media"
|
"github.com/photoprism/photoprism/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConvertCommand represents a command to be executed for converting a MediaFile.
|
// ConvertCmd represents a command to be executed for converting a MediaFile.
|
||||||
// including any options to be used for this.
|
// including any options to be used for this.
|
||||||
type ConvertCommand struct {
|
type ConvertCmd struct {
|
||||||
Cmd *exec.Cmd
|
Cmd *exec.Cmd
|
||||||
Orientation media.Orientation
|
Orientation media.Orientation
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the conversion command as string e.g. for logging.
|
// String returns the conversion command as string e.g. for logging.
|
||||||
func (c *ConvertCommand) String() string {
|
func (c *ConvertCmd) String() string {
|
||||||
if c.Cmd == nil {
|
if c.Cmd == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -23,33 +23,32 @@ func (c *ConvertCommand) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithOrientation sets the media Orientation after successful conversion.
|
// WithOrientation sets the media Orientation after successful conversion.
|
||||||
func (c *ConvertCommand) WithOrientation(o media.Orientation) *ConvertCommand {
|
func (c *ConvertCmd) WithOrientation(o media.Orientation) *ConvertCmd {
|
||||||
c.Orientation = media.ParseOrientation(o, c.Orientation)
|
c.Orientation = media.ParseOrientation(o, c.Orientation)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetOrientation resets the media Orientation after successful conversion.
|
// ResetOrientation resets the media Orientation after successful conversion.
|
||||||
func (c *ConvertCommand) ResetOrientation() *ConvertCommand {
|
func (c *ConvertCmd) ResetOrientation() *ConvertCmd {
|
||||||
return c.WithOrientation(media.ResetOrientation)
|
return c.WithOrientation(media.ResetOrientation)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConvertCommand returns a new file converter command with default options.
|
// NewConvertCmd returns a new file converter command with default options.
|
||||||
func NewConvertCommand(cmd *exec.Cmd) *ConvertCommand {
|
func NewConvertCmd(cmd *exec.Cmd) *ConvertCmd {
|
||||||
if cmd == nil {
|
if cmd == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ConvertCommand{
|
return &ConvertCmd{
|
||||||
Cmd: cmd, // File conversion command.
|
Cmd: cmd, // File conversion command.
|
||||||
Orientation: media.KeepOrientation, // Keep the orientation by default.
|
Orientation: media.KeepOrientation, // Keep the orientation by default.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertCommands represents a list of possible ConvertCommand commands for converting a MediaFile,
|
// ConvertCmds represents a list of possible ConvertCommand commands for converting a MediaFile, sorted by priority.
|
||||||
// sorted by priority.
|
type ConvertCmds []*ConvertCmd
|
||||||
type ConvertCommands []*ConvertCommand
|
|
||||||
|
|
||||||
// NewConvertCommands returns a new, empty list of ConvertCommand commands.
|
// NewConvertCmds returns a new, empty list of ConvertCommand commands.
|
||||||
func NewConvertCommands() ConvertCommands {
|
func NewConvertCmds() ConvertCmds {
|
||||||
return make(ConvertCommands, 0, 8)
|
return make(ConvertCmds, 0, 8)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/media"
|
"github.com/photoprism/photoprism/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewConvertCommand(t *testing.T) {
|
func TestNewConvertCmd(t *testing.T) {
|
||||||
t.Run("Nil", func(t *testing.T) {
|
t.Run("Nil", func(t *testing.T) {
|
||||||
assert.Nil(t, NewConvertCommand(nil))
|
assert.Nil(t, NewConvertCmd(nil))
|
||||||
})
|
})
|
||||||
t.Run("Default", func(t *testing.T) {
|
t.Run("Default", func(t *testing.T) {
|
||||||
result := NewConvertCommand(
|
result := NewConvertCmd(
|
||||||
exec.Command("/usr/bin/sips", "-Z", "123", "-s", "format", "jpeg", "--out", "file.jpeg", "file.heic"),
|
exec.Command("/usr/bin/sips", "-Z", "123", "-s", "format", "jpeg", "--out", "file.jpeg", "file.heic"),
|
||||||
)
|
)
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
@@ -23,7 +23,7 @@ func TestNewConvertCommand(t *testing.T) {
|
|||||||
assert.Equal(t, media.KeepOrientation, result.Orientation)
|
assert.Equal(t, media.KeepOrientation, result.Orientation)
|
||||||
})
|
})
|
||||||
t.Run("WithOrientation", func(t *testing.T) {
|
t.Run("WithOrientation", func(t *testing.T) {
|
||||||
result := NewConvertCommand(
|
result := NewConvertCmd(
|
||||||
exec.Command("/usr/bin/sips", "-Z", "123", "-s", "format", "jpeg", "--out", "file.jpeg", "file.heic"),
|
exec.Command("/usr/bin/sips", "-Z", "123", "-s", "format", "jpeg", "--out", "file.jpeg", "file.heic"),
|
||||||
)
|
)
|
||||||
result.WithOrientation(media.ResetOrientation)
|
result.WithOrientation(media.ResetOrientation)
|
||||||
@@ -34,9 +34,9 @@ func TestNewConvertCommand(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewConvertCommands(t *testing.T) {
|
func TestNewConvertCmds(t *testing.T) {
|
||||||
t.Run("Success", func(t *testing.T) {
|
t.Run("Success", func(t *testing.T) {
|
||||||
result := NewConvertCommands()
|
result := NewConvertCmds()
|
||||||
assert.NotNil(t, result)
|
assert.NotNil(t, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,16 +108,16 @@ func (w *Convert) ToImage(f *MediaFile, force bool) (result *MediaFile, err erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run external commands for other formats.
|
// Run external commands for other formats.
|
||||||
var cmds ConvertCommands
|
var cmds ConvertCmds
|
||||||
var useMutex bool
|
var useMutex bool
|
||||||
var expectedMime string
|
var expectedMime string
|
||||||
|
|
||||||
switch fs.LowerExt(imageName) {
|
switch fs.LowerExt(imageName) {
|
||||||
case fs.ExtPNG:
|
case fs.ExtPNG:
|
||||||
cmds, useMutex, err = w.PngConvertCommands(f, imageName)
|
cmds, useMutex, err = w.PngConvertCmds(f, imageName)
|
||||||
expectedMime = fs.MimeTypePNG
|
expectedMime = fs.MimeTypePNG
|
||||||
case fs.ExtJPEG:
|
case fs.ExtJPEG:
|
||||||
cmds, useMutex, err = w.JpegConvertCommands(f, imageName, xmpName)
|
cmds, useMutex, err = w.JpegConvertCmds(f, imageName, xmpName)
|
||||||
expectedMime = fs.MimeTypeJPEG
|
expectedMime = fs.MimeTypeJPEG
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("convert: unspported target format %s (%s)", fs.LowerExt(imageName), clean.Log(f.RootRelName()))
|
return nil, fmt.Errorf("convert: unspported target format %s (%s)", fs.LowerExt(imageName), clean.Log(f.RootRelName()))
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JpegConvertCommands returns the supported commands for converting a MediaFile to JPEG, sorted by priority.
|
// JpegConvertCmds returns the supported commands for converting a MediaFile to JPEG, sorted by priority.
|
||||||
func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName string) (result ConvertCommands, useMutex bool, err error) {
|
func (w *Convert) JpegConvertCmds(f *MediaFile, jpegName string, xmpName string) (result ConvertCmds, useMutex bool, err error) {
|
||||||
result = NewConvertCommands()
|
result = NewConvertCmds()
|
||||||
|
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return result, useMutex, fmt.Errorf("file is nil - you may have found a bug")
|
return result, useMutex, fmt.Errorf("file is nil - you may have found a bug")
|
||||||
@@ -23,7 +23,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||||||
|
|
||||||
// Apple Scriptable image processing system: https://ss64.com/osx/sips.html
|
// Apple Scriptable image processing system: https://ss64.com/osx/sips.html
|
||||||
if (f.IsRaw() || f.IsHEIF()) && w.conf.SipsEnabled() && w.sipsExclude.Allow(fileExt) {
|
if (f.IsRaw() || f.IsHEIF()) && w.conf.SipsEnabled() && w.sipsExclude.Allow(fileExt) {
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName())),
|
exec.Command(w.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||||||
|
|
||||||
// TODO: Adjust command flags for correct colors with HDR10-encoded HEVC videos,
|
// TODO: Adjust command flags for correct colors with HDR10-encoded HEVC videos,
|
||||||
// see https://github.com/photoprism/photoprism/issues/4488
|
// see https://github.com/photoprism/photoprism/issues/4488
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.FFmpegBin(), "-y", "-strict", "-2", "-ss", timeOffset, "-i", f.FileName(), "-vframes", "1",
|
exec.Command(w.conf.FFmpegBin(), "-y", "-strict", "-2", "-ss", timeOffset, "-i", f.FileName(), "-vframes", "1",
|
||||||
// Unfortunately, this filter renders thumbnails of non-HDR videos too dark:
|
// Unfortunately, this filter renders 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",
|
||||||
@@ -44,7 +44,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||||||
|
|
||||||
// Use heif-convert for HEIC/HEIF and AVIF image files.
|
// Use heif-convert for HEIC/HEIF and AVIF image files.
|
||||||
if (f.IsHEIC() || f.IsAVIF()) && w.conf.HeifConvertEnabled() {
|
if (f.IsHEIC() || f.IsAVIF()) && w.conf.HeifConvertEnabled() {
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.HeifConvertBin(), "-q", w.conf.JpegQuality().String(), f.FileName(), jpegName)).
|
exec.Command(w.conf.HeifConvertBin(), "-q", w.conf.JpegQuality().String(), f.FileName(), jpegName)).
|
||||||
WithOrientation(w.conf.HeifConvertOrientation()),
|
WithOrientation(w.conf.HeifConvertOrientation()),
|
||||||
)
|
)
|
||||||
@@ -82,7 +82,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||||||
args = append(args, "--cachedir", dir)
|
args = append(args, "--cachedir", dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.DarktableBin(), args...)),
|
exec.Command(w.conf.DarktableBin(), args...)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||||||
|
|
||||||
args := []string{"-o", jpegName, "-p", profile, "-s", "-d", jpegQuality, "-js3", "-b8", "-c", f.FileName()}
|
args := []string{"-o", jpegName, "-p", profile, "-s", "-d", jpegQuality, "-js3", "-b8", "-c", f.FileName()}
|
||||||
|
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.RawTherapeeBin(), args...)),
|
exec.Command(w.conf.RawTherapeeBin(), args...)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -102,14 +102,14 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||||||
// Extract preview image from DNG files.
|
// Extract preview image from DNG files.
|
||||||
if f.IsDNG() && w.conf.ExifToolEnabled() {
|
if f.IsDNG() && w.conf.ExifToolEnabled() {
|
||||||
// Example: exiftool -b -PreviewImage -w IMG_4691.DNG.jpg IMG_4691.DNG
|
// Example: exiftool -b -PreviewImage -w IMG_4691.DNG.jpg IMG_4691.DNG
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.ExifToolBin(), "-q", "-q", "-b", "-PreviewImage", f.FileName())),
|
exec.Command(w.conf.ExifToolBin(), "-q", "-q", "-b", "-PreviewImage", f.FileName())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode JPEG XL image if support is enabled.
|
// Decode JPEG XL image if support is enabled.
|
||||||
if f.IsJpegXL() && w.conf.JpegXLEnabled() {
|
if f.IsJpegXL() && w.conf.JpegXLEnabled() {
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.JpegXLDecoderBin(), f.FileName(), jpegName)),
|
exec.Command(w.conf.JpegXLDecoderBin(), f.FileName(), jpegName)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str
|
|||||||
quality := fmt.Sprintf("%d", w.conf.JpegQuality())
|
quality := fmt.Sprintf("%d", w.conf.JpegQuality())
|
||||||
resize := fmt.Sprintf("%dx%d>", w.conf.JpegSize(), w.conf.JpegSize())
|
resize := fmt.Sprintf("%dx%d>", w.conf.JpegSize(), w.conf.JpegSize())
|
||||||
args := []string{f.FileName(), "-flatten", "-resize", resize, "-quality", quality, jpegName}
|
args := []string{f.FileName(), "-flatten", "-resize", resize, "-quality", quality, jpegName}
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.ImageMagickBin(), args...)),
|
exec.Command(w.conf.ImageMagickBin(), args...)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PngConvertCommands returns commands for converting a media file to PNG, if possible.
|
// PngConvertCmds returns commands for converting a media file to PNG, if possible.
|
||||||
func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result ConvertCommands, useMutex bool, err error) {
|
func (w *Convert) PngConvertCmds(f *MediaFile, pngName string) (result ConvertCmds, useMutex bool, err error) {
|
||||||
result = NewConvertCommands()
|
result = NewConvertCmds()
|
||||||
|
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return result, useMutex, fmt.Errorf("file is nil - you may have found a bug")
|
return result, useMutex, fmt.Errorf("file is nil - you may have found a bug")
|
||||||
@@ -22,7 +22,7 @@ func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result Conve
|
|||||||
|
|
||||||
// Apple Scriptable image processing system: https://ss64.com/osx/sips.html
|
// Apple Scriptable image processing system: https://ss64.com/osx/sips.html
|
||||||
if (f.IsRaw() || f.IsHEIF()) && w.conf.SipsEnabled() && w.sipsExclude.Allow(fileExt) {
|
if (f.IsRaw() || f.IsHEIF()) && w.conf.SipsEnabled() && w.sipsExclude.Allow(fileExt) {
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.SipsBin(), "-Z", maxSize, "-s", "format", "png", "--out", pngName, f.FileName())),
|
exec.Command(w.conf.SipsBin(), "-Z", maxSize, "-s", "format", "png", "--out", pngName, f.FileName())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -30,14 +30,14 @@ func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result Conve
|
|||||||
// Extract a video still image that can be used as preview.
|
// Extract a video still image that can be used as preview.
|
||||||
if f.IsAnimated() && !f.IsWebP() && w.conf.FFmpegEnabled() {
|
if f.IsAnimated() && !f.IsWebP() && w.conf.FFmpegEnabled() {
|
||||||
// Use "ffmpeg" to extract a PNG still image from the video.
|
// Use "ffmpeg" to extract a PNG still image from the video.
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.FFmpegBin(), "-y", "-strict", "-2", "-ss", ffmpeg.PreviewTimeOffset(f.Duration()), "-i", f.FileName(), "-vframes", "1", pngName)),
|
exec.Command(w.conf.FFmpegBin(), "-y", "-strict", "-2", "-ss", ffmpeg.PreviewTimeOffset(f.Duration()), "-i", f.FileName(), "-vframes", "1", pngName)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use heif-convert for HEIC/HEIF and AVIF image files.
|
// Use heif-convert for HEIC/HEIF and AVIF image files.
|
||||||
if (f.IsHEIC() || f.IsAVIF()) && w.conf.HeifConvertEnabled() {
|
if (f.IsHEIC() || f.IsAVIF()) && w.conf.HeifConvertEnabled() {
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.HeifConvertBin(), f.FileName(), pngName)).
|
exec.Command(w.conf.HeifConvertBin(), f.FileName(), pngName)).
|
||||||
WithOrientation(w.conf.HeifConvertOrientation()),
|
WithOrientation(w.conf.HeifConvertOrientation()),
|
||||||
)
|
)
|
||||||
@@ -45,7 +45,7 @@ func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result Conve
|
|||||||
|
|
||||||
// Decode JPEG XL image if support is enabled.
|
// Decode JPEG XL image if support is enabled.
|
||||||
if f.IsJpegXL() && w.conf.JpegXLEnabled() {
|
if f.IsJpegXL() && w.conf.JpegXLEnabled() {
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.JpegXLDecoderBin(), f.FileName(), pngName)),
|
exec.Command(w.conf.JpegXLDecoderBin(), f.FileName(), pngName)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -54,14 +54,14 @@ func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result Conve
|
|||||||
// otherwise try to convert the media file with ImageMagick.
|
// otherwise try to convert the media file with ImageMagick.
|
||||||
if w.conf.RsvgConvertEnabled() && f.IsSVG() {
|
if w.conf.RsvgConvertEnabled() && f.IsSVG() {
|
||||||
args := []string{"-a", "-f", "png", "-o", pngName, f.FileName()}
|
args := []string{"-a", "-f", "png", "-o", pngName, f.FileName()}
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.RsvgConvertBin(), args...)),
|
exec.Command(w.conf.RsvgConvertBin(), args...)),
|
||||||
)
|
)
|
||||||
} else if w.conf.ImageMagickEnabled() && w.imageMagickExclude.Allow(fileExt) &&
|
} else if w.conf.ImageMagickEnabled() && w.imageMagickExclude.Allow(fileExt) &&
|
||||||
(f.IsImage() && !f.IsJpegXL() && !f.IsRaw() && !f.IsHEIF() || f.IsVector() && w.conf.VectorEnabled()) {
|
(f.IsImage() && !f.IsJpegXL() && !f.IsRaw() && !f.IsHEIF() || f.IsVector() && w.conf.VectorEnabled()) {
|
||||||
resize := fmt.Sprintf("%dx%d>", w.conf.PngSize(), w.conf.PngSize())
|
resize := fmt.Sprintf("%dx%d>", w.conf.PngSize(), w.conf.PngSize())
|
||||||
args := []string{f.FileName(), "-flatten", "-resize", resize, pngName}
|
args := []string{f.FileName(), "-flatten", "-resize", resize, pngName}
|
||||||
result = append(result, NewConvertCommand(
|
result = append(result, NewConvertCmd(
|
||||||
exec.Command(w.conf.ImageMagickBin(), args...)),
|
exec.Command(w.conf.ImageMagickBin(), args...)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ func TestConvert_ToImage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvert_PngConvertCommands(t *testing.T) {
|
func TestConvert_PngConvertCmds(t *testing.T) {
|
||||||
cnf := config.TestConfig()
|
cnf := config.TestConfig()
|
||||||
convert := NewConvert(cnf)
|
convert := NewConvert(cnf)
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ func TestConvert_PngConvertCommands(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmds, useMutex, err := convert.PngConvertCommands(mediaFile, pngFile)
|
cmds, useMutex, err := convert.PngConvertCmds(mediaFile, pngFile)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import (
|
|||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToAvc converts a single video file to MPEG-4 AVC.
|
// ToAvc converts a single video file to MPEG-4 AVC.
|
||||||
func (w *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force bool) (file *MediaFile, err error) {
|
func (w *Convert) ToAvc(f *MediaFile, encoder encode.Encoder, noMutex, force bool) (file *MediaFile, err error) {
|
||||||
// Abort if the source media file is nil.
|
// Abort if the source media file is nil.
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return nil, fmt.Errorf("convert: file is nil - you may have found a bug")
|
return nil, fmt.Errorf("convert: file is nil - you may have found a bug")
|
||||||
@@ -66,7 +66,7 @@ func (w *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
|
|||||||
avcName, _ = fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtAVC)
|
avcName, _ = fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtAVC)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, useMutex, err := w.AvcConvertCommand(f, avcName, encoder)
|
cmd, useMutex, err := w.AvcConvertCmd(f, avcName, encoder)
|
||||||
|
|
||||||
// Return if an error occurred.
|
// Return if an error occurred.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -138,8 +138,8 @@ func (w *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try again using software encoder.
|
// Try again using software encoder.
|
||||||
if encoder != ffmpeg.SoftwareEncoder {
|
if encoder != encode.SoftwareAvc {
|
||||||
return w.ToAvc(f, ffmpeg.SoftwareEncoder, true, false)
|
return w.ToAvc(f, encode.SoftwareAvc, true, false)
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -152,8 +152,8 @@ func (w *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
|
|||||||
return NewMediaFile(avcName)
|
return NewMediaFile(avcName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AvcConvertCommand returns the command for converting video files to MPEG-4 AVC.
|
// AvcConvertCmd returns the command for converting video files to MPEG-4 AVC.
|
||||||
func (w *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg.AvcEncoder) (result *exec.Cmd, useMutex bool, err error) {
|
func (w *Convert) AvcConvertCmd(f *MediaFile, avcName string, encoder encode.Encoder) (result *exec.Cmd, useMutex bool, err error) {
|
||||||
fileExt := f.Extension()
|
fileExt := f.Extension()
|
||||||
fileName := f.FileName()
|
fileName := f.FileName()
|
||||||
|
|
||||||
@@ -170,11 +170,11 @@ func (w *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use FFmpeg to transcode all other media files to AVC.
|
// Use FFmpeg to transcode all other media files to AVC.
|
||||||
var opt ffmpeg.Options
|
var opt encode.Options
|
||||||
if opt, err = w.conf.FFmpegOptions(encoder, w.AvcBitrate(f)); err != nil {
|
if opt, err = w.conf.FFmpegOptions(encoder, w.AvcBitrate(f)); err != nil {
|
||||||
return nil, false, fmt.Errorf("convert: failed to transcode %s (%s)", clean.Log(f.BaseName()), err)
|
return nil, false, fmt.Errorf("convert: failed to transcode %s (%s)", clean.Log(f.BaseName()), err)
|
||||||
} else {
|
} else {
|
||||||
return ffmpeg.AvcConvertCommand(fileName, avcName, opt)
|
return ffmpeg.AvcConvertCmd(fileName, avcName, opt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ func TestConvert_ToAvc(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
avcFile, err := convert.ToAvc(mf, ffmpeg.SoftwareEncoder, false, false)
|
avcFile, err := convert.ToAvc(mf, encode.SoftwareAvc, false, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -137,7 +137,7 @@ func TestConvert_AvcBitrate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvert_AvcConvertCommand(t *testing.T) {
|
func TestConvert_AvcConvertCmd(t *testing.T) {
|
||||||
conf := config.TestConfig()
|
conf := config.TestConfig()
|
||||||
convert := NewConvert(conf)
|
convert := NewConvert(conf)
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ func TestConvert_AvcConvertCommand(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, _, err := convert.AvcConvertCommand(mf, "avc1", ffmpeg.SoftwareEncoder)
|
r, _, err := convert.AvcConvertCmd(mf, "avc1", encode.SoftwareAvc)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -166,7 +166,7 @@ func TestConvert_AvcConvertCommand(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, useMutex, err := convert.AvcConvertCommand(mf, "avc1", ffmpeg.SoftwareEncoder)
|
r, useMutex, err := convert.AvcConvertCmd(mf, "avc1", encode.SoftwareAvc)
|
||||||
|
|
||||||
assert.False(t, useMutex)
|
assert.False(t, useMutex)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -181,7 +181,7 @@ func TestConvert_AvcConvertCommand(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, useMutex, err := convert.AvcConvertCommand(mf, avcName, ffmpeg.SoftwareEncoder)
|
r, useMutex, err := convert.AvcConvertCmd(mf, avcName, encode.SoftwareAvc)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
Reference in New Issue
Block a user