mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Reorg
This commit is contained in:
166
pkg/ffmpeg/downloader.go
Normal file
166
pkg/ffmpeg/downloader.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetPaths(configDirectory string) (string, string) {
|
||||
var ffmpegPath, ffprobePath string
|
||||
|
||||
// Check if ffmpeg exists in the PATH
|
||||
if pathBinaryHasCorrectFlags() {
|
||||
ffmpegPath, _ = exec.LookPath("ffmpeg")
|
||||
ffprobePath, _ = exec.LookPath("ffprobe")
|
||||
}
|
||||
|
||||
// Check if ffmpeg exists in the config directory
|
||||
ffmpegConfigPath := filepath.Join(configDirectory, getFFMPEGFilename())
|
||||
ffprobeConfigPath := filepath.Join(configDirectory, getFFProbeFilename())
|
||||
ffmpegConfigExists, _ := utils.FileExists(ffmpegConfigPath)
|
||||
ffprobeConfigExists, _ := utils.FileExists(ffprobeConfigPath)
|
||||
if ffmpegPath == "" && ffmpegConfigExists {
|
||||
ffmpegPath = ffmpegConfigPath
|
||||
}
|
||||
if ffprobePath == "" && ffprobeConfigExists {
|
||||
ffprobePath = ffprobeConfigPath
|
||||
}
|
||||
|
||||
return ffmpegPath, ffprobePath
|
||||
}
|
||||
|
||||
func Download(configDirectory string) error {
|
||||
url := getFFMPEGURL()
|
||||
if url == "" {
|
||||
return fmt.Errorf("no ffmpeg url for this platform")
|
||||
}
|
||||
|
||||
// Configure where we want to download the archive
|
||||
urlExt := path.Ext(url)
|
||||
archivePath := filepath.Join(configDirectory, "ffmpeg"+urlExt)
|
||||
_ = os.Remove(archivePath) // remove archive if it already exists
|
||||
out, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Make the HTTP request
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Write the response to the archive file location
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if urlExt == ".zip" {
|
||||
if err := unzip(archivePath, configDirectory); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("ffmpeg was downloaded to %s", archivePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFFMPEGURL() string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-4.1-macos64-static.zip"
|
||||
case "linux":
|
||||
// TODO: untar this
|
||||
//return "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"
|
||||
return ""
|
||||
case "windows":
|
||||
return "https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-4.1-win64-static.zip"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getFFMPEGFilename() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "ffmpeg.exe"
|
||||
}
|
||||
return "ffmpeg"
|
||||
}
|
||||
|
||||
func getFFProbeFilename() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "ffprobe.exe"
|
||||
}
|
||||
return "ffprobe"
|
||||
}
|
||||
|
||||
// Checks if FFMPEG in the path has the correct flags
|
||||
func pathBinaryHasCorrectFlags() bool {
|
||||
ffmpegPath, err := exec.LookPath("ffmpeg")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
bytes, _ := exec.Command(ffmpegPath).CombinedOutput()
|
||||
output := string(bytes)
|
||||
hasOpus := strings.Contains(output, "--enable-libopus")
|
||||
hasVpx := strings.Contains(output, "--enable-libvpx")
|
||||
hasX264 := strings.Contains(output, "--enable-libx264")
|
||||
hasX265 := strings.Contains(output, "--enable-libx265")
|
||||
hasWebp := strings.Contains(output, "--enable-libwebp")
|
||||
return hasOpus && hasVpx && hasX264 && hasX265 && hasWebp
|
||||
}
|
||||
|
||||
func unzip(src, configDirectory string) error {
|
||||
zipReader, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
for _, f := range zipReader.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
filename := f.FileInfo().Name()
|
||||
if filename != "ffprobe" && filename != "ffmpeg" && filename != "ffprobe.exe" && filename != "ffmpeg.exe" {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
|
||||
unzippedPath := filepath.Join(configDirectory, filename)
|
||||
unzippedOutput, err := os.Create(unzippedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(unzippedOutput, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := unzippedOutput.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
71
pkg/ffmpeg/encoder.go
Normal file
71
pkg/ffmpeg/encoder.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var progressRegex = regexp.MustCompile(`time=(\d+):(\d+):(\d+.\d+)`)
|
||||
|
||||
type Encoder struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func NewEncoder(ffmpegPath string) Encoder {
|
||||
return Encoder{
|
||||
Path: ffmpegPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
|
||||
cmd := exec.Command(e.Path, args...)
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
logger.Error("FFMPEG stderr not available: " + err.Error())
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if nil != err {
|
||||
logger.Error("FFMPEG stdout not available: " + err.Error())
|
||||
}
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := make([]byte, 80)
|
||||
for {
|
||||
n, err := stderr.Read(buf)
|
||||
if n > 0 {
|
||||
data := string(buf[0:n])
|
||||
regexResult := progressRegex.FindStringSubmatch(data)
|
||||
if len(regexResult) == 4 && probeResult.Duration > 0 {
|
||||
h, _ := strconv.ParseFloat(regexResult[1], 64)
|
||||
m, _ := strconv.ParseFloat(regexResult[2], 64)
|
||||
s, _ := strconv.ParseFloat(regexResult[3], 64)
|
||||
hours := h * 3600
|
||||
mins := m * 60
|
||||
secs := s
|
||||
time := hours + mins + secs
|
||||
progress := time / probeResult.Duration
|
||||
logger.Infof("Progress %.2f", progress)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
stdoutData, _ := ioutil.ReadAll(stdout)
|
||||
stdoutString := string(stdoutData)
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return stdoutString, err
|
||||
}
|
||||
|
||||
return stdoutString, nil
|
||||
}
|
||||
58
pkg/ffmpeg/encoder_marker.go
Normal file
58
pkg/ffmpeg/encoder_marker.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type SceneMarkerOptions struct {
|
||||
ScenePath string
|
||||
Seconds int
|
||||
Width int
|
||||
OutputPath string
|
||||
}
|
||||
|
||||
func (e *Encoder) SceneMarkerVideo(probeResult VideoFile, options SceneMarkerOptions) error {
|
||||
args := []string{
|
||||
"-v", "quiet",
|
||||
"-ss", strconv.Itoa(options.Seconds),
|
||||
"-t", "20",
|
||||
"-i", probeResult.Path,
|
||||
"-c:v", "libx264",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", "veryslow",
|
||||
"-crf", "24",
|
||||
"-movflags", "+faststart",
|
||||
"-threads", "4",
|
||||
"-vf", fmt.Sprintf("scale=%v:-2", options.Width),
|
||||
"-sws_flags", "lanczos",
|
||||
"-c:a", "aac",
|
||||
"-b:a", "64k",
|
||||
"-strict", "-2",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, err := e.run(probeResult, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Encoder) SceneMarkerImage(probeResult VideoFile, options SceneMarkerOptions) error {
|
||||
args := []string{
|
||||
"-v", "quiet",
|
||||
"-ss", strconv.Itoa(options.Seconds),
|
||||
"-t", "5",
|
||||
"-i", probeResult.Path,
|
||||
"-c:v", "libwebp",
|
||||
"-lossless", "1",
|
||||
"-q:v", "70",
|
||||
"-compression_level", "6",
|
||||
"-preset", "default",
|
||||
"-loop", "0",
|
||||
"-threads", "4",
|
||||
"-vf", fmt.Sprintf("scale=%v:-2,fps=12", options.Width),
|
||||
"-an",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, err := e.run(probeResult, args)
|
||||
return err
|
||||
}
|
||||
66
pkg/ffmpeg/encoder_scene_preview_chunk.go
Normal file
66
pkg/ffmpeg/encoder_scene_preview_chunk.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ScenePreviewChunkOptions struct {
|
||||
Time int
|
||||
Width int
|
||||
OutputPath string
|
||||
}
|
||||
|
||||
func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePreviewChunkOptions) {
|
||||
args := []string{
|
||||
"-v", "quiet",
|
||||
"-ss", strconv.Itoa(options.Time),
|
||||
"-t", "0.75",
|
||||
"-i", probeResult.Path,
|
||||
"-y",
|
||||
"-c:v", "libx264",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", "veryslow",
|
||||
"-crf", "21",
|
||||
"-threads", "4",
|
||||
"-vf", fmt.Sprintf("scale=%v:-2", options.Width),
|
||||
"-c:a", "aac",
|
||||
"-b:a", "128k",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
|
||||
func (e *Encoder) ScenePreviewVideoChunkCombine(probeResult VideoFile, concatFilePath string, outputPath string) {
|
||||
args := []string{
|
||||
"-v", "quiet",
|
||||
"-f", "concat",
|
||||
"-i", utils.FixWindowsPath(concatFilePath),
|
||||
"-y",
|
||||
"-c", "copy",
|
||||
outputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
|
||||
func (e *Encoder) ScenePreviewVideoToImage(probeResult VideoFile, width int, videoPreviewPath string, outputPath string) error {
|
||||
args := []string{
|
||||
"-v", "quiet",
|
||||
"-i", videoPreviewPath,
|
||||
"-y",
|
||||
"-c:v", "libwebp",
|
||||
"-lossless", "1",
|
||||
"-q:v", "70",
|
||||
"-compression_level", "6",
|
||||
"-preset", "default",
|
||||
"-loop", "0",
|
||||
"-threads", "4",
|
||||
"-vf", fmt.Sprintf("scale=%v:-2,fps=12", width),
|
||||
"-an",
|
||||
outputPath,
|
||||
}
|
||||
_, err := e.run(probeResult, args)
|
||||
return err
|
||||
}
|
||||
32
pkg/ffmpeg/encoder_screenshot.go
Normal file
32
pkg/ffmpeg/encoder_screenshot.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package ffmpeg
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ScreenshotOptions struct {
|
||||
OutputPath string
|
||||
Quality int
|
||||
Time float64
|
||||
Width int
|
||||
Verbosity string
|
||||
}
|
||||
|
||||
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
|
||||
if options.Verbosity == "" {
|
||||
options.Verbosity = "quiet"
|
||||
}
|
||||
if options.Quality == 0 {
|
||||
options.Quality = 1
|
||||
}
|
||||
args := []string{
|
||||
"-v", options.Verbosity,
|
||||
"-ss", fmt.Sprintf("%v", options.Time),
|
||||
"-y",
|
||||
"-i", probeResult.Path, // TODO: Wrap in quotes?
|
||||
"-vframes", "1",
|
||||
"-q:v", fmt.Sprintf("%v", options.Quality),
|
||||
"-vf", fmt.Sprintf("scale=%v:-1", options.Width),
|
||||
"-f", "image2",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
20
pkg/ffmpeg/encoder_transcode.go
Normal file
20
pkg/ffmpeg/encoder_transcode.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package ffmpeg
|
||||
|
||||
type TranscodeOptions struct {
|
||||
OutputPath string
|
||||
}
|
||||
|
||||
func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
|
||||
args := []string{
|
||||
"-i", probeResult.Path,
|
||||
"-c:v", "libx264",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", "superfast",
|
||||
"-crf", "23",
|
||||
"-vf", "scale=iw:-2",
|
||||
"-c:a", "aac",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
152
pkg/ffmpeg/ffprobe.go
Normal file
152
pkg/ffmpeg/ffprobe.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ValidCodecs = []string{"h264", "h265", "vp8", "vp9"}
|
||||
|
||||
func IsValidCodec(codecName string) bool {
|
||||
for _, c := range ValidCodecs {
|
||||
if c == codecName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type VideoFile struct {
|
||||
JSON FFProbeJSON
|
||||
AudioStream *FFProbeStream
|
||||
VideoStream *FFProbeStream
|
||||
|
||||
Path string
|
||||
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.
|
||||
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()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", videoPath, string(out), err.Error())
|
||||
}
|
||||
|
||||
probeJSON := &FFProbeJSON{}
|
||||
if err := json.Unmarshal(out, probeJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parse(videoPath, probeJSON)
|
||||
}
|
||||
|
||||
func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
||||
if probeJSON == nil {
|
||||
return nil, fmt.Errorf("failed to get ffprobe json")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
result.Path = filePath
|
||||
|
||||
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, _ := os.Stat(filePath)
|
||||
result.Size = fileStat.Size()
|
||||
result.StartTime, _ = strconv.ParseFloat(probeJSON.Format.StartTime, 64)
|
||||
result.CreationTime = probeJSON.Format.Tags.CreationTime
|
||||
|
||||
audioStream := result.GetAudioStream()
|
||||
if audioStream != nil {
|
||||
result.AudioCodec = audioStream.CodecName
|
||||
result.AudioStream = audioStream
|
||||
}
|
||||
|
||||
videoStream := result.GetVideoStream()
|
||||
if videoStream != nil {
|
||||
result.VideoStream = videoStream
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (v *VideoFile) GetAudioStream() *FFProbeStream {
|
||||
index := v.getStreamIndex("audio", v.JSON)
|
||||
if index != -1 {
|
||||
return &v.JSON.Streams[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VideoFile) GetVideoStream() *FFProbeStream {
|
||||
index := v.getStreamIndex("video", v.JSON)
|
||||
if index != -1 {
|
||||
return &v.JSON.Streams[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VideoFile) getStreamIndex(fileType string, probeJSON FFProbeJSON) int {
|
||||
for i, stream := range probeJSON.Streams {
|
||||
if stream.CodecType == fileType {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
92
pkg/ffmpeg/types.go
Normal file
92
pkg/ffmpeg/types.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type FFProbeJSON struct {
|
||||
Format struct {
|
||||
BitRate string `json:"bit_rate"`
|
||||
Duration string `json:"duration"`
|
||||
Filename string `json:"filename"`
|
||||
FormatLongName string `json:"format_long_name"`
|
||||
FormatName string `json:"format_name"`
|
||||
NbPrograms int `json:"nb_programs"`
|
||||
NbStreams int `json:"nb_streams"`
|
||||
ProbeScore int `json:"probe_score"`
|
||||
Size string `json:"size"`
|
||||
StartTime string `json:"start_time"`
|
||||
Tags struct {
|
||||
CompatibleBrands string `json:"compatible_brands"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
Encoder string `json:"encoder"`
|
||||
MajorBrand string `json:"major_brand"`
|
||||
MinorVersion string `json:"minor_version"`
|
||||
} `json:"tags"`
|
||||
} `json:"format"`
|
||||
Streams []FFProbeStream `json:"streams"`
|
||||
Error struct {
|
||||
Code int `json:"code"`
|
||||
String string `json:"string"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type FFProbeStream struct {
|
||||
AvgFrameRate string `json:"avg_frame_rate"`
|
||||
BitRate string `json:"bit_rate"`
|
||||
BitsPerRawSample string `json:"bits_per_raw_sample,omitempty"`
|
||||
ChromaLocation string `json:"chroma_location,omitempty"`
|
||||
CodecLongName string `json:"codec_long_name"`
|
||||
CodecName string `json:"codec_name"`
|
||||
CodecTag string `json:"codec_tag"`
|
||||
CodecTagString string `json:"codec_tag_string"`
|
||||
CodecTimeBase string `json:"codec_time_base"`
|
||||
CodecType string `json:"codec_type"`
|
||||
CodedHeight int `json:"coded_height,omitempty"`
|
||||
CodedWidth int `json:"coded_width,omitempty"`
|
||||
DisplayAspectRatio string `json:"display_aspect_ratio,omitempty"`
|
||||
Disposition struct {
|
||||
AttachedPic int `json:"attached_pic"`
|
||||
CleanEffects int `json:"clean_effects"`
|
||||
Comment int `json:"comment"`
|
||||
Default int `json:"default"`
|
||||
Dub int `json:"dub"`
|
||||
Forced int `json:"forced"`
|
||||
HearingImpaired int `json:"hearing_impaired"`
|
||||
Karaoke int `json:"karaoke"`
|
||||
Lyrics int `json:"lyrics"`
|
||||
Original int `json:"original"`
|
||||
TimedThumbnails int `json:"timed_thumbnails"`
|
||||
VisualImpaired int `json:"visual_impaired"`
|
||||
} `json:"disposition"`
|
||||
Duration string `json:"duration"`
|
||||
DurationTs int `json:"duration_ts"`
|
||||
HasBFrames int `json:"has_b_frames,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
Index int `json:"index"`
|
||||
IsAvc string `json:"is_avc,omitempty"`
|
||||
Level int `json:"level,omitempty"`
|
||||
NalLengthSize string `json:"nal_length_size,omitempty"`
|
||||
NbFrames string `json:"nb_frames"`
|
||||
PixFmt string `json:"pix_fmt,omitempty"`
|
||||
Profile string `json:"profile"`
|
||||
RFrameRate string `json:"r_frame_rate"`
|
||||
Refs int `json:"refs,omitempty"`
|
||||
SampleAspectRatio string `json:"sample_aspect_ratio,omitempty"`
|
||||
StartPts int `json:"start_pts"`
|
||||
StartTime string `json:"start_time"`
|
||||
Tags struct {
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
HandlerName string `json:"handler_name"`
|
||||
Language string `json:"language"`
|
||||
Rotate string `json:"rotate"`
|
||||
} `json:"tags"`
|
||||
TimeBase string `json:"time_base"`
|
||||
Width int `json:"width,omitempty"`
|
||||
BitsPerSample int `json:"bits_per_sample,omitempty"`
|
||||
ChannelLayout string `json:"channel_layout,omitempty"`
|
||||
Channels int `json:"channels,omitempty"`
|
||||
MaxBitRate string `json:"max_bit_rate,omitempty"`
|
||||
SampleFmt string `json:"sample_fmt,omitempty"`
|
||||
SampleRate string `json:"sample_rate,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user