stash/pkg/ffmpeg/ffprobe.go

345 lines
10 KiB
Go
Raw Normal View History

2019-02-09 12:30:49 +00:00
package ffmpeg
import (
"encoding/json"
"fmt"
"math"
"os"
"os/exec"
"path/filepath"
2019-02-09 12:30:49 +00:00
"strconv"
"strings"
"time"
"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"
2019-02-09 12:30:49 +00:00
)
// 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}
//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
}
2019-02-10 05:30:54 +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
}
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
}
//extend stream validation check to take into account container
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)
}
2019-02-10 05:30:54 +00:00
type VideoFile struct {
JSON FFProbeJSON
2019-02-10 05:30:54 +00:00
AudioStream *FFProbeStream
VideoStream *FFProbeStream
2019-02-09 12:30:49 +00:00
Path string
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
AudioCodec string
}
// Execute exec command and bind result to struct.
2019-02-10 05:30:54 +00:00
func NewVideoFile(ffprobePath string, videoPath string) (*VideoFile, error) {
args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", videoPath}
//// Extremely slow on windows for some reason
//if runtime.GOOS != "windows" {
// args = append(args, "-count_frames")
//}
out, err := exec.Command(ffprobePath, args...).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 {
return nil, fmt.Errorf("Error unmarshalling video data for <%s>: %s", videoPath, err.Error())
2019-02-09 12:30:49 +00:00
}
2019-02-10 05:30:54 +00:00
return parse(videoPath, probeJSON)
2019-02-09 12:30:49 +00:00
}
2019-02-10 05:30:54 +00:00
func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
if probeJSON == nil {
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)
}
//} else if (ffprobeResult.stderr.includes("could not find codec parameters")) {
// throw new Error(`FFProbe [${filePath}] -> Could not find codec parameters`);
//} // TODO nil_or_unsupported.(video_stream) && nil_or_unsupported.(audio_stream)
2019-02-09 12:30:49 +00:00
result.Path = filePath
result.Title = probeJSON.Format.Tags.Title
if result.Title == "" {
// default title to filename
2019-10-12 08:20:27 +00:00
result.SetTitleFromPath()
}
result.Comment = probeJSON.Format.Tags.Comment
2019-02-10 05:30:54 +00:00
result.Bitrate, _ = strconv.ParseInt(probeJSON.Format.BitRate, 10, 64)
result.Container = probeJSON.Format.FormatName
duration, _ := strconv.ParseFloat(probeJSON.Format.Duration, 64)
result.Duration = math.Round(duration*100) / 100
fileStat, err := os.Stat(filePath)
if err != nil {
logger.Errorf("Error statting file <%s>: %s", filePath, err.Error())
return nil, err
}
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)
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
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)
}
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
}
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-10-12 08:20:27 +00:00
func (v *VideoFile) SetTitleFromPath() {
v.Title = filepath.Base(v.Path)
}