mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
FFmpeg: Refactor extraction of JPEG and PNG images from videos #4604
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package config provides global options, command-line flags, and user settings.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -77,12 +77,12 @@ func (c *Config) FFmpegMapAudio() string {
|
||||
func (c *Config) FFmpegOptions(encoder encode.Encoder, bitrate string) (encode.Options, error) {
|
||||
// Transcode all other formats with FFmpeg.
|
||||
opt := encode.Options{
|
||||
Bin: c.FFmpegBin(),
|
||||
Encoder: encoder,
|
||||
Size: c.FFmpegSize(),
|
||||
Bitrate: bitrate,
|
||||
MapVideo: c.FFmpegMapVideo(),
|
||||
MapAudio: c.FFmpegMapAudio(),
|
||||
Bin: c.FFmpegBin(),
|
||||
Encoder: encoder,
|
||||
DestSize: c.FFmpegSize(),
|
||||
DestBitrate: bitrate,
|
||||
MapVideo: c.FFmpegMapVideo(),
|
||||
MapAudio: c.FFmpegMapAudio(),
|
||||
}
|
||||
|
||||
// Check
|
||||
|
||||
@@ -109,7 +109,7 @@ 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.Bitrate)
|
||||
assert.Equal(t, bitrate, opt.DestBitrate)
|
||||
assert.Equal(t, ffmpeg.MapVideoDefault, opt.MapVideo)
|
||||
assert.Equal(t, ffmpeg.MapAudioDefault, opt.MapAudio)
|
||||
assert.Equal(t, c.FFmpegMapVideo(), opt.MapVideo)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package customize provides user settings to customize the app.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package pwa provides data structures and tools for working with progressive web applications.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package ttl provides cache expiration defaults and helper functions.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"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 {
|
||||
// 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_videotoolbox
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
@@ -22,7 +22,7 @@ func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-profile", "high",
|
||||
"-level", "51",
|
||||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-b:v", opt.DestBitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart",
|
||||
destName,
|
||||
|
||||
@@ -2,8 +2,8 @@ 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 {
|
||||
// TranscodeToAvcCmd returns the default FFmpeg command for transcoding video files to MPEG-4 AVC.
|
||||
func TranscodeToAvcCmd(srcName, destName string, opt Options) *exec.Cmd {
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-y",
|
||||
@@ -17,7 +17,7 @@ func AvcConvertCmd(srcName, destName string, opt Options) *exec.Cmd {
|
||||
"-max_muxing_queue_size", "1024",
|
||||
"-crf", "23",
|
||||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-b:v", opt.DestBitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
destName,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package encode provides FFmpeg video encoder related types and functions.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -10,7 +10,7 @@ func (name Encoder) String() string {
|
||||
return string(name)
|
||||
}
|
||||
|
||||
// Supported FFmpeg AVC encoders.
|
||||
// Currently supported FFmpeg output 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.
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package ffmpeg
|
||||
package encode
|
||||
|
||||
import "time"
|
||||
|
||||
// PreviewTimeOffset returns an appropriate time offset depending on the duration for extracting a preview image.
|
||||
// PreviewTimeOffset returns a time offset depending on the video duration for extracting a cover image,
|
||||
// see https://trac.ffmpeg.org/wiki/Seeking and https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax.
|
||||
func PreviewTimeOffset(d time.Duration) string {
|
||||
// Default.
|
||||
// Default time offset.
|
||||
result := "00:00:00.001"
|
||||
|
||||
if d <= 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// If the video is long enough, don't use the first frames to avoid completely
|
||||
// black or white thumbnails in case there is an effect or intro.
|
||||
switch {
|
||||
35
internal/ffmpeg/encode/offset_test.go
Normal file
35
internal/ffmpeg/encode/offset_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package encode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPreviewTimeOffset(t *testing.T) {
|
||||
t.Run("Second", func(t *testing.T) {
|
||||
assert.Equal(t, "00:00:00.001", PreviewTimeOffset(time.Second))
|
||||
})
|
||||
t.Run("Minute", func(t *testing.T) {
|
||||
assert.Equal(t, "00:00:03.000", PreviewTimeOffset(time.Minute))
|
||||
})
|
||||
t.Run("ThreeMinutes", func(t *testing.T) {
|
||||
assert.Equal(t, "00:00:09.000", PreviewTimeOffset(3*time.Minute))
|
||||
})
|
||||
t.Run("FiveMinutes", func(t *testing.T) {
|
||||
assert.Equal(t, "00:00:30.000", PreviewTimeOffset(5*time.Minute))
|
||||
})
|
||||
t.Run("FifteenMinutes", func(t *testing.T) {
|
||||
assert.Equal(t, "00:01:00.000", PreviewTimeOffset(15*time.Minute))
|
||||
})
|
||||
t.Run("HalfHour", func(t *testing.T) {
|
||||
assert.Equal(t, "00:01:00.000", PreviewTimeOffset(30*time.Minute))
|
||||
})
|
||||
t.Run("Hour", func(t *testing.T) {
|
||||
assert.Equal(t, "00:01:00.000", PreviewTimeOffset(time.Hour))
|
||||
})
|
||||
t.Run("ThreeHours", func(t *testing.T) {
|
||||
assert.Equal(t, "00:02:30.000", PreviewTimeOffset(3*time.Hour))
|
||||
})
|
||||
}
|
||||
@@ -1,25 +1,38 @@
|
||||
package encode
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Options represents transcoding options.
|
||||
// Options represents FFmpeg encoding options.
|
||||
type Options struct {
|
||||
Bin string
|
||||
Encoder Encoder
|
||||
Size int
|
||||
Bitrate string
|
||||
MapVideo string
|
||||
MapAudio string
|
||||
Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg.
|
||||
Encoder Encoder // Supported FFmpeg output Encoder.
|
||||
DestSize int // Maximum width and height of the output video file in pixels.
|
||||
DestBitrate 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.
|
||||
}
|
||||
|
||||
// NewPreviewImageOptions generates encoding options for extracting a video preview image.
|
||||
func NewPreviewImageOptions(ffmpegBin string, videoDuration time.Duration) *Options {
|
||||
return &Options{
|
||||
Bin: ffmpegBin,
|
||||
TimeOffset: PreviewTimeOffset(videoDuration),
|
||||
}
|
||||
}
|
||||
|
||||
// VideoFilter returns the FFmpeg video filter string based on the size limit in pixels and the pixel format.
|
||||
func (o Options) VideoFilter(format PixelFormat) string {
|
||||
func (o *Options) VideoFilter(format PixelFormat) string {
|
||||
// scale specifies the FFmpeg downscale filter, see http://trac.ffmpeg.org/wiki/Scaling.
|
||||
if format == "" {
|
||||
return fmt.Sprintf("scale='if(gte(iw,ih), min(%d, iw), -2):if(gte(iw,ih), -2, min(%d, ih))'", o.Size, o.Size)
|
||||
return fmt.Sprintf("scale='if(gte(iw,ih), min(%d, iw), -2):if(gte(iw,ih), -2, min(%d, ih))'", o.DestSize, o.DestSize)
|
||||
} else if format == FormatQSV {
|
||||
return fmt.Sprintf("scale_qsv=w='if(gte(iw,ih), min(%d, iw), -1)':h='if(gte(iw,ih), -1, min(%d, ih))':format=nv12", o.Size, o.Size)
|
||||
return fmt.Sprintf("scale_qsv=w='if(gte(iw,ih), min(%d, iw), -1)':h='if(gte(iw,ih), -1, min(%d, ih))':format=nv12", o.DestSize, o.DestSize)
|
||||
} else {
|
||||
return fmt.Sprintf("scale='if(gte(iw,ih), min(%d, iw), -2):if(gte(iw,ih), -2, min(%d, ih))',format=%s", o.Size, o.Size, format)
|
||||
return fmt.Sprintf("scale='if(gte(iw,ih), min(%d, iw), -2):if(gte(iw,ih), -2, min(%d, ih))',format=%s", o.DestSize, o.DestSize, format)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,23 @@ import (
|
||||
)
|
||||
|
||||
func TestOptions_VideoFilter(t *testing.T) {
|
||||
Options := &Options{
|
||||
Bin: "",
|
||||
Encoder: "intel",
|
||||
Size: 1500,
|
||||
Bitrate: "50M",
|
||||
MapVideo: "",
|
||||
MapAudio: "",
|
||||
opt := &Options{
|
||||
Bin: "",
|
||||
Encoder: "intel",
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: "",
|
||||
MapAudio: "",
|
||||
}
|
||||
|
||||
t.Run("rgb32", func(t *testing.T) {
|
||||
r := Options.VideoFilter("rgb32")
|
||||
assert.Contains(t, r, "format=rgb32")
|
||||
assert.Contains(t, r, "min(1500, iw)")
|
||||
})
|
||||
t.Run("empty format", func(t *testing.T) {
|
||||
r := Options.VideoFilter("")
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
r := opt.VideoFilter("")
|
||||
assert.NotContains(t, r, "format")
|
||||
assert.Contains(t, r, "min(1500, iw)")
|
||||
})
|
||||
t.Run("Rgb32", func(t *testing.T) {
|
||||
r := opt.VideoFilter("rgb32")
|
||||
assert.Contains(t, r, "format=rgb32")
|
||||
assert.Contains(t, r, "min(1500, iw)")
|
||||
})
|
||||
}
|
||||
|
||||
35
internal/ffmpeg/extract_image_cmd.go
Normal file
35
internal/ffmpeg/extract_image_cmd.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
)
|
||||
|
||||
// ExtractImageCmd extracts a still image from the specified source video file.
|
||||
func ExtractImageCmd(videoName, imageName string, opt *encode.Options) *exec.Cmd {
|
||||
imageExt := strings.ToLower(filepath.Ext(imageName))
|
||||
|
||||
switch imageExt {
|
||||
case ".png":
|
||||
return ExtractPngImageCmd(videoName, imageName, opt)
|
||||
default:
|
||||
return ExtractJpegImageCmd(videoName, imageName, opt)
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractJpegImageCmd extracts a JPEG still image from the specified source video file.
|
||||
func ExtractJpegImageCmd(videoName, imageName string, opt *encode.Options) *exec.Cmd {
|
||||
// TODO: Adjust command flags for correct colors with HDR10-encoded HEVC videos,
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
63
internal/ffmpeg/extract_image_cmd_test.go
Normal file
63
internal/ffmpeg/extract_image_cmd_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
func TestExtractImageCmd(t *testing.T) {
|
||||
opt := encode.NewPreviewImageOptions("/usr/bin/ffmpeg", time.Second*9)
|
||||
|
||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||
destName := fs.Abs("./testdata/25fps.jpg")
|
||||
|
||||
cmd := ExtractImageCmd(srcName, destName, opt)
|
||||
|
||||
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 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
||||
|
||||
RunCommandTest(t, "jpg", srcName, destName, cmd, true)
|
||||
}
|
||||
|
||||
func TestExtractJpegImageCmd(t *testing.T) {
|
||||
opt := encode.NewPreviewImageOptions("/usr/bin/ffmpeg", time.Second*9)
|
||||
|
||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||
destName := fs.Abs("./testdata/25fps.jpeg")
|
||||
|
||||
cmd := ExtractJpegImageCmd(srcName, destName, opt)
|
||||
|
||||
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 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
||||
|
||||
RunCommandTest(t, "jpeg", srcName, destName, cmd, true)
|
||||
}
|
||||
|
||||
func TestExtractPngImageCmd(t *testing.T) {
|
||||
opt := encode.NewPreviewImageOptions("/usr/bin/ffmpeg", time.Second*9)
|
||||
|
||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||
destName := fs.Abs("./testdata/25fps.png")
|
||||
|
||||
cmd := ExtractPngImageCmd(srcName, destName, opt)
|
||||
|
||||
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 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr)
|
||||
|
||||
RunCommandTest(t, "png", srcName, destName, cmd, true)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package ffmpeg provides FFmpeg video transcoding related types and functions.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"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 {
|
||||
// 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,
|
||||
@@ -23,8 +23,8 @@ func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-bitrate", opt.Bitrate,
|
||||
"-b:v", opt.DestBitrate,
|
||||
"-bitrate", opt.DestBitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
destName,
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"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 {
|
||||
// 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_nvenc
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
@@ -28,7 +28,7 @@ func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-cq", "0",
|
||||
"-tune", "2",
|
||||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-b:v", opt.DestBitrate,
|
||||
"-profile:v", "1",
|
||||
"-level:v", "auto",
|
||||
"-coder:v", "1",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPreviewTimeOffset(t *testing.T) {
|
||||
assert.Equal(t, "00:00:00.001", PreviewTimeOffset(time.Second))
|
||||
assert.Equal(t, "00:00:03.000", PreviewTimeOffset(time.Minute))
|
||||
assert.Equal(t, "00:00:09.000", PreviewTimeOffset(3*time.Minute))
|
||||
assert.Equal(t, "00:00:30.000", PreviewTimeOffset(5*time.Minute))
|
||||
assert.Equal(t, "00:01:00.000", PreviewTimeOffset(15*time.Minute))
|
||||
assert.Equal(t, "00:01:00.000", PreviewTimeOffset(30*time.Minute))
|
||||
assert.Equal(t, "00:01:00.000", PreviewTimeOffset(time.Hour))
|
||||
assert.Equal(t, "00:02:30.000", PreviewTimeOffset(3*time.Hour))
|
||||
}
|
||||
58
internal/ffmpeg/test.go
Normal file
58
internal/ffmpeg/test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
func RunCommandTest(t *testing.T, encoder encode.Encoder, srcName, destName string, cmd *exec.Cmd, deleteAfterTest bool) {
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = append(cmd.Env, []string{
|
||||
fmt.Sprintf("HOME=%s", fs.Abs("./testdata")),
|
||||
}...)
|
||||
|
||||
// Transcode source media file to AVC.
|
||||
start := time.Now()
|
||||
if err := cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
err = errors.New(stderr.String())
|
||||
}
|
||||
|
||||
// Remove broken video file.
|
||||
if !deleteAfterTest || !fs.FileExists(destName) {
|
||||
// Do nothing.
|
||||
} 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))
|
||||
|
||||
// Return if destination file should not be deleted.
|
||||
if !deleteAfterTest {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete destination file after test.
|
||||
if removeErr := os.Remove(destName); removeErr != nil {
|
||||
t.Fatalf("%s: failed to remove %s after successful test (%s)", encoder, srcName, removeErr)
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// AvcConvertCmd returns the command for converting video files to MPEG-4 AVC.
|
||||
func AvcConvertCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd, useMutex bool, err error) {
|
||||
// TranscodeCmd returns the FFmpeg command for transcoding existing video files to MPEG-4 AVC.
|
||||
func TranscodeCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd, useMutex bool, err error) {
|
||||
if srcName == "" {
|
||||
return nil, false, fmt.Errorf("empty source filename")
|
||||
} else if destName == "" {
|
||||
@@ -24,7 +24,7 @@ func AvcConvertCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd,
|
||||
// Don't transcode more than one video at the same time.
|
||||
useMutex = true
|
||||
|
||||
// Use default ffmpeg command name.
|
||||
// Use default FFmpeg command name.
|
||||
if opt.Bin == "" {
|
||||
opt.Bin = DefaultBin
|
||||
}
|
||||
@@ -53,22 +53,22 @@ func AvcConvertCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd,
|
||||
|
||||
switch opt.Encoder {
|
||||
case encode.IntelAvc:
|
||||
cmd = intel.AvcConvertCmd(srcName, destName, opt)
|
||||
cmd = intel.TranscodeToAvcCmd(srcName, destName, opt)
|
||||
|
||||
case encode.AppleAvc:
|
||||
cmd = apple.AvcConvertCmd(srcName, destName, opt)
|
||||
cmd = apple.TranscodeToAvcCmd(srcName, destName, opt)
|
||||
|
||||
case encode.VaapiAvc:
|
||||
cmd = vaapi.AvcConvertCmd(srcName, destName, opt)
|
||||
cmd = vaapi.TranscodeToAvcCmd(srcName, destName, opt)
|
||||
|
||||
case encode.NvidiaAvc:
|
||||
cmd = nvidia.AvcConvertCmd(srcName, destName, opt)
|
||||
cmd = nvidia.TranscodeToAvcCmd(srcName, destName, opt)
|
||||
|
||||
case encode.V4LAvc:
|
||||
cmd = v4l.AvcConvertCmd(srcName, destName, opt)
|
||||
cmd = v4l.TranscodeToAvcCmd(srcName, destName, opt)
|
||||
|
||||
default:
|
||||
cmd = encode.AvcConvertCmd(srcName, destName, opt)
|
||||
cmd = encode.TranscodeToAvcCmd(srcName, destName, opt)
|
||||
}
|
||||
|
||||
return cmd, useMutex, nil
|
||||
@@ -1,14 +1,8 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@@ -16,81 +10,43 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
func TestAvcConvertCmd(t *testing.T) {
|
||||
runCmd := func(t *testing.T, encoder encode.Encoder, srcName, destName string, cmd *exec.Cmd) {
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = append(cmd.Env, []string{
|
||||
fmt.Sprintf("HOME=%s", fs.Abs("./testdata")),
|
||||
}...)
|
||||
|
||||
// Transcode source media file to AVC.
|
||||
start := time.Now()
|
||||
if err := cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
err = errors.New(stderr.String())
|
||||
}
|
||||
|
||||
// Remove broken video file.
|
||||
if !fs.FileExists(destName) {
|
||||
// Do nothing.
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscodeCmd(t *testing.T) {
|
||||
t.Run("NoSource", func(t *testing.T) {
|
||||
opt := encode.Options{
|
||||
Bin: "",
|
||||
Encoder: "intel",
|
||||
Size: 1500,
|
||||
Bitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
Bin: "",
|
||||
Encoder: "intel",
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
_, _, err := AvcConvertCmd("", "", opt)
|
||||
_, _, err := TranscodeCmd("", "", 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,
|
||||
Bin: "",
|
||||
Encoder: "intel",
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
_, _, err := AvcConvertCmd("VID123.mov", "", opt)
|
||||
_, _, err := TranscodeCmd("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,
|
||||
Bin: "",
|
||||
Encoder: "intel",
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
r, _, err := AvcConvertCmd("VID123.gif", "VID123.gif.avc", opt)
|
||||
r, _, err := TranscodeCmd("VID123.gif", "VID123.gif.avc", opt)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -98,22 +54,22 @@ func TestAvcConvertCmd(t *testing.T) {
|
||||
|
||||
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("VP9", func(t *testing.T) {
|
||||
t.Run("VP9toAVC", func(t *testing.T) {
|
||||
encoder := encode.SoftwareAvc
|
||||
|
||||
opt := encode.Options{
|
||||
Bin: "/usr/bin/ffmpeg",
|
||||
Encoder: encoder,
|
||||
Size: 1500,
|
||||
Bitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
Bin: "/usr/bin/ffmpeg",
|
||||
Encoder: encoder,
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
|
||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||
destName := fs.Abs("./testdata/25fps.avc")
|
||||
|
||||
cmd, _, err := AvcConvertCmd(srcName, destName, opt)
|
||||
cmd, _, err := TranscodeCmd(srcName, destName, opt)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -125,24 +81,24 @@ func TestAvcConvertCmd(t *testing.T) {
|
||||
|
||||
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)
|
||||
// Run generated command to test software transcoding.
|
||||
RunCommandTest(t, encoder, srcName, destName, cmd, true)
|
||||
})
|
||||
t.Run("Vaapi", func(t *testing.T) {
|
||||
encoder := encode.VaapiAvc
|
||||
opt := encode.Options{
|
||||
Bin: "/usr/bin/ffmpeg",
|
||||
Encoder: encoder,
|
||||
Size: 1500,
|
||||
Bitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
Bin: "/usr/bin/ffmpeg",
|
||||
Encoder: encoder,
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
|
||||
srcName := fs.Abs("./testdata/25fps.vp9")
|
||||
destName := fs.Abs("./testdata/25fps.vaapi.avc")
|
||||
|
||||
cmd, _, err := AvcConvertCmd(srcName, destName, opt)
|
||||
cmd, _, err := TranscodeCmd(srcName, destName, opt)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -154,19 +110,19 @@ func TestAvcConvertCmd(t *testing.T) {
|
||||
|
||||
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)
|
||||
// Running the generated command to test vaapi transcoding requires a compatible device.
|
||||
// RunCommandTest(t, encoder, srcName, destName, cmd, true)
|
||||
})
|
||||
t.Run("QSV", func(t *testing.T) {
|
||||
opt := encode.Options{
|
||||
Bin: "",
|
||||
Encoder: "h264_qsv",
|
||||
Size: 1500,
|
||||
Bitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
Bin: "",
|
||||
Encoder: "h264_qsv",
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -176,14 +132,14 @@ func TestAvcConvertCmd(t *testing.T) {
|
||||
})
|
||||
t.Run("Apple", func(t *testing.T) {
|
||||
opt := encode.Options{
|
||||
Bin: "",
|
||||
Encoder: "h264_videotoolbox",
|
||||
Size: 1500,
|
||||
Bitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
Bin: "",
|
||||
Encoder: "h264_videotoolbox",
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -193,14 +149,14 @@ func TestAvcConvertCmd(t *testing.T) {
|
||||
})
|
||||
t.Run("Nvidia", func(t *testing.T) {
|
||||
opt := encode.Options{
|
||||
Bin: "",
|
||||
Encoder: "h264_nvenc",
|
||||
Size: 1500,
|
||||
Bitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
Bin: "",
|
||||
Encoder: "h264_nvenc",
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -210,14 +166,14 @@ func TestAvcConvertCmd(t *testing.T) {
|
||||
})
|
||||
t.Run("Video4Linux", func(t *testing.T) {
|
||||
opt := encode.Options{
|
||||
Bin: "",
|
||||
Encoder: "h264_v4l2m2m",
|
||||
Size: 1500,
|
||||
Bitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
Bin: "",
|
||||
Encoder: "h264_v4l2m2m",
|
||||
DestSize: 1500,
|
||||
DestBitrate: "50M",
|
||||
MapVideo: MapVideoDefault,
|
||||
MapAudio: MapAudioDefault,
|
||||
}
|
||||
r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||
r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"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 {
|
||||
// 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_v4l2m2m
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
@@ -24,7 +24,7 @@ func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-max_muxing_queue_size", "1024",
|
||||
"-crf", "23",
|
||||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-b:v", opt.DestBitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
destName,
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"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 {
|
||||
// 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",
|
||||
@@ -20,7 +20,7 @@ func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-b:v", opt.DestBitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
destName,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package backup provides backup and restore functions for databases and albums.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
)
|
||||
|
||||
// JpegConvertCmds returns the supported commands for converting a MediaFile to JPEG, sorted by priority.
|
||||
@@ -30,15 +31,8 @@ func (w *Convert) JpegConvertCmds(f *MediaFile, jpegName string, xmpName string)
|
||||
|
||||
// Extract a video still image for use as a thumbnail (poster image).
|
||||
if f.IsAnimated() && !f.IsWebP() && w.conf.FFmpegEnabled() {
|
||||
timeOffset := ffmpeg.PreviewTimeOffset(f.Duration())
|
||||
|
||||
// TODO: Adjust command flags for correct colors with HDR10-encoded HEVC videos,
|
||||
// see https://github.com/photoprism/photoprism/issues/4488
|
||||
result = append(result, NewConvertCmd(
|
||||
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:
|
||||
// "-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",
|
||||
jpegName)),
|
||||
ffmpeg.ExtractJpegImageCmd(f.FileName(), jpegName, encode.NewPreviewImageOptions(w.conf.FFmpegBin(), f.Duration()))),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
)
|
||||
|
||||
// PngConvertCmds returns commands for converting a media file to PNG, if possible.
|
||||
@@ -31,7 +32,7 @@ func (w *Convert) PngConvertCmds(f *MediaFile, pngName string) (result ConvertCm
|
||||
if f.IsAnimated() && !f.IsWebP() && w.conf.FFmpegEnabled() {
|
||||
// Use "ffmpeg" to extract a PNG still image from the video.
|
||||
result = append(result, NewConvertCmd(
|
||||
exec.Command(w.conf.FFmpegBin(), "-y", "-strict", "-2", "-ss", ffmpeg.PreviewTimeOffset(f.Duration()), "-i", f.FileName(), "-vframes", "1", pngName)),
|
||||
ffmpeg.ExtractPngImageCmd(f.FileName(), pngName, encode.NewPreviewImageOptions(w.conf.FFmpegBin(), f.Duration()))),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ func (w *Convert) ToAvc(f *MediaFile, encoder encode.Encoder, noMutex, force boo
|
||||
avcName, _ = fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtAVC)
|
||||
}
|
||||
|
||||
cmd, useMutex, err := w.AvcConvertCmd(f, avcName, encoder)
|
||||
cmd, useMutex, err := w.TranscodeToAvcCmd(f, avcName, encoder)
|
||||
|
||||
// Return if an error occurred.
|
||||
if err != nil {
|
||||
@@ -152,8 +152,8 @@ func (w *Convert) ToAvc(f *MediaFile, encoder encode.Encoder, noMutex, force boo
|
||||
return NewMediaFile(avcName)
|
||||
}
|
||||
|
||||
// AvcConvertCmd returns the command for converting video files to MPEG-4 AVC.
|
||||
func (w *Convert) AvcConvertCmd(f *MediaFile, avcName string, encoder encode.Encoder) (result *exec.Cmd, useMutex bool, err error) {
|
||||
// TranscodeToAvcCmd returns the command for converting video files to MPEG-4 AVC.
|
||||
func (w *Convert) TranscodeToAvcCmd(f *MediaFile, avcName string, encoder encode.Encoder) (result *exec.Cmd, useMutex bool, err error) {
|
||||
fileExt := f.Extension()
|
||||
fileName := f.FileName()
|
||||
|
||||
@@ -174,7 +174,7 @@ func (w *Convert) AvcConvertCmd(f *MediaFile, avcName string, encoder encode.Enc
|
||||
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)
|
||||
} else {
|
||||
return ffmpeg.AvcConvertCmd(fileName, avcName, opt)
|
||||
return ffmpeg.TranscodeCmd(fileName, avcName, opt)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestConvert_AvcBitrate(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
convert := NewConvert(conf)
|
||||
|
||||
t.Run("low", func(t *testing.T) {
|
||||
t.Run("Low", func(t *testing.T) {
|
||||
fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4")
|
||||
|
||||
assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName)
|
||||
@@ -85,7 +85,7 @@ func TestConvert_AvcBitrate(t *testing.T) {
|
||||
assert.Equal(t, "1M", convert.AvcBitrate(mf))
|
||||
})
|
||||
|
||||
t.Run("medium", func(t *testing.T) {
|
||||
t.Run("Medium", func(t *testing.T) {
|
||||
fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4")
|
||||
|
||||
assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName)
|
||||
@@ -102,7 +102,7 @@ func TestConvert_AvcBitrate(t *testing.T) {
|
||||
assert.Equal(t, "16M", convert.AvcBitrate(mf))
|
||||
})
|
||||
|
||||
t.Run("high", func(t *testing.T) {
|
||||
t.Run("High", func(t *testing.T) {
|
||||
fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4")
|
||||
|
||||
assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName)
|
||||
@@ -119,7 +119,7 @@ func TestConvert_AvcBitrate(t *testing.T) {
|
||||
assert.Equal(t, "25M", convert.AvcBitrate(mf))
|
||||
})
|
||||
|
||||
t.Run("very_high", func(t *testing.T) {
|
||||
t.Run("VeryHigh", func(t *testing.T) {
|
||||
fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4")
|
||||
|
||||
assert.Truef(t, fs.FileExists(fileName), "input file does not exist: %s", fileName)
|
||||
@@ -137,7 +137,7 @@ func TestConvert_AvcBitrate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvert_AvcConvertCmd(t *testing.T) {
|
||||
func TestConvert_TranscodeToAvcCmd(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
convert := NewConvert(conf)
|
||||
|
||||
@@ -149,7 +149,7 @@ func TestConvert_AvcConvertCmd(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, _, err := convert.AvcConvertCmd(mf, "avc1", encode.SoftwareAvc)
|
||||
r, _, err := convert.TranscodeToAvcCmd(mf, "avc1", encode.SoftwareAvc)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -158,7 +158,7 @@ func TestConvert_AvcConvertCmd(t *testing.T) {
|
||||
assert.Contains(t, r.Path, "ffmpeg")
|
||||
assert.Contains(t, r.Args, "mp4")
|
||||
})
|
||||
t.Run("JPEG", func(t *testing.T) {
|
||||
t.Run("Jpeg", func(t *testing.T) {
|
||||
fileName := filepath.Join(conf.ExamplesPath(), "cat_black.jpg")
|
||||
mf, err := NewMediaFile(fileName)
|
||||
|
||||
@@ -166,7 +166,7 @@ func TestConvert_AvcConvertCmd(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, useMutex, err := convert.AvcConvertCmd(mf, "avc1", encode.SoftwareAvc)
|
||||
r, useMutex, err := convert.TranscodeToAvcCmd(mf, "avc1", encode.SoftwareAvc)
|
||||
|
||||
assert.False(t, useMutex)
|
||||
assert.Error(t, err)
|
||||
@@ -181,7 +181,7 @@ func TestConvert_AvcConvertCmd(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, useMutex, err := convert.AvcConvertCmd(mf, avcName, encode.SoftwareAvc)
|
||||
r, useMutex, err := convert.TranscodeToAvcCmd(mf, avcName, encode.SoftwareAvc)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package get provides a registry for common services.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package get provides a registry for common application services.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Package photoprism provides the core functionality of PhotoPrism®.
|
||||
|
||||
Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved.
|
||||
Copyright (c) 2018 - 2025 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"):
|
||||
|
||||
Reference in New Issue
Block a user