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:
WithoutPants
2022-04-18 10:50:10 +10:00
committed by GitHub
parent cdaa191155
commit aacf07feef
89 changed files with 3208 additions and 2004 deletions

View File

@@ -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)
}
}