Transcoding files

Added getters & setters
Get file's metada using ffprobe
Getting ffmpeg proccess info: frames processed, bitrate, current time and progress percentage through channel
This commit is contained in:
frr
2018-02-01 14:39:38 +01:00
parent e545a08502
commit 37d274517b
4 changed files with 667 additions and 48 deletions

360
models/media.go Normal file
View File

@@ -0,0 +1,360 @@
package models
import (
"strings"
"strconv"
"fmt"
"reflect"
)
type Mediafile struct {
aspect string
resolution string
videoBitRate int
videoBitRateTolerance int
videoMaxBitRate int
videoMinBitrate int
videoCodec string
frameRate int
maxKeyframe int
minKeyframe int
keyframeInterval int
audioCodec string
audioBitrate int
audioChannels int
bufferSize int
threads int
target string
duration string
seekTime string
quality int
metadata Metadata
}
/*** SETTERS ***/
func (m *Mediafile) SetAspect(v string) {
m.aspect = v
}
func (m *Mediafile) SetResolution(v string) {
m.resolution = v
}
func (m *Mediafile) SetVideoBitRate(v int) {
m.videoBitRate = v
}
func (m *Mediafile) SetVideoBitRateTolerance(v int) {
m.videoBitRateTolerance = v
}
func (m *Mediafile) SetVideoMaxBitrate(v int) {
m.videoMaxBitRate = v
}
func (m *Mediafile) SetVideoMinBitRate(v int) {
m.videoMinBitrate = v
}
func (m *Mediafile) SetVideoCodec(v string) {
m.videoCodec = v
}
func (m *Mediafile) SetFrameRate(v int) {
m.frameRate = v
}
func (m *Mediafile) SetMaxKeyFrame(v int) {
m.maxKeyframe = v
}
func (m *Mediafile) SetMinKeyFrame(v int) {
m.minKeyframe = v
}
func (m *Mediafile) SetKeyframeInterval(v int) {
m.keyframeInterval = v
}
func (m *Mediafile) SetAudioCodec(v string) {
m.audioCodec = v
}
func (m *Mediafile) SetAudioBitRate(v int) {
m.audioBitrate = v
}
func (m *Mediafile) SetAudioChannels(v int) {
m.audioChannels = v
}
func (m *Mediafile) SetBufferSize(v int) {
m.bufferSize = v
}
func (m *Mediafile) SetThreads(v int) {
m.threads = v
}
func (m *Mediafile) SetDuration(v string) {
m.duration = v
}
func (m *Mediafile) SetSeekTime(v string) {
m.seekTime = v
}
func (m *Mediafile) SetQuality(v int) {
m.quality = v
}
func (m *Mediafile) SetMetadata(v Metadata) {
m.metadata = v
}
/*** GETTERS ***/
func (m Mediafile) Aspect() string {
return m.aspect
}
func (m Mediafile) Resolution() string {
return m.resolution
}
func (m Mediafile) VideoBitrate() int {
return m.videoBitRate
}
func (m Mediafile) VideoBitRateTolerance() int {
return m.videoBitRateTolerance
}
func (m Mediafile) VideoMaxBitRate() int {
return m.videoMaxBitRate
}
func (m Mediafile) VideoMinBitRate() int {
return m.videoMinBitrate
}
func (m Mediafile) VideoCodec() string {
return m.videoCodec
}
func (m Mediafile) FrameRate() int {
return m.frameRate
}
func (m Mediafile) MaxKeyFrame() int {
return m.maxKeyframe
}
func (m Mediafile) MinKeyFrame() int {
return m.minKeyframe
}
func (m Mediafile) KeyFrameInterval() int {
return m.keyframeInterval
}
func (m Mediafile) AudioCodec() string {
return m.audioCodec
}
func (m Mediafile) AudioBitrate() int {
return m.audioBitrate
}
func (m Mediafile) AudioChannels() int {
return m.audioChannels
}
func (m Mediafile) BufferSize() int {
return m.bufferSize
}
func (m Mediafile) Threads() int {
return m.threads
}
func (m Mediafile) Target() string {
return m.target
}
func (m Mediafile) Duration() string {
return m.duration
}
func (m Mediafile) SeekTime() string {
return m.seekTime
}
func (m Mediafile) Quality() int {
return m.quality
}
func (m Mediafile) Metadata() Metadata {
return m.metadata
}
/** OPTS **/
func (m Mediafile) ToStrCommand() string {
var strCommand string
opts := []string{"Aspect", "VideoCodec", "FrameRate", "Resolution", "VideoBitRate", "VideoBitRateTolerance", "AudioCodec", "AudioBitRate", "AudioChannels", "VideoMaxBitRate", "VideoMinBitRate", "BufferSize", "Threads", "Target", "Duration", "KeyframeInterval", "SeekTime", "Quality"}
for _, name := range opts {
opt := reflect.ValueOf(&m).MethodByName(fmt.Sprintf("Obtain%s", name))
if (opt != reflect.Value{}) {
result := opt.Call([]reflect.Value{})
if result[0].String() != "" {
strCommand += " "
strCommand += result[0].String()
}
}
}
return strCommand
}
func (m *Mediafile) ObtainAspect() string {
// Set aspect
if m.resolution != "" {
resolution := strings.Split(m.resolution, "x")
if len(resolution) != 0 {
width, _ := strconv.ParseFloat(resolution[0], 64)
height, _ := strconv.ParseFloat(resolution[1], 64)
return fmt.Sprintf("-aspect %f", width/height)
}
}
if m.aspect != "" {
return fmt.Sprintf("-aspect %s", m.aspect)
} else {
return ""
}
}
func (m *Mediafile) ObtainVideoCodec() string {
if m.videoCodec != "" {
return fmt.Sprintf("-vcodec %s", m.videoCodec)
}
return ""
}
func (m *Mediafile) ObtainFrameRate() string {
if m.frameRate != 0 {
return fmt.Sprintf("-r %d", m.frameRate)
}
return ""
}
func (m *Mediafile) ObtainResolution() string {
if m.resolution != "" {
return fmt.Sprintf("-s %s", m.resolution)
}
return ""
}
func (m *Mediafile) ObtainVideoBitRate() string {
if m.videoBitRate != 0 {
return fmt.Sprintf("-b:v %d", m.videoBitRate)
}
return ""
}
func (m *Mediafile) ObtainAudioCodec() string {
if m.audioCodec != "" {
return fmt.Sprintf("-acodec %s", m.audioCodec)
}
return ""
}
func (m *Mediafile) ObtainAudioBitRate() string {
if m.audioBitrate != 0 {
return fmt.Sprintf("-b:a %d", m.audioBitrate)
}
return ""
}
func (m *Mediafile) ObtainAudioChannels() string {
if m.audioChannels != 0 {
return fmt.Sprintf("-ac %d", m.audioChannels)
}
return ""
}
func (m *Mediafile) ObtainVideoMaxBitRate() string {
if m.videoMaxBitRate != 0 {
return fmt.Sprintf("-maxrate %dk", m.videoMaxBitRate)
}
return ""
}
func (m *Mediafile) ObtainVideoMinBitRate() string {
if m.videoMinBitrate != 0 {
return fmt.Sprintf("-minrate %dk", m.videoMinBitrate)
}
return ""
}
func (m *Mediafile) ObtainBufferSize() string {
if m.bufferSize != 0 {
return fmt.Sprintf("-bufsize %dk", m.bufferSize)
}
return ""
}
func (m *Mediafile) ObtainVideoBitRateTolerance() string {
if m.videoBitRateTolerance != 0 {
return fmt.Sprintf("-bt %dk", m.videoBitRateTolerance)
}
return ""
}
func (m *Mediafile) ObtainThreads() string {
if m.threads != 0 {
return fmt.Sprintf("-threads %d", m.threads)
}
return ""
}
func (m *Mediafile) ObtainTarget() string {
if m.target != "" {
return fmt.Sprintf("-target %s", m.target)
}
return ""
}
func (m *Mediafile) ObtainDuration() string {
if m.duration != "" {
return fmt.Sprintf("-t %s", m.duration)
}
return ""
}
func (m *Mediafile) ObtainKeyframeInterval() string {
if m.keyframeInterval != 0 {
return fmt.Sprintf("-g %d", m.keyframeInterval)
}
return ""
}
func (m *Mediafile) ObtainSeekTime() string {
if m.seekTime != "" {
return fmt.Sprintf("-ss %s", m.seekTime)
}
return ""
}
func (m *Mediafile) ObtainQuality() string {
if m.quality != 0 {
return fmt.Sprintf("-q:v %d", m.quality)
}
return ""
}

