Video: Refactor FFmpeg Transcoding Size Limit #3466 #3498 #3549

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-07-18 15:15:04 +02:00
parent 8c0955dd41
commit 3cf1c699df
23 changed files with 405 additions and 146 deletions

View file

@ -3872,6 +3872,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/arraybuffer.prototype.slice": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz",
"integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==",
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"get-intrinsic": "^1.2.1",
"is-array-buffer": "^3.0.2",
"is-shared-array-buffer": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@ -5619,9 +5638,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.461",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.461.tgz",
"integrity": "sha512-1JkvV2sgEGTDXjdsaQCeSwYYuhLRphRpc+g6EHTFELJXEiznLt3/0pZ9JuAOQ5p2rI3YxKTbivtvajirIfhrEQ=="
"version": "1.4.463",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.463.tgz",
"integrity": "sha512-fT3hvdUWLjDbaTGzyOjng/CQhQJSQP8ThO3XZAoaxHvHo2kUXiRQVMj9M235l8uDFiNPsPa6KHT1p3RaR6ugRw=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@ -5717,11 +5736,12 @@
}
},
"node_modules/es-abstract": {
"version": "1.21.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.3.tgz",
"integrity": "sha512-ZU4miiY1j3sGPFLJ34VJXEqhpmL+HGByCinGHv4HC+Fxl2fI2Z4yR6tl0mORnDr6PA8eihWo4LmSWDbvhALckg==",
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz",
"integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==",
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"arraybuffer.prototype.slice": "^1.0.1",
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
"es-set-tostringtag": "^2.0.1",
@ -5748,10 +5768,13 @@
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.5.0",
"safe-array-concat": "^1.0.0",
"safe-regex-test": "^1.0.0",
"string.prototype.trim": "^1.2.7",
"string.prototype.trimend": "^1.0.6",
"string.prototype.trimstart": "^1.0.6",
"typed-array-buffer": "^1.0.0",
"typed-array-byte-length": "^1.0.0",
"typed-array-byte-offset": "^1.0.0",
"typed-array-length": "^1.0.4",
"unbox-primitive": "^1.0.2",
@ -8454,15 +8477,11 @@
}
},
"node_modules/is-typed-array": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
"integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
"dependencies": {
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"has-tostringtag": "^1.0.0"
"which-typed-array": "^1.1.11"
},
"engines": {
"node": ">= 0.4"
@ -8493,6 +8512,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
},
"node_modules/isbinaryfile": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
@ -12153,6 +12177,23 @@
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
},
"node_modules/safe-array-concat": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz",
"integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.2.0",
"has-symbols": "^1.0.3",
"isarray": "^2.0.5"
},
"engines": {
"node": ">=0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -13113,9 +13154,9 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/terser": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.0.tgz",
"integrity": "sha512-JpcpGOQLOXm2jsomozdMDpd5f8ZHh1rR48OFgWUH3QsyZcfPgv2qDCYbcDEAYNd4OZRj2bWYKpwdll/udZCk/Q==",
"version": "5.19.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.1.tgz",
"integrity": "sha512-27hxBUVdV6GoNg1pKQ7Z5cbR6V9txPVyBA+FQw3BaZ1Wuzvztce5p156DaP0NVZNrMZZ+6iG9Syf7WgMNKDg2Q==",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@ -13413,6 +13454,36 @@
"node": ">= 0.6"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
"integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.2.1",
"is-typed-array": "^1.1.10"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/typed-array-byte-length": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
"integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
"dependencies": {
"call-bind": "^1.0.2",
"for-each": "^0.3.3",
"has-proto": "^1.0.1",
"is-typed-array": "^1.1.10"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typed-array-byte-offset": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
@ -14077,9 +14148,9 @@
}
},
"node_modules/webpack": {
"version": "5.88.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz",
"integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==",
"version": "5.88.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz",
"integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0",
@ -14431,16 +14502,15 @@
}
},
"node_modules/which-typed-array": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.10.tgz",
"integrity": "sha512-uxoA5vLUfRPdjCuJ1h5LlYdmTLbYfums398v3WLkM+i/Wltl2/XyZpQWKbN++ck5L64SR/grOHqtXCUKmlZPNA==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
"integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
"dependencies": {
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"has-tostringtag": "^1.0.0",
"is-typed-array": "^1.1.10"
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"

View file

@ -408,7 +408,7 @@ export default {
for (let i = 0; i < thumbs.length; i++) {
let t = thumbs[i];
result.push({"text": t.w + 'x' + t.h, "value": t.size});
result.push({"text": t.w + ' × ' + t.h, "value": t.size});
}
return result;

View file

@ -238,48 +238,14 @@ const clientConfig = {
},
],
thumbs: [
{
size: "fit_720",
use: "Mobile, TV",
w: 720,
h: 720,
},
{
size: "fit_1280",
use: "Mobile, HD Ready TV",
w: 1280,
h: 1024,
},
{
size: "fit_1920",
use: "Mobile, Full HD TV",
w: 1920,
h: 1200,
},
{
size: "fit_2048",
use: "Tablets, Cinema 2K",
w: 2048,
h: 2048,
},
{
size: "fit_2560",
use: "Quad HD, Retina Display",
w: 2560,
h: 1600,
},
{
size: "fit_4096",
use: "Ultra HD, Retina 4K",
w: 4096,
h: 4096,
},
{
size: "fit_7680",
use: "8K Ultra HD 2, Retina 6K",
w: 7680,
h: 4320,
},
{ size: "fit_720", usage: "SD TV, Mobile", w: 720, h: 720 },
{ size: "fit_1280", usage: "HD TV, SXGA", w: 1280, h: 1024 },
{ size: "fit_1920", usage: "Full HD", w: 1920, h: 1200 },
{ size: "fit_2048", usage: "DCI 2K, Tablets", w: 2048, h: 2048 },
{ size: "fit_2560", usage: "Quad HD, Notebooks", w: 2560, h: 1600 },
{ size: "fit_3840", usage: "4K Ultra HD", w: 3840, h: 2400 },
{ size: "fit_4096", usage: "DCI 4K, Retina 4K", w: 4096, h: 4096 },
{ size: "fit_7680", usage: "8K Ultra HD 2", w: 7680, h: 4320 },
],
status: "unregistered",
mapKey: "D9ve6edlcVR2mEsNvCXa",

View file

@ -14,6 +14,8 @@ var ShowCommand = cli.Command{
ShowConfigYamlCommand,
ShowSearchFiltersCommand,
ShowFileFormatsCommand,
ShowThumbSizesCommand,
ShowVideoSizesCommand,
ShowMetadataCommand,
},
}

