mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Restructure ffmpeg (#2392)
* Refactor transcode generation * Move phash generation into separate package * Refactor image thumbnail generation * Move JSONTime to separate package * Ffmpeg refactoring * Refactor live transcoding * Refactor scene marker preview generation * Refactor preview generation * Refactor screenshot generation * Refactor sprite generation * Change ffmpeg.IsStreamable to return error * Move frame rate calculation into ffmpeg * Refactor file locking * Refactor title set during scan * Add missing lockmanager instance * Return error instead of logging in MatchContainer
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,188 +13,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
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"
|
||||
Mkv string = "mkv" // only used from the browser to indicate mkv support
|
||||
Hls string = "hls" // only used from the browser to indicate hls support
|
||||
MimeWebm string = "video/webm"
|
||||
MimeMkv string = "video/x-matroska"
|
||||
MimeMp4 string = "video/mp4"
|
||||
MimeHLS string = "application/vnd.apple.mpegurl"
|
||||
MimeMpegts string = "video/MP2T"
|
||||
)
|
||||
|
||||
// only support H264 by default, since Safari does not support VP8/VP9
|
||||
var DefaultSupportedCodecs = []string{H264, H265}
|
||||
|
||||
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}
|
||||
|
||||
// ContainerToFfprobe maps user readable container strings to ffprobe's format_name.
|
||||
// On some formats ffprobe can't differentiate
|
||||
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
|
||||
}
|
||||
|
||||
func isValidCodec(codecName string, supportedCodecs []string) bool {
|
||||
for _, c := range supportedCodecs {
|
||||
if c == codecName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidAudio(audio AudioCodec, validCodecs []AudioCodec) bool {
|
||||
// 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
|
||||
}
|
||||
|
||||
for _, c := range validCodecs {
|
||||
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
|
||||
}
|
||||
|
||||
// isValidCombo checks if a codec/container combination is valid.
|
||||
// Returns true on validity, false otherwise
|
||||
func isValidCombo(codecName string, format Container, supportedVideoCodecs []string) bool {
|
||||
supportMKV := isValidCodec(Mkv, supportedVideoCodecs)
|
||||
supportHEVC := isValidCodec(Hevc, supportedVideoCodecs)
|
||||
|
||||
switch codecName {
|
||||
case H264:
|
||||
if supportMKV {
|
||||
return isValidForContainer(format, validForH264Mkv)
|
||||
}
|
||||
return isValidForContainer(format, validForH264)
|
||||
case H265:
|
||||
if supportMKV {
|
||||
return isValidForContainer(format, validForH265Mkv)
|
||||
}
|
||||
return isValidForContainer(format, validForH265)
|
||||
case Vp8:
|
||||
return isValidForContainer(format, validForVp8)
|
||||
case Vp9:
|
||||
if supportMKV {
|
||||
return isValidForContainer(format, validForVp9Mkv)
|
||||
}
|
||||
return isValidForContainer(format, validForVp9)
|
||||
case Hevc:
|
||||
if supportHEVC {
|
||||
if supportMKV {
|
||||
return isValidForContainer(format, validForHevcMkv)
|
||||
}
|
||||
return isValidForContainer(format, validForHevc)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// VideoFile represents the ffprobe output for a video file.
|
||||
type VideoFile struct {
|
||||
JSON FFProbeJSON
|
||||
AudioStream *FFProbeStream
|
||||
@@ -222,11 +40,38 @@ type VideoFile struct {
|
||||
AudioCodec string
|
||||
}
|
||||
|
||||
// FFProbe
|
||||
// TranscodeScale calculates the dimension scaling for a transcode, where maxSize is the maximum size of the longest dimension of the input video.
|
||||
// If no scaling is required, then returns 0, 0.
|
||||
// Returns -2 for the dimension that will scale to maintain aspect ratio.
|
||||
func (v *VideoFile) TranscodeScale(maxSize int) (int, int) {
|
||||
// get the smaller dimension of the video file
|
||||
videoSize := v.Height
|
||||
if v.Width < videoSize {
|
||||
videoSize = v.Width
|
||||
}
|
||||
|
||||
// if our streaming resolution is larger than the video dimension
|
||||
// or we are streaming the original resolution, then just set the
|
||||
// input width
|
||||
if maxSize >= videoSize || maxSize == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// we're setting either the width or height
|
||||
// we'll set the smaller dimesion
|
||||
if v.Width > v.Height {
|
||||
// set the height
|
||||
return -2, maxSize
|
||||
}
|
||||
|
||||
return maxSize, -2
|
||||
}
|
||||
|
||||
// FFProbe provides an interface to the ffprobe executable.
|
||||
type FFProbe string
|
||||
|
||||
// Execute exec command and bind result to struct.
|
||||
func (f *FFProbe) NewVideoFile(videoPath string, stripExt bool) (*VideoFile, error) {
|
||||
// NewVideoFile runs ffprobe on the given path and returns a VideoFile.
|
||||
func (f *FFProbe) NewVideoFile(videoPath string) (*VideoFile, error) {
|
||||
args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", videoPath}
|
||||
cmd := exec.Command(string(*f), args...)
|
||||
out, err := cmd.Output()
|
||||
@@ -240,28 +85,29 @@ func (f *FFProbe) NewVideoFile(videoPath string, stripExt bool) (*VideoFile, err
|
||||
return nil, fmt.Errorf("error unmarshalling video data for <%s>: %s", videoPath, err.Error())
|
||||
}
|
||||
|
||||
return parse(videoPath, probeJSON, stripExt)
|
||||
return parse(videoPath, probeJSON)
|
||||
}
|
||||
|
||||
// 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}
|
||||
// GetReadFrameCount counts the actual frames of the video file.
|
||||
// Used when the frame count is missing or incorrect.
|
||||
func (f *FFProbe) GetReadFrameCount(path string) (int64, error) {
|
||||
args := []string{"-v", "quiet", "-print_format", "json", "-count_frames", "-show_format", "-show_streams", "-show_error", 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())
|
||||
return 0, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", 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())
|
||||
return 0, fmt.Errorf("error unmarshalling video data for <%s>: %s", path, err.Error())
|
||||
}
|
||||
|
||||
fc, err := parse(vf.Path, probeJSON, false)
|
||||
fc, err := parse(path, probeJSON)
|
||||
return fc.FrameCount, err
|
||||
}
|
||||
|
||||
func parse(filePath string, probeJSON *FFProbeJSON, stripExt bool) (*VideoFile, error) {
|
||||
func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
||||
if probeJSON == nil {
|
||||
return nil, fmt.Errorf("failed to get ffprobe json for <%s>", filePath)
|
||||
}
|
||||
@@ -276,11 +122,6 @@ func parse(filePath string, probeJSON *FFProbeJSON, stripExt bool) (*VideoFile,
|
||||
result.Path = filePath
|
||||
result.Title = probeJSON.Format.Tags.Title
|
||||
|
||||
if result.Title == "" {
|
||||
// default title to filename
|
||||
result.SetTitleFromPath(stripExt)
|
||||
}
|
||||
|
||||
result.Comment = probeJSON.Format.Tags.Comment
|
||||
result.Bitrate, _ = strconv.ParseInt(probeJSON.Format.BitRate, 10, 64)
|
||||
|
||||
@@ -364,11 +205,3 @@ func (v *VideoFile) getStreamIndex(fileType string, probeJSON FFProbeJSON) int {
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (v *VideoFile) SetTitleFromPath(stripExtension bool) {
|
||||
v.Title = filepath.Base(v.Path)
|
||||
if stripExtension {
|
||||
ext := filepath.Ext(v.Title)
|
||||
v.Title = strings.TrimSuffix(v.Title, ext)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user