View File

@@ -5,24 +5,74 @@ type Ffmpeg struct {
FfprobeBinPath string FfprobeBinPath string
} }
type Mediafile struct { type Metadata struct {
Aspect string Streams []Streams `json:"streams"`
Resolution string Format Format `json:"format"`
VideoBitRate string }
VideoMaxBitrate string
VideoMinBitrate string type Streams struct {
VideoCodec string Index int
FrameRate string CodecName string `json:"codec_name"`
MaxKeyframe string CodecLongName string `json:"codec_long_name"`
MinKeyframe string Profile string `json:"profile"`
AudioCodec string CodecType string `json:"codec_type"`
AudioBitrate string CodecTimeBase string `json:"codec_time_base"`
AudioSampleRate string CodecTagString string `json:"codec_tag_string"`
AudioChannels string CodecTag string `json:"codec_tag"`
BufferSize string Width int `json:"width"`
Threads string Height int `json:"height"`
Target string CodedWidth int `json:"coded_width"`
Duration string CodedHeight int `json:"coded_height"`
SeekTime string HasBFrames int `json:"has_b_frames"`
Quality string SampleAspectRatio string `json:"sample_aspect_ratio"`
DisplayAspectRatio string `json:"display_aspect_ratio"`
PixFmt string `json:"pix_fmt"`
Level int `json:"level"`
ChromaLocation string `json:"chroma_location"`
Refs int `json:"refs"`
QuarterSample string `json:"quarter_sample"`
DivxPacked string `json:"divx_packed"`
RFrameRrate string `json:"r_frame_rate"`
AvgFrameRate string `json:"avg_frame_rate"`
TimeBase string `json:"time_base"`
DurationTs int `json:"duration_ts"`
Duration string `json:"duration"`
Disposition Disposition `json:"disposition"`
}
type Disposition struct {
Default int `json:"default"`
Dub int `json:"dub"`
Original int `json:"original"`
Comment int `json:"comment"`
Lyrics int `json:"lyrics"`
Karaoke int `json:"karaoke"`
Forced int `json:"forced"`
HearingImpaired int `json:"hearing_impaired"`
VisualImpaired int `json:"visual_impaired"`
CleanEffects int `json:"clean_effects"`
}
type Format struct {
Filename string
NbStreams int `json:"nb_streams"`
NbPrograms int `json:"nb_programs"`
FormatName string `json:"format_name"`
FormatLongName string `json:"format_long_name"`
Duration string `json:"duration"`
Size string `json:"size"`
BitRate string `json:"bit_rate"`
ProbeScore int `json:"probe_score"`
Tags Tags `json:"tags"`
}
type Progress struct {
FramesProcessed string
CurrentTime string
CurrentBitrate string
Progress float64
}
type Tags struct {
Encoder string `json:"ENCODER"`
} }