View file

@ -0,0 +1,29 @@
package commands
import (
"fmt"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/report"
)
// ShowThumbSizesCommand configures the command name, flags, and action.
var ShowThumbSizesCommand = cli.Command{
Name: "thumb-sizes",
Usage: "Displays supported standard thumbnail sizes",
Flags: report.CliFlags,
Action: showThumbSizesAction,
}
// showThumbSizesAction displays supported standard thumbnail sizes.
func showThumbSizesAction(ctx *cli.Context) error {
rows, cols := thumb.Report(thumb.Sizes.All(), false)
result, err := report.RenderFormat(rows, cols, report.CliFormat(ctx))
fmt.Println(result)
return err
}

View file

@ -0,0 +1,29 @@
package commands
import (
"fmt"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/report"
)
// ShowVideoSizesCommand configures the command name, flags, and action.
var ShowVideoSizesCommand = cli.Command{
Name: "video-sizes",
Usage: "Displays supported standard video sizes",
Flags: report.CliFlags,
Action: showVideoSizesAction,
}
// showVideoSizesAction displays supported standard video sizes.
func showVideoSizesAction(ctx *cli.Context) error {
rows, cols := thumb.Report(thumb.VideoSizes, true)
result, err := report.RenderFormat(rows, cols, report.CliFormat(ctx))
fmt.Println(result)
return err
}

View file

