2019-02-09 12:30:49 +00:00
|
|
|
package ffmpeg
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
2019-08-15 22:47:35 +00:00
|
|
|
"path/filepath"
|
2019-02-09 12:30:49 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2020-04-09 22:38:34 +00:00
|
|
|
|
2022-02-03 00:20:34 +00:00
|
|
|
"github.com/stashapp/stash/pkg/desktop"
|
2020-05-26 23:33:49 +00:00
|
|
|
"github.com/stashapp/stash/pkg/logger"
|
2020-04-09 22:38:34 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Container string
|
|
|
|
type AudioCodec string
|
|
|
|
|
|
|
|
const (
|
|
|
|
Mp4 Container = "mp4"
|
|
|
|
M4v Container = "m4v"
|
|
|
|
Mov Container = "mov"
|
|
|
|
Wmv Container = "wmv"
|
|
|
|
Webm Container = "webm"
|
|
|
|
Matroska Container = "matroska"
|
|
|
|
Avi Container = "avi"
|
|
|
|
Flv Container = "flv"
|
|
|
|
Mpegts Container = "mpegts"
|
|
|
|
Aac AudioCodec = "aac"
|
|
|
|
Mp3 AudioCodec = "mp3"
|
|
|
|
Opus AudioCodec = "opus"
|
|
|
|
Vorbis AudioCodec = "vorbis"
|
|
|
|
MissingUnsupported AudioCodec = ""
|
|
|
|
Mp4Ffmpeg string = "mov,mp4,m4a,3gp,3g2,mj2" // browsers support all of them
|
|
|
|
M4vFfmpeg string = "mov,mp4,m4a,3gp,3g2,mj2" // so we don't care that ffmpeg
|
|
|
|
MovFfmpeg string = "mov,mp4,m4a,3gp,3g2,mj2" // can't differentiate between them
|
|
|
|
WmvFfmpeg string = "asf"
|
|
|
|
WebmFfmpeg string = "matroska,webm"
|
|
|
|
MatroskaFfmpeg string = "matroska,webm"
|
|
|
|
AviFfmpeg string = "avi"
|
|
|
|
FlvFfmpeg string = "flv"
|
|
|
|
MpegtsFfmpeg string = "mpegts"
|
|
|
|
H264 string = "h264"
|
|
|
|
H265 string = "h265" // found in rare cases from a faulty encoder
|
|
|
|
Hevc string = "hevc"
|
|
|
|
Vp8 string = "vp8"
|
|
|
|
Vp9 string = "vp9"
|
2020-07-23 01:56:08 +00:00
|
|
|
Mkv string = "mkv" // only used from the browser to indicate mkv support
|
|
|
|
Hls string = "hls" // only used from the browser to indicate hls support
|
2020-04-09 22:38:34 +00:00
|
|
|
MimeWebm string = "video/webm"
|
|
|
|
MimeMkv string = "video/x-matroska"
|
2020-07-23 01:56:08 +00:00
|
|
|
MimeMp4 string = "video/mp4"
|
|
|
|
MimeHLS string = "application/vnd.apple.mpegurl"
|
|
|
|
MimeMpegts string = "video/MP2T"
|
2019-02-09 12:30:49 +00:00
|
|
|
)
|
|
|
|
|
2020-07-23 01:56:08 +00:00
|
|
|
// only support H264 by default, since Safari does not support VP8/VP9
|
|
|
|
var DefaultSupportedCodecs = []string{H264, H265}
|
2020-04-09 22:38:34 +00:00
|
|
|
|
|
|
|
var validForH264Mkv = []Container{Mp4, Matroska}
|
|
|
|
var validForH264 = []Container{Mp4}
|
|
|
|
var validForH265Mkv = []Container{Mp4, Matroska}
|
|
|
|
var validForH265 = []Container{Mp4}
|
|
|
|
var validForVp8 = []Container{Webm}
|
|
|
|
var validForVp9Mkv = []Container{Webm, Matroska}
|
|
|
|
var validForVp9 = []Container{Webm}
|
|
|
|
var validForHevcMkv = []Container{Mp4, Matroska}
|
|
|
|
var validForHevc = []Container{Mp4}
|
|
|
|
|
|
|
|
var validAudioForMkv = []AudioCodec{Aac, Mp3, Vorbis, Opus}
|
|
|
|
var validAudioForWebm = []AudioCodec{Vorbis, Opus}
|
|
|
|
var validAudioForMp4 = []AudioCodec{Aac, Mp3}
|
|
|
|
|
2021-10-20 05:10:46 +00:00
|
|
|
// ContainerToFfprobe maps user readable container strings to ffprobe's format_name.
|
|
|
|
// On some formats ffprobe can't differentiate
|
2020-04-09 22:38:34 +00:00
|
|
|
var ContainerToFfprobe = map[Container]string{
|
|
|
|
Mp4: Mp4Ffmpeg,
|
|
|
|
M4v: M4vFfmpeg,
|
|
|
|
Mov: MovFfmpeg,
|
|
|
|
Wmv: WmvFfmpeg,
|
|
|
|
Webm: WebmFfmpeg,
|
|
|
|
Matroska: MatroskaFfmpeg,
|
|
|
|
Avi: AviFfmpeg,
|
|
|
|
Flv: FlvFfmpeg,
|
|
|
|
Mpegts: MpegtsFfmpeg,
|
|
|
|
}
|
|
|
|
|
|
|
|
var FfprobeToContainer = map[string]Container{
|
|
|
|
Mp4Ffmpeg: Mp4,
|
|
|
|
WmvFfmpeg: Wmv,
|
|
|
|
AviFfmpeg: Avi,
|
|
|
|
FlvFfmpeg: Flv,
|
|
|
|
MpegtsFfmpeg: Mpegts,
|
|
|
|
MatroskaFfmpeg: Matroska,
|
|
|
|
}
|
|
|
|
|
|
|
|
func MatchContainer(format string, filePath string) Container { // match ffprobe string to our Container
|
|
|
|
|
|
|
|
container := FfprobeToContainer[format]
|
|
|
|
if container == Matroska {
|
|
|
|
container = MagicContainer(filePath) // use magic number instead of ffprobe for matroska,webm
|
|
|
|
}
|
|
|
|
if container == "" { // if format is not in our Container list leave it as ffprobes reported format_name
|
|
|
|
container = Container(format)
|
|
|
|
}
|
|
|
|
return container
|
|
|
|
}
|
2019-02-10 05:30:54 +00:00
|
|
|
|
2020-07-23 01:56:08 +00:00
|
|
|
func IsValidCodec(codecName string, supportedCodecs []string) bool {
|
|
|
|
for _, c := range supportedCodecs {
|
2019-02-10 05:30:54 +00:00
|
|
|
if c == codecName {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
2019-02-09 12:30:49 +00:00
|
|
|
}
|
|
|
|
|
Enable gocritic (#1848)
* Don't capitalize local variables
ValidCodecs -> validCodecs
* Capitalize deprecation markers
A deprecated marker should be capitalized.
* Use re.MustCompile for static regexes
If the regex fails to compile, it's a programmer error, and should be
treated as such. The regex is entirely static.
* Simplify else-if constructions
Rewrite
else { if cond {}}
to
else if cond {}
* Use a switch statement to analyze formats
Break an if-else chain. While here, simplify code flow.
Also introduce a proper static error for unsupported image formats,
paving the way for being able to check against the error.
* Rewrite ifElse chains into switch statements
The "Effective Go" https://golang.org/doc/effective_go#switch document
mentions it is more idiomatic to write if-else chains as switches when
it is possible.
Find all the plain rewrite occurrences in the code base and rewrite.
In some cases, the if-else chains are replaced by a switch scrutinizer.
That is, the code sequence
if x == 1 {
..
} else if x == 2 {
..
} else if x == 3 {
...
}
can be rewritten into
switch x {
case 1:
..
case 2:
..
case 3:
..
}
which is clearer for the compiler: it can decide if the switch is
better served by a jump-table then a branch-chain.
* Rewrite switches, introduce static errors
Introduce two new static errors:
* `ErrNotImplmented`
* `ErrNotSupported`
And use these rather than forming new generative errors whenever the
code is called. Code can now test on the errors (since they are static
and the pointers to them wont change).
Also rewrite ifElse chains into switches in this part of the code base.
* Introduce a StashBoxError in configuration
Since all stashbox errors are the same, treat them as such in the code
base. While here, rewrite an ifElse chain.
In the future, it might be beneifical to refactor configuration errors
into one error which can handle missing fields, which context the error
occurs in and so on. But for now, try to get an overview of the error
categories by hoisting them into static errors.
* Get rid of an else-block in transaction handling
If we succesfully `recover()`, we then always `panic()`. This means the
rest of the code is not reachable, so we can avoid having an else-block
here.
It also solves an ifElse-chain style check in the code base.
* Use strings.ReplaceAll
Rewrite
strings.Replace(s, o, n, -1)
into
strings.ReplaceAll(s, o, n)
To make it consistent and clear that we are doing an all-replace in the
string rather than replacing parts of it. It's more of a nitpick since
there are no implementation differences: the stdlib implementation is
just to supply -1.
* Rewrite via gocritic's assignOp
Statements of the form
x = x + e
is rewritten into
x += e
where applicable.
* Formatting
* Review comments handled
Stash-box is a proper noun.
Rewrite a switch into an if-chain which returns on the first error
encountered.
* Use context.TODO() over context.Background()
Patch in the same vein as everything else: use the TODO() marker so we
can search for it later and link it into the context tree/tentacle once
it reaches down to this level in the code base.
* Tell the linter to ignore a section in manager_tasks.go
The section is less readable, so mark it with a nolint for now. Because
the rewrite enables a ifElseChain, also mark that as nolint for now.
* Use strings.ReplaceAll over strings.Replace
* Apply an ifElse rewrite
else { if .. { .. } } rewrite into else if { .. }
* Use switch-statements over ifElseChains
Rewrite chains of if-else into switch statements. Where applicable,
add an early nil-guard to simplify case analysis. Also, in
ScanTask's Start(..), invert the logic to outdent the whole block, and
help the reader: if it's not a scene, the function flow is now far more
local to the top of the function, and it's clear that the rest of the
function has to do with scene management.
* Enable gocritic on the code base.
Disable appendAssign for now since we aren't passing that check yet.
* Document the nolint additions
* Document StashBoxBatchPerformerTagInput
2021-10-18 03:12:40 +00:00
|
|
|
func IsValidAudio(audio AudioCodec, validCodecs []AudioCodec) bool {
|
2020-04-09 22:38:34 +00:00
|
|
|
|
|
|
|
// if audio codec is missing or unsupported by ffmpeg we can't do anything about it
|
|
|
|
// report it as valid so that the file can at least be streamed directly if the video codec is supported
|
|
|
|
if audio == MissingUnsupported {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
Enable gocritic (#1848)
* Don't capitalize local variables
ValidCodecs -> validCodecs
* Capitalize deprecation markers
A deprecated marker should be capitalized.
* Use re.MustCompile for static regexes
If the regex fails to compile, it's a programmer error, and should be
treated as such. The regex is entirely static.
* Simplify else-if constructions
Rewrite
else { if cond {}}
to
else if cond {}
* Use a switch statement to analyze formats
Break an if-else chain. While here, simplify code flow.
Also introduce a proper static error for unsupported image formats,
paving the way for being able to check against the error.
* Rewrite ifElse chains into switch statements
The "Effective Go" https://golang.org/doc/effective_go#switch document
mentions it is more idiomatic to write if-else chains as switches when
it is possible.
Find all the plain rewrite occurrences in the code base and rewrite.
In some cases, the if-else chains are replaced by a switch scrutinizer.
That is, the code sequence
if x == 1 {
..
} else if x == 2 {
..
} else if x == 3 {
...
}
can be rewritten into
switch x {
case 1:
..
case 2:
..
case 3:
..
}
which is clearer for the compiler: it can decide if the switch is
better served by a jump-table then a branch-chain.
* Rewrite switches, introduce static errors
Introduce two new static errors:
* `ErrNotImplmented`
* `ErrNotSupported`
And use these rather than forming new generative errors whenever the
code is called. Code can now test on the errors (since they are static
and the pointers to them wont change).
Also rewrite ifElse chains into switches in this part of the code base.
* Introduce a StashBoxError in configuration
Since all stashbox errors are the same, treat them as such in the code
base. While here, rewrite an ifElse chain.
In the future, it might be beneifical to refactor configuration errors
into one error which can handle missing fields, which context the error
occurs in and so on. But for now, try to get an overview of the error
categories by hoisting them into static errors.
* Get rid of an else-block in transaction handling
If we succesfully `recover()`, we then always `panic()`. This means the
rest of the code is not reachable, so we can avoid having an else-block
here.
It also solves an ifElse-chain style check in the code base.
* Use strings.ReplaceAll
Rewrite
strings.Replace(s, o, n, -1)
into
strings.ReplaceAll(s, o, n)
To make it consistent and clear that we are doing an all-replace in the
string rather than replacing parts of it. It's more of a nitpick since
there are no implementation differences: the stdlib implementation is
just to supply -1.
* Rewrite via gocritic's assignOp
Statements of the form
x = x + e
is rewritten into
x += e
where applicable.
* Formatting
* Review comments handled
Stash-box is a proper noun.
Rewrite a switch into an if-chain which returns on the first error
encountered.
* Use context.TODO() over context.Background()
Patch in the same vein as everything else: use the TODO() marker so we
can search for it later and link it into the context tree/tentacle once
it reaches down to this level in the code base.
* Tell the linter to ignore a section in manager_tasks.go
The section is less readable, so mark it with a nolint for now. Because
the rewrite enables a ifElseChain, also mark that as nolint for now.
* Use strings.ReplaceAll over strings.Replace
* Apply an ifElse rewrite
else { if .. { .. } } rewrite into else if { .. }
* Use switch-statements over ifElseChains
Rewrite chains of if-else into switch statements. Where applicable,
add an early nil-guard to simplify case analysis. Also, in
ScanTask's Start(..), invert the logic to outdent the whole block, and
help the reader: if it's not a scene, the function flow is now far more
local to the top of the function, and it's clear that the rest of the
function has to do with scene management.
* Enable gocritic on the code base.
Disable appendAssign for now since we aren't passing that check yet.
* Document the nolint additions
* Document StashBoxBatchPerformerTagInput
2021-10-18 03:12:40 +00:00
|
|
|
for _, c := range validCodecs {
|
2020-04-09 22:38:34 +00:00
|
|
|
if c == audio {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func IsValidAudioForContainer(audio AudioCodec, format Container) bool {
|
|
|
|
switch format {
|
|
|
|
case Matroska:
|
|
|
|
return IsValidAudio(audio, validAudioForMkv)
|
|
|
|
case Webm:
|
|
|
|
return IsValidAudio(audio, validAudioForWebm)
|
|
|
|
case Mp4:
|
|
|
|
return IsValidAudio(audio, validAudioForMp4)
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func IsValidForContainer(format Container, validContainers []Container) bool {
|
|
|
|
for _, fmt := range validContainers {
|
|
|
|
if fmt == format {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-10-20 05:10:46 +00:00
|
|
|
// IsValidCombo checks if a codec/container combination is valid.
|
|
|
|
// Returns true on validity, false otherwise
|
2020-07-23 01:56:08 +00:00
|
|
|
func IsValidCombo(codecName string, format Container, supportedVideoCodecs []string) bool {
|
|
|
|
supportMKV := IsValidCodec(Mkv, supportedVideoCodecs)
|
|
|
|
supportHEVC := IsValidCodec(Hevc, supportedVideoCodecs)
|
|
|
|
|
2020-04-09 22:38:34 +00:00
|
|
|
switch codecName {
|
|
|
|
case H264:
|
2020-07-23 01:56:08 +00:00
|
|
|
if supportMKV {
|
2020-04-09 22:38:34 +00:00
|
|
|
return IsValidForContainer(format, validForH264Mkv)
|
|
|
|
}
|
|
|
|
return IsValidForContainer(format, validForH264)
|
|
|
|
case H265:
|
2020-07-23 01:56:08 +00:00
|
|
|
if supportMKV {
|
2020-04-09 22:38:34 +00:00
|
|
|
return IsValidForContainer(format, validForH265Mkv)
|
|
|
|
}
|
|
|
|
return IsValidForContainer(format, validForH265)
|
|
|
|
case Vp8:
|
|
|
|
return IsValidForContainer(format, validForVp8)
|
|
|
|
case Vp9:
|
2020-07-23 01:56:08 +00:00
|
|
|
if supportMKV {
|
2020-04-09 22:38:34 +00:00
|
|
|
return IsValidForContainer(format, validForVp9Mkv)
|
|
|
|
}
|
|
|
|
return IsValidForContainer(format, validForVp9)
|
|
|
|
case Hevc:
|
2020-07-23 01:56:08 +00:00
|
|
|
if supportHEVC {
|
|
|
|
if supportMKV {
|
2020-04-09 22:38:34 +00:00
|
|
|
return IsValidForContainer(format, validForHevcMkv)
|
|
|
|
}
|
|
|
|
return IsValidForContainer(format, validForHevc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-07-23 01:56:08 +00:00
|
|
|
func IsStreamable(videoCodec string, audioCodec AudioCodec, container Container) bool {
|
|
|
|
supportedVideoCodecs := DefaultSupportedCodecs
|
|
|
|
|
|
|
|
// check if the video codec matches the supported codecs
|
|
|
|
return IsValidCodec(videoCodec, supportedVideoCodecs) && IsValidCombo(videoCodec, container, supportedVideoCodecs) && IsValidAudioForContainer(audioCodec, container)
|
|
|
|
}
|
|
|
|
|
2019-02-10 05:30:54 +00:00
|
|
|
type VideoFile struct {
|
2019-02-14 22:53:32 +00:00
|
|
|
JSON FFProbeJSON
|
2019-02-10 05:30:54 +00:00
|
|
|
AudioStream *FFProbeStream
|
|
|
|
VideoStream *FFProbeStream
|
2019-02-09 12:30:49 +00:00
|
|
|
|
|
|
|
Path string
|
2019-08-15 22:47:35 +00:00
|
|
|
Title string
|
|
|
|
Comment string
|
2019-02-09 12:30:49 +00:00
|
|
|
Container string
|
|
|
|
Duration float64
|
|
|
|
StartTime float64
|
|
|
|
Bitrate int64
|
|
|
|
Size int64
|
|
|
|
CreationTime time.Time
|
|
|
|
|
|
|
|
VideoCodec string
|
|
|
|
VideoBitrate int64
|
|
|
|
Width int
|
|
|
|
Height int
|
|
|
|
FrameRate float64
|
|
|
|
Rotation int64
|
2022-01-04 02:46:53 +00:00
|
|
|
FrameCount int64
|
2019-02-09 12:30:49 +00:00
|
|
|
|
|
|
|
AudioCodec string
|
|
|
|
}
|
|
|
|
|
2021-10-14 23:39:48 +00:00
|
|
|
// FFProbe
|
|
|
|
type FFProbe string
|
|
|
|
|
2019-02-09 12:30:49 +00:00
|
|
|
// Execute exec command and bind result to struct.
|
2021-10-14 23:39:48 +00:00
|
|
|
func (f *FFProbe) NewVideoFile(videoPath string, stripExt bool) (*VideoFile, error) {
|
2019-02-10 05:30:54 +00:00
|
|
|
args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", videoPath}
|
2022-02-03 00:20:34 +00:00
|
|
|
cmd := exec.Command(string(*f), args...)
|
|
|
|
desktop.HideExecShell(cmd)
|
|
|
|
out, err := cmd.Output()
|
2019-02-09 12:30:49 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2019-02-10 05:30:54 +00:00
|
|
|
return nil, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", videoPath, string(out), err.Error())
|
2019-02-09 12:30:49 +00:00
|
|
|
}
|
|
|
|
|
2019-02-10 05:30:54 +00:00
|
|
|
probeJSON := &FFProbeJSON{}
|
2019-02-09 12:30:49 +00:00
|
|
|
if err := json.Unmarshal(out, probeJSON); err != nil {
|
2021-09-08 01:23:10 +00:00
|
|
|
return nil, fmt.Errorf("error unmarshalling video data for <%s>: %s", videoPath, err.Error())
|
2019-02-09 12:30:49 +00:00
|
|
|
}
|
|
|
|
|
2021-01-07 00:38:30 +00:00
|
|
|
return parse(videoPath, probeJSON, stripExt)
|
2019-02-09 12:30:49 +00:00
|
|
|
}
|
|
|
|
|
2022-01-04 02:46:53 +00:00
|
|
|
// GetReadFrameCount counts the actual frames of the video file
|
|
|
|
func (f *FFProbe) GetReadFrameCount(vf *VideoFile) (int64, error) {
|
|
|
|
args := []string{"-v", "quiet", "-print_format", "json", "-count_frames", "-show_format", "-show_streams", "-show_error", vf.Path}
|
|
|
|
out, err := exec.Command(string(*f), args...).Output()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", vf.Path, string(out), err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
probeJSON := &FFProbeJSON{}
|
|
|
|
if err := json.Unmarshal(out, probeJSON); err != nil {
|
|
|
|
return 0, fmt.Errorf("error unmarshalling video data for <%s>: %s", vf.Path, err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
fc, err := parse(vf.Path, probeJSON, false)
|
|
|
|
return fc.FrameCount, err
|
|
|
|
}
|
|
|
|
|
2021-01-07 00:38:30 +00:00
|
|
|
func parse(filePath string, probeJSON *FFProbeJSON, stripExt bool) (*VideoFile, error) {
|
2019-02-10 05:30:54 +00:00
|
|
|
if probeJSON == nil {
|
2020-10-11 01:02:41 +00:00
|
|
|
return nil, fmt.Errorf("failed to get ffprobe json for <%s>", filePath)
|
2019-02-10 05:30:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
result := &VideoFile{}
|
|
|
|
result.JSON = *probeJSON
|
|
|
|
|
|
|
|
if result.JSON.Error.Code != 0 {
|
|
|
|
return nil, fmt.Errorf("ffprobe error code %d: %s", result.JSON.Error.Code, result.JSON.Error.String)
|
|
|
|
}
|
2019-02-09 12:30:49 +00:00
|
|
|
|
|
|
|
result.Path = filePath
|
2019-08-15 22:47:35 +00:00
|
|
|
result.Title = probeJSON.Format.Tags.Title
|
|
|
|
|
|
|
|
if result.Title == "" {
|
|
|
|
// default title to filename
|
2021-01-07 00:38:30 +00:00
|
|
|
result.SetTitleFromPath(stripExt)
|
2019-08-15 22:47:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
result.Comment = probeJSON.Format.Tags.Comment
|
2019-02-10 05:30:54 +00:00
|
|
|
result.Bitrate, _ = strconv.ParseInt(probeJSON.Format.BitRate, 10, 64)
|
2022-01-04 02:46:53 +00:00
|
|
|
|
2019-02-10 05:30:54 +00:00
|
|
|
result.Container = probeJSON.Format.FormatName
|
|
|
|
duration, _ := strconv.ParseFloat(probeJSON.Format.Duration, 64)
|
2019-02-14 22:53:32 +00:00
|
|
|
result.Duration = math.Round(duration*100) / 100
|
2020-05-26 23:33:49 +00:00
|
|
|
fileStat, err := os.Stat(filePath)
|
|
|
|
if err != nil {
|
2021-09-20 23:34:25 +00:00
|
|
|
statErr := fmt.Errorf("error statting file <%s>: %w", filePath, err)
|
|
|
|
logger.Errorf("%v", statErr)
|
|
|
|
return nil, statErr
|
2020-05-26 23:33:49 +00:00
|
|
|
}
|
2019-02-09 12:30:49 +00:00
|
|
|
result.Size = fileStat.Size()
|
2019-02-10 05:30:54 +00:00
|
|
|
result.StartTime, _ = strconv.ParseFloat(probeJSON.Format.StartTime, 64)
|
2019-03-09 18:14:55 +00:00
|
|
|
result.CreationTime = probeJSON.Format.Tags.CreationTime.Time
|
2019-02-10 05:30:54 +00:00
|
|
|
|
|
|
|
audioStream := result.GetAudioStream()
|
|
|
|
if audioStream != nil {
|
|
|
|
result.AudioCodec = audioStream.CodecName
|
|
|
|
result.AudioStream = audioStream
|
|
|
|
}
|
2019-02-09 12:30:49 +00:00
|
|
|
|
2019-02-10 05:30:54 +00:00
|
|
|
videoStream := result.GetVideoStream()
|
|
|
|
if videoStream != nil {
|
|
|
|
result.VideoStream = videoStream
|
2019-02-09 12:30:49 +00:00
|
|
|
result.VideoCodec = videoStream.CodecName
|
2022-01-04 02:46:53 +00:00
|
|
|
result.FrameCount, _ = strconv.ParseInt(videoStream.NbFrames, 10, 64)
|
|
|
|
if videoStream.NbReadFrames != "" { // if ffprobe counted the frames use that instead
|
|
|
|
fc, _ := strconv.ParseInt(videoStream.NbReadFrames, 10, 64)
|
|
|
|
if fc > 0 {
|
|
|
|
result.FrameCount, _ = strconv.ParseInt(videoStream.NbReadFrames, 10, 64)
|
|
|
|
} else {
|
|
|
|
logger.Debugf("[ffprobe] <%s> invalid Read Frames count", videoStream.NbReadFrames)
|
|
|
|
}
|
|
|
|
}
|
2019-02-09 12:30:49 +00:00
|
|
|
result.VideoBitrate, _ = strconv.ParseInt(videoStream.BitRate, 10, 64)
|
|
|
|
var framerate float64
|
|
|
|
if strings.Contains(videoStream.AvgFrameRate, "/") {
|
|
|
|
frameRateSplit := strings.Split(videoStream.AvgFrameRate, "/")
|
|
|
|
numerator, _ := strconv.ParseFloat(frameRateSplit[0], 64)
|
|
|
|
denominator, _ := strconv.ParseFloat(frameRateSplit[1], 64)
|
|
|
|
framerate = numerator / denominator
|
|
|
|
} else {
|
|
|
|
framerate, _ = strconv.ParseFloat(videoStream.AvgFrameRate, 64)
|
|
|
|
}
|
2019-02-14 22:53:32 +00:00
|
|
|
result.FrameRate = math.Round(framerate*100) / 100
|
2019-02-09 12:30:49 +00:00
|
|
|
if rotate, err := strconv.ParseInt(videoStream.Tags.Rotate, 10, 64); err == nil && rotate != 180 {
|
|
|
|
result.Width = videoStream.Height
|
|
|
|
result.Height = videoStream.Width
|
|
|
|
} else {
|
|
|
|
result.Width = videoStream.Width
|
|
|
|
result.Height = videoStream.Height
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-10 05:30:54 +00:00
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *VideoFile) GetAudioStream() *FFProbeStream {
|
|
|
|
index := v.getStreamIndex("audio", v.JSON)
|
|
|
|
if index != -1 {
|
|
|
|
return &v.JSON.Streams[index]
|
2019-02-09 12:30:49 +00:00
|
|
|
}
|
2019-02-10 05:30:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
2019-02-09 12:30:49 +00:00
|
|
|
|
2019-02-10 05:30:54 +00:00
|
|
|
func (v *VideoFile) GetVideoStream() *FFProbeStream {
|
|
|
|
index := v.getStreamIndex("video", v.JSON)
|
|
|
|
if index != -1 {
|
|
|
|
return &v.JSON.Streams[index]
|
|
|
|
}
|
|
|
|
return nil
|
2019-02-09 12:30:49 +00:00
|
|
|
}
|
|
|
|
|
2019-02-14 22:53:32 +00:00
|
|
|
func (v *VideoFile) getStreamIndex(fileType string, probeJSON FFProbeJSON) int {
|
|
|
|
for i, stream := range probeJSON.Streams {
|
2019-02-09 12:30:49 +00:00
|
|
|
if stream.CodecType == fileType {
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return -1
|
2019-02-14 22:53:32 +00:00
|
|
|
}
|
2019-10-12 08:20:27 +00:00
|
|
|
|
2021-01-07 00:38:30 +00:00
|
|
|
func (v *VideoFile) SetTitleFromPath(stripExtension bool) {
|
2019-10-12 08:20:27 +00:00
|
|
|
v.Title = filepath.Base(v.Path)
|
2021-01-07 00:38:30 +00:00
|
|
|
if stripExtension {
|
|
|
|
ext := filepath.Ext(v.Title)
|
|
|
|
v.Title = strings.TrimSuffix(v.Title, ext)
|
|
|
|
}
|
|
|
|
|
2019-10-12 08:20:27 +00:00
|
|
|
}
|