View File

@@ -1,41 +1,107 @@
package transcoder package transcoder
import ( import (
//"fmt"
//"os"
"errors" "errors"
"os" "os"
"goffmpeg/models" "goffmpeg/models"
"os/exec" "os/exec"
"fmt" "fmt"
"goffmpeg/ffmpeg" "goffmpeg/ffmpeg"
"goffmpeg/utils"
"bytes" "bytes"
"encoding/json"
"bufio"
"strings"
"regexp"
"strconv"
"log"
) )
type Transcoder struct { type Transcoder struct {
Process *os.Process process *exec.Cmd
InputPath string inputPath string
OutputPath string outputPath string
MediaFile *models.Mediafile mediafile *models.Mediafile
configuration ffmpeg.Configuration
} }
func New(inputPath *string, configuration *ffmpeg.Configuration) (*Transcoder, error) { func (t *Transcoder) SetProccess(v *exec.Cmd) {
transcoding := new(Transcoder) t.process = v
if inputPath == nil {
return nil, errors.New("error: transcoder.Initialize -> inputPath missing")
} }
_, err := os.Stat(*inputPath) func (t *Transcoder) SetInputPath(v string) {
t.inputPath = v
}
func (t *Transcoder) SetOutputPath(v string) {
t.outputPath = v
}
func (t *Transcoder) SetMediaFile(v *models.Mediafile) {
t.mediafile = v
}
func (t *Transcoder) SetConfiguration(v ffmpeg.Configuration) {
t.configuration = v
}
/*** GETTERS ***/
func (t Transcoder) Process() *exec.Cmd {
return t.process
}
func (t Transcoder) InputPath() string {
return t.inputPath
}
func (t Transcoder) OutputPath() string {
return t.outputPath
}
func (t Transcoder) MediaFile() *models.Mediafile {
return t.mediafile
}
func (t Transcoder) FFmpegExec() string {
return t.configuration.FfmpegBin
}
func (t Transcoder) FFprobeExec() string {
return t.configuration.FfprobeBin
}
func (t Transcoder) GetCommand() string {
var rcommand string
rcommand = fmt.Sprintf("%s -y -i %s ", t.configuration.FfmpegBin, t.inputPath)
media := t.mediafile
rcommand += media.ToStrCommand()
rcommand += " " + t.outputPath
fmt.Println(rcommand)
return rcommand
}
/*** FUNCTIONS ***/
func (t *Transcoder) Initialize(inputPath string, outputPath string, configuration *ffmpeg.Configuration) (error) {
if inputPath == "" {
return errors.New("error: transcoder.Initialize -> inputPath missing")
}
_, err := os.Stat(inputPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, errors.New("error: transcoder.Initialize -> input file not found") return errors.New("error: transcoder.Initialize -> input file not found")
} }
// Set input path command := fmt.Sprintf("%s -i %s -print_format json -show_format -show_streams -show_error", configuration.FfprobeBin, inputPath)
transcoding.InputPath = *inputPath
// TODO: Get file metadata from ffprobe and set MediaFile
command := fmt.Sprintf("%s -i %s -print_format json -show_format -show_streams -show_error", configuration.FfprobeBin, *inputPath)
cmd := exec.Command("/bin/sh", "-c", command) cmd := exec.Command("/bin/sh", "-c", command)
@@ -46,25 +112,144 @@ func New(inputPath *string, configuration *ffmpeg.Configuration) (*Transcoder, e
cmdErr := cmd.Start() cmdErr := cmd.Start()
if cmdErr != nil { if cmdErr != nil {
return nil, cmdErr return cmdErr
} }
_, errProc := cmd.Process.Wait() _, errProc := cmd.Process.Wait()
if errProc != nil { if errProc != nil {
return nil, errProc return errProc
} }
stdout := out.String() var Metadata models.Metadata
fmt.Println(stdout) if err := json.Unmarshal([]byte(out.String()), &Metadata); err != nil {
return err
}
transcoding.MediaFile = new(models.Mediafile) // Set new Mediafile
MediaFile := new(models.Mediafile)
MediaFile.SetMetadata(Metadata)
return transcoding, nil // Set transcoder configuration
t.SetInputPath(inputPath)
t.SetOutputPath(outputPath)
t.SetMediaFile(MediaFile)
t.SetConfiguration(*configuration)
return nil
} }
func (t *Transcoder) SetBitRate(v *string) string { func (t *Transcoder) Run() (<-chan bool) {
t.MediaFile.VideoBitRate = *v done := make(chan bool)
return t.MediaFile.VideoBitRate command := t.GetCommand()
proc := exec.Command("/bin/sh", "-c", command)
t.SetProccess(proc)
go func() {
perror := proc.Start()
if perror != nil {
log.Fatal(perror)
return
}
proc.Wait()
done <- true
}()
return done
}
// TODO: ONLY WORKS FOR VIDEO FILES
func (t Transcoder) Output() (chan models.Progress) {
out := make(chan models.Progress)
go func() {
stderr, serr := t.Process().StderrPipe()
if serr != nil {
log.Fatal(serr)
return
}
scanner := bufio.NewScanner(stderr)
split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if err != nil {
log.Fatal(err)
return 0, nil, nil
}
if atEOF && len(data) == 0 {
return 0, nil, nil
}
fr := strings.Index(string(data), "frame=")
if fr > 0 {
return fr + 1, data[fr:], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
scanner.Split(split)
buf := make([]byte, 2)
scanner.Buffer(buf, bufio.MaxScanTokenSize)
var lastProgress float64
var lastFrames string
for scanner.Scan() {
Progress := new(models.Progress)
line := scanner.Text()
if strings.Contains(line, "time=") && strings.Contains(line, "bitrate=") {
var re= regexp.MustCompile(`=\s+`)
st := re.ReplaceAllString(line, `=`)
f := strings.Fields(st)
// Frames processed
framesProcessed := strings.Split(f[0], "=")[1]
// Current time processed
time := strings.Split(f[4], "=")[1]
timesec := utils.DurToSec(time)
dursec, _ := strconv.ParseFloat(t.MediaFile().Metadata().Format.Duration, 64)
// Progress calculation
progress := (timesec * 100) / dursec
// Current bitrate
currentBitrate := strings.Split(f[5], "=")[1]
Progress.Progress = progress
Progress.CurrentBitrate = currentBitrate
Progress.FramesProcessed = framesProcessed
Progress.CurrentTime = time
if progress != lastProgress && framesProcessed != lastFrames{
lastProgress = progress
lastFrames = framesProcessed
out <- *Progress
}
}
}
defer close(out)
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}()
return out
} }

24
utils/utils.go Normal file
View File

@@ -0,0 +1,24 @@
package utils
import (
"strings"
"strconv"
)
func DurToSec(dur string) (sec float64) {
durAry := strings.Split(dur, ":")
var secs float64
if len(durAry) != 3 {
return
}
hr, _ := strconv.ParseFloat(durAry[0], 64)
secs = hr * (60 * 60)
min, _ := strconv.ParseFloat(durAry[1], 64)
secs += min * (60)
second, _ := strconv.ParseFloat(durAry[2], 64)
secs += second
return secs
}