@ -73,7 +73,7 @@ func init() {
t := thumb.Sizes[name]
if t.Public {
Thumbs = append(Thumbs, ThumbSize{Size: string(name), Use: t.Use, Width: t.Width, Height: t.Height})
Thumbs = append(Thumbs, ThumbSize{Size: string(name), Usage: t.Usage, Width: t.Width, Height: t.Height})
}
}
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/photoprism/photoprism/internal/ffmpeg"
"github.com/photoprism/photoprism/internal/thumb"
)
// FFmpegBin returns the ffmpeg executable file name.
@ -25,6 +26,11 @@ func (c *Config) FFmpegEncoder() ffmpeg.AvcEncoder {
return ffmpeg.FindEncoder(c.options.FFmpegEncoder)
}
// FFmpegSize returns the maximum ffmpeg video encoding size in pixels (720-7680).
func (c *Config) FFmpegSize() int {
return thumb.VideoSize(c.options.FFmpegSize).Width
}
// FFmpegBitrate returns the ffmpeg bitrate limit in MBit/s.
func (c *Config) FFmpegBitrate() int {
switch {
@ -37,18 +43,6 @@ func (c *Config) FFmpegBitrate() int {
}
}
// FFmpegResolution returns the ffmpeg resolution limit in pixel height. Goes from 144p to 8k.
func (c *Config) FFmpegResolution() int {
switch {
case c.options.FFmpegResolution <= 0:
return 4096
case c.options.FFmpegResolution >= 8192:
return 8192
default:
return c.options.FFmpegResolution
}
}
// FFmpegBitrateExceeded tests if the ffmpeg bitrate limit is exceeded.
func (c *Config) FFmpegBitrateExceeded(mbit float64) bool {
if mbit <= 0 {
@ -82,12 +76,12 @@ func (c *Config) FFmpegMapAudio() string {
func (c *Config) FFmpegOptions(encoder ffmpeg.AvcEncoder, bitrate string) (ffmpeg.Options, error) {
// Transcode all other formats with FFmpeg.
opt := ffmpeg.Options{
Bin: c.FFmpegBin(),
Encoder: encoder,
Bitrate: bitrate,
MapVideo: c.FFmpegMapVideo(),
MapAudio: c.FFmpegMapAudio(),
Resolution: fmt.Sprintf("%v", c.FFmpegResolution()),
Bin: c.FFmpegBin(),
Encoder: encoder,
Size: c.FFmpegSize(),
Bitrate: bitrate,
MapVideo: c.FFmpegMapVideo(),
MapAudio: c.FFmpegMapAudio(),
}
// Check

View file

@ -4,6 +4,7 @@ import (
"testing"
"github.com/photoprism/photoprism/internal/ffmpeg"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/stretchr/testify/assert"
)
@ -43,15 +44,30 @@ func TestConfig_FFmpegBitrate(t *testing.T) {
assert.Equal(t, 800, c.FFmpegBitrate())
}
func TestConfig_FFmpegResolution(t *testing.T) {
func TestConfig_FFmpegSize(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, 4096, c.FFmpegResolution())
assert.Equal(t, 3840, c.FFmpegSize())
c.options.FFmpegResolution = 1920
assert.Equal(t, 1920, c.FFmpegResolution())
c.options.FFmpegSize = 0
assert.Equal(t, 3840, c.FFmpegSize())
c.options.FFmpegResolution = 8640
assert.Equal(t, 8192, c.FFmpegResolution())
c.options.FFmpegSize = -1
assert.Equal(t, 7680, c.FFmpegSize())
c.options.FFmpegSize = 10
assert.Equal(t, 720, c.FFmpegSize())
c.options.FFmpegSize = 720
assert.Equal(t, 720, c.FFmpegSize())
c.options.FFmpegSize = 1920
assert.Equal(t, 1920, c.FFmpegSize())
c.options.FFmpegSize = 4000
assert.Equal(t, 3840, c.FFmpegSize())
c.options.FFmpegSize = 8640
assert.Equal(t, thumb.Sizes[thumb.Fit7680].Width, c.FFmpegSize())
}
func TestConfig_FFmpegBitrateExceeded(t *testing.T) {

View file

@ -580,16 +580,16 @@ var Flags = CliFlags{
EnvVar: EnvVar("FFMPEG_ENCODER"),
}}, {
Flag: cli.IntFlag{
Name: "ffmpeg-bitrate, vb",
Usage: "maximum FFmpeg encoding `BITRATE` (Mbit/s)",
Value: 50,
EnvVar: EnvVar("FFMPEG_BITRATE"),
Name: "ffmpeg-size, vs",
Usage: "maximum FFmpeg encoding size in `PIXELS` (720-7680)",
Value: thumb.Sizes[thumb.Fit3840].Width,
EnvVar: EnvVar("FFMPEG_SIZE"),
}}, {
Flag: cli.IntFlag{
Name: "ffmpeg-resolution",
Usage: "maximum FFmpeg encoding `RESOLUTION` (height)",
Value: 4096,
EnvVar: EnvVar("FFMPEG_RESOLUTION"),
Name: "ffmpeg-bitrate, vb",
Usage: "maximum FFmpeg video `BITRATE` in Mbit/s",
Value: 50,
EnvVar: EnvVar("FFMPEG_BITRATE"),
}}, {
Flag: cli.StringFlag{
Name: "ffmpeg-map-video",

View file

@ -129,8 +129,8 @@ type Options struct {
SipsBlacklist string `yaml:"SipsBlacklist" json:"-" flag:"sips-blacklist"`
FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"`
FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"`
FFmpegSize int `yaml:"FFmpegSize" json:"FFmpegSize" flag:"ffmpeg-size"`
FFmpegBitrate int `yaml:"FFmpegBitrate" json:"FFmpegBitrate" flag:"ffmpeg-bitrate"`
FFmpegResolution int `yaml:"FFmpegResolution" json:"FFmpegResolution" flag:"ffmpeg-resolution"`
FFmpegMapVideo string `yaml:"FFmpegMapVideo" json:"FFmpegMapVideo" flag:"ffmpeg-map-video"`
FFmpegMapAudio string `yaml:"FFmpegMapAudio" json:"FFmpegMapAudio" flag:"ffmpeg-map-audio"`
ExifToolBin string `yaml:"ExifToolBin" json:"-" flag:"exiftool-bin"`

View file

@ -184,8 +184,8 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"sips-blacklist", c.SipsBlacklist()},
{"ffmpeg-bin", c.FFmpegBin()},
{"ffmpeg-encoder", c.FFmpegEncoder().String()},
{"ffmpeg-size", fmt.Sprintf("%d", c.FFmpegSize())},
{"ffmpeg-bitrate", fmt.Sprintf("%d", c.FFmpegBitrate())},
{"ffmpeg-resolution", fmt.Sprintf("%d", c.FFmpegResolution())},
{"ffmpeg-map-video", c.FFmpegMapVideo()},
{"ffmpeg-map-audio", c.FFmpegMapAudio()},
{"exiftool-bin", c.ExifToolBin()},

View file

@ -3,7 +3,7 @@ package config
// ThumbSize represents thumbnail info for use in client apps.
type ThumbSize struct {
Size string `json:"size"`
Use string `json:"use"`
Usage string `json:"usage"`
Width int `json:"w"`
Height int `json:"h"`
}

View file

@ -1,11 +1,23 @@
package ffmpeg
import "fmt"
// Options represents transcoding options.
type Options struct {
Bin string
Encoder AvcEncoder
Bitrate string
MapVideo string
MapAudio string
Resolution string
Bin string
Encoder AvcEncoder
Size int
Bitrate string
MapVideo string
MapAudio string
}
// 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 {
// 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)
} 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)
}
}

View file

@ -15,8 +15,6 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
return nil, false, fmt.Errorf("empty output filename")
}
scale := "scale='if(gte(iw,ih), min(" + opt.Resolution + ", iw), -2):if(gte(iw,ih), -2, min(" + opt.Resolution + ", ih))'"
// Don't transcode more than one video at the same time.
useMutex = true
@ -26,7 +24,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
opt.Bin,
"-i", fileName,
"-movflags", "faststart",
"-pix_fmt", "yuv420p",
"-pix_fmt", FormatYUV420P.String(),
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
"-f", "mp4",
"-y",
@ -44,13 +42,12 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
switch opt.Encoder {
case IntelEncoder:
// ffmpeg -hide_banner -h encoder=h264_qsv
format := "format=rgb32"
result = exec.Command(
opt.Bin,
"-qsv_device", "/dev/dri/renderD128",
"-i", fileName,
"-c:a", "aac",
"-vf", scale+", "+format+"",
"-vf", opt.VideoFilter(FormatRGB32),
"-c:v", opt.Encoder.String(),
"-map", opt.MapVideo,
"-map", opt.MapAudio,
@ -65,7 +62,6 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
case AppleEncoder:
// ffmpeg -hide_banner -h encoder=h264_videotoolbox
format := "format=yuv420p"
result = exec.Command(
opt.Bin,
"-i", fileName,
@ -73,7 +69,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
"-map", opt.MapVideo,
"-map", opt.MapAudio,
"-c:a", "aac",
"-vf", scale+", "+format+"",
"-vf", opt.VideoFilter(FormatYUV420P),
"-profile", "high",
"-level", "51",
"-vsync", "vfr",
@ -85,13 +81,12 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
)
case VAAPIEncoder:
format := "format=nv12,hwupload"
result = exec.Command(
opt.Bin,
"-hwaccel", "vaapi",
"-i", fileName,
"-c:a", "aac",
"-vf", scale+", "+format+"",
"-vf", opt.VideoFilter(FormatNV12),
"-c:v", opt.Encoder.String(),
"-map", opt.MapVideo,
"-map", opt.MapAudio,
@ -105,12 +100,11 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
case NvidiaEncoder:
// ffmpeg -hide_banner -h encoder=h264_nvenc
format := "format=yuv420p"
result = exec.Command(
opt.Bin,
"-hwaccel", "auto",
"-i", fileName,
"-pix_fmt", "yuv420p",
"-pix_fmt", FormatYUV420P.String(),
"-c:v", opt.Encoder.String(),
"-map", opt.MapVideo,
"-map", opt.MapAudio,
@ -118,7 +112,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
"-preset", "15",
"-pixel_format", "yuv420p",
"-gpu", "any",
"-vf", scale+", "+format+"",
"-vf", opt.VideoFilter(FormatYUV420P),
"-rc:v", "constqp",
"-cq", "0",
"-tune", "2",
@ -134,7 +128,6 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
case Video4LinuxEncoder:
// ffmpeg -hide_banner -h encoder=h264_v4l2m2m
format := "format=yuv420p"
result = exec.Command(
opt.Bin,
"-i", fileName,
@ -142,7 +135,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
"-map", opt.MapVideo,
"-map", opt.MapAudio,
"-c:a", "aac",
"-vf", scale+", "+format+"",
"-vf", opt.VideoFilter(FormatYUV420P),
"-num_output_buffers", "72",
"-num_capture_buffers", "64",
"-max_muxing_queue_size", "1024",
@ -156,7 +149,6 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
)
default:
format := "format=yuv420p"
result = exec.Command(
opt.Bin,
"-i", fileName,
@ -164,7 +156,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
"-map", opt.MapVideo,
"-map", opt.MapAudio,
"-c:a", "aac",
"-vf", scale+", "+format+"",
"-vf", opt.VideoFilter(FormatYUV420P),
"-max_muxing_queue_size", "1024",
"-crf", "23",
"-vsync", "vfr",

16
internal/ffmpeg/format.go Normal file
View file

@ -0,0 +1,16 @@
package ffmpeg
// PixelFormat represents a standard pixel format.
type PixelFormat string
// String returns the pixel format as string.
func (f PixelFormat) String() string {
return string(f)
}
// Standard pixel formats.
const (
FormatYUV420P PixelFormat = "yuv420p"
FormatRGB32 PixelFormat = "rgb32"
FormatNV12 PixelFormat = "nv12,hwupload"
)

View file

@ -2,9 +2,9 @@ package thumb
import "image"
// Fitted contains only "fit" cropped thumbnail sizes from largest to smallest.
// FitSizes contains "fit" cropped thumbnail sizes from largest to smallest.
// Best for the viewer as proportional resizing maintains the aspect ratio.
var Fitted = []Size{
var FitSizes = SizeList{
Sizes[Fit7680],
Sizes[Fit4096],
Sizes[Fit3840],
@ -17,15 +17,15 @@ var Fitted = []Size{
// Fit returns the largest fitting thumbnail size.
func Fit(w, h int) (size Size) {
j := len(Fitted) - 1
j := len(FitSizes) - 1
for i := j; i >= 0; i-- {
if size = Fitted[i]; w <= size.Width && h <= size.Height {
if size = FitSizes[i]; w <= size.Width && h <= size.Height {
return size
}
}
return Fitted[0]
return FitSizes[0]
}
// FitBounds returns the largest thumbnail size fitting the rectangle.

39
internal/thumb/report.go Normal file
View file

@ -0,0 +1,39 @@
package thumb
import (
"fmt"
"sort"
"github.com/photoprism/photoprism/pkg/report"
)
// Report returns a file format documentation table.
func Report(sizes SizeList, short bool) (rows [][]string, cols []string) {
if short {
cols = []string{"Size", "Usage"}
} else {
cols = []string{"Name", "Width", "Height", "Aspect Ratio", "Usage"}
}
sorted := append(SizeList{}, sizes...)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].Width == sorted[j].Width {
return sorted[i].Name < sorted[j].Name
} else {
return sorted[i].Width < sorted[j].Width
}
})
rows = make([][]string, 0, len(sorted))
for _, s := range sorted {
if short {
rows = append(rows, []string{fmt.Sprintf("%d", s.Width), s.Usage})
} else {
rows = append(rows, []string{s.Name.String(), fmt.Sprintf("%d", s.Width), fmt.Sprintf("%d", s.Height), report.Bool(s.Fit, "Preserved", "1:1"), s.Usage})
}
}
return rows, cols
}

View file

@ -0,0 +1,20 @@
package thumb
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestReport(t *testing.T) {
t.Run("Videos", func(t *testing.T) {
rows, cols := Report(VideoSizes, true)
assert.Equal(t, 2, len(cols))
assert.Equal(t, len(VideoSizes), len(rows))
})
t.Run("Thumbs", func(t *testing.T) {
rows, cols := Report(Sizes.All(), false)
assert.Equal(t, 5, len(cols))
assert.Equal(t, len(Sizes), len(rows))
})
}

View file

@ -4,10 +4,11 @@ import (
"image"
)
// Size represents a standard media resolution.
type Size struct {
Name Name `json:"name"`
Source Name `json:"-"`
Use string `json:"use"`
Usage string `json:"usage"`
Width int `json:"w"`
Height int `json:"h"`
Public bool `json:"-"`

View file

@ -6,7 +6,7 @@ var (
Filter = ResampleLanczos
)
// MaxSize returns the max supported thumb size in pixels.
// MaxSize returns the max supported size in pixels.
func MaxSize() int {
if SizePrecached > SizeUncached {
return SizePrecached
@ -15,29 +15,43 @@ func MaxSize() int {
return SizeUncached
}
// InvalidSize tests if the thumb size in pixels is invalid.
// InvalidSize tests if the size in pixels is invalid.
func InvalidSize(size int) bool {
return size < 0 || size > MaxSize()
}
// SizeList represents a list of sizes.
type SizeList []Size
// SizeMap maps size names to sizes.
type SizeMap map[Name]Size
// All returns a slice containing all sizes.
func (m SizeMap) All() SizeList {
result := make(SizeList, 0, len(m))
for _, s := range m {
result = append(result, s)
}
return result
}
// Sizes contains the properties of all thumbnail sizes.
var Sizes = SizeMap{
Tile50: {Tile50, Tile500, "Lists", 50, 50, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}},
Tile100: {Tile100, Tile500, "Maps", 100, 100, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}},
Tile224: {Tile224, Tile500, "TensorFlow, Mosaic", 224, 224, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}},
Tile500: {Tile500, "", "Tiles", 500, 500, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}},
Tile50: {Tile50, Tile500, "List View", 50, 50, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}},
Tile100: {Tile100, Tile500, "Places View", 100, 100, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}},
Tile224: {Tile224, Tile500, "TensorFlow, Mosaic View", 224, 224, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}},
Tile500: {Tile500, "", "Cards View", 500, 500, false, false, []ResampleOption{ResampleFillCenter, ResampleDefault}},
Colors: {Colors, Fit720, "Color Detection", 3, 3, false, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}},
Left224: {Left224, Fit720, "TensorFlow", 224, 224, false, false, []ResampleOption{ResampleFillTopLeft, ResampleDefault}},
Right224: {Right224, Fit720, "TensorFlow", 224, 224, false, false, []ResampleOption{ResampleFillBottomRight, ResampleDefault}},
Fit720: {Fit720, "", "Mobile, TV", 720, 720, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit1280: {Fit1280, Fit2048, "Mobile, HD Ready TV", 1280, 1024, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit1920: {Fit1920, Fit2048, "Mobile, Full HD TV", 1920, 1200, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit2048: {Fit2048, "", "Tablets, Cinema 2K", 2048, 2048, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit2560: {Fit2560, "", "Quad HD, Retina Display", 2560, 1600, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit3840: {Fit3840, "", "Ultra HD", 3840, 2400, true, true, []ResampleOption{ResampleFit, ResampleDefault}}, // Deprecated in favor of fit_4096
Fit4096: {Fit4096, "", "Ultra HD, Retina 4K", 4096, 4096, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit7680: {Fit7680, "", "8K Ultra HD 2, Retina 6K", 7680, 4320, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit720: {Fit720, "", "SD TV, Mobile", 720, 720, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit1280: {Fit1280, Fit2048, "HD TV, SXGA", 1280, 1024, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit1920: {Fit1920, Fit2048, "Full HD", 1920, 1200, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit2048: {Fit2048, "", "DCI 2K, Tablets", 2048, 2048, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit2560: {Fit2560, "", "Quad HD, Notebooks", 2560, 1600, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit3840: {Fit3840, "", "4K Ultra HD", 3840, 2400, true, true, []ResampleOption{ResampleFit, ResampleDefault}}, // Deprecated in favor of fit_4096
Fit4096: {Fit4096, "", "DCI 4K, Retina 4K", 4096, 4096, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
Fit7680: {Fit7680, "", "8K Ultra HD 2", 7680, 4320, true, true, []ResampleOption{ResampleFit, ResampleDefault}},
}

37
internal/thumb/video.go Normal file
View file

@ -0,0 +1,37 @@
package thumb
// VideoSizes contains all valid video output sizes sorted by size.
var VideoSizes = SizeList{
Sizes[Fit7680],
Sizes[Fit4096],
Sizes[Fit3840],
Sizes[Fit2560],
Sizes[Fit2048],
Sizes[Fit1920],
Sizes[Fit1280],
Sizes[Fit720],
}
// VideoSize returns the largest video size type for the given width limit.
func VideoSize(limit int) Size {
if limit < 0 {
// Return maximum size.
return Sizes[Fit7680]
} else if limit == 0 {
// Return default size.
return Sizes[Fit3840]
} else if limit <= 720 {
// Return minimum size.
return Sizes[Fit720]
}
// Find match.
for _, t := range VideoSizes {
if t.Width <= limit {
return t
}
}
// Return maximum size.
return Sizes[Fit7680]
}

View file

@ -0,0 +1,22 @@
package thumb
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestVideoSize(t *testing.T) {
assert.Equal(t, Sizes[Fit720], VideoSize(720))
assert.Equal(t, Sizes[Fit720], VideoSize(1279))
assert.Equal(t, Sizes[Fit1280], VideoSize(1280))
assert.Equal(t, Sizes[Fit1280], VideoSize(1281))
assert.Equal(t, Sizes[Fit1920], VideoSize(1920))
assert.Equal(t, Sizes[Fit1920], VideoSize(2000))
assert.Equal(t, Sizes[Fit2048], VideoSize(2048))
assert.Equal(t, Sizes[Fit2560], VideoSize(3000))
assert.Equal(t, Sizes[Fit3840], VideoSize(0))
assert.Equal(t, Sizes[Fit3840], VideoSize(4000))
assert.Equal(t, Sizes[Fit7680], VideoSize(8000))
assert.Equal(t, Sizes[Fit7680], VideoSize(-1))
}