diff --git a/README.md b/README.md index 651b613..3693e9b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ ## Download from Github ```shell -go get github.com/floostack/transcoder +go get github.com/admpub/transcoder ``` ## Example @@ -45,7 +45,7 @@ package main import ( "log" - ffmpeg "github.com/floostack/transcoder/ffmpeg" + ffmpeg "github.com/admpub/transcoder/ffmpeg" ) func main() { diff --git a/ffmpeg/config.go b/ffmpeg/config.go index 05eb7ff..10cfa1c 100644 --- a/ffmpeg/config.go +++ b/ffmpeg/config.go @@ -1,9 +1,14 @@ package ffmpeg +import "github.com/admpub/transcoder" + // Config ... type Config struct { FfmpegBinPath string FfprobeBinPath string ProgressEnabled bool Verbose bool + Env []string + Dir string + OnMetadata func(transcoder.Metadata) error } diff --git a/ffmpeg/ffmpeg.go b/ffmpeg/ffmpeg.go index ce262f8..95fff24 100644 --- a/ffmpeg/ffmpeg.go +++ b/ffmpeg/ffmpeg.go @@ -8,14 +8,15 @@ import ( "errors" "fmt" "io" + "log" "os" "os/exec" "regexp" "strconv" "strings" - "github.com/floostack/transcoder" - "github.com/floostack/transcoder/utils" + "github.com/admpub/transcoder" + "github.com/admpub/transcoder/utils" ) // Transcoder ... @@ -23,13 +24,13 @@ type Transcoder struct { config *Config input string output []string - options [][]string + options []transcoder.Options metadata transcoder.Metadata - inputPipeReader *io.ReadCloser - outputPipeReader *io.ReadCloser - inputPipeWriter *io.WriteCloser - outputPipeWriter *io.WriteCloser - commandContext *context.Context + inputPipeReader io.ReadCloser + outputPipeReader io.ReadCloser + inputPipeWriter io.WriteCloser + outputPipeWriter io.WriteCloser + commandContext context.Context } // New ... @@ -42,8 +43,6 @@ func (t *Transcoder) Start(opts transcoder.Options) (<-chan transcoder.Progress, var stderrIn io.ReadCloser - out := make(chan transcoder.Progress) - defer t.closePipes() // Validates config @@ -52,10 +51,15 @@ func (t *Transcoder) Start(opts transcoder.Options) (<-chan transcoder.Progress, } // Get file metadata - _, err := t.GetMetadata() + metadata, err := t.GetMetadata() if err != nil { return nil, err } + if t.config.OnMetadata != nil { + if err := t.config.OnMetadata(metadata); err != nil { + return nil, err + } + } // Append input file and standard options args := append([]string{"-i", t.input}, opts.GetStrArguments()...) @@ -66,16 +70,20 @@ func (t *Transcoder) Start(opts transcoder.Options) (<-chan transcoder.Progress, // Just append the 1 output file we've got args = append(args, t.output[0]) } else { + arguments := make([][]string, len(t.options)) + for i, o := range t.options { + arguments[i] = o.GetStrArguments() + } for index, out := range t.output { // Get executable flags // If we are at the last output file but still have several options, append them all at once if index == outputLength-1 && outputLength < optionsLength { for i := index; i < len(t.options); i++ { - args = append(args, t.options[i]...) + args = append(args, arguments[i]...) } // Otherwise just append the current options } else { - args = append(args, t.options[index]...) + args = append(args, arguments[index]...) } // Append output flag @@ -91,14 +99,16 @@ func (t *Transcoder) Start(opts transcoder.Options) (<-chan transcoder.Progress, if t.commandContext == nil { cmd = exec.Command(t.config.FfmpegBinPath, args...) } else { - cmd = exec.CommandContext(*t.commandContext, t.config.FfmpegBinPath, args...) + cmd = exec.CommandContext(t.commandContext, t.config.FfmpegBinPath, args...) } + cmd.Env = append(t.config.Env, os.Environ()...) + cmd.Dir = t.config.Dir // If progresss enabled, get stderr pipe and start progress process if t.config.ProgressEnabled && !t.config.Verbose { stderrIn, err = cmd.StderrPipe() if err != nil { - return nil, fmt.Errorf("Failed getting transcoding progress (%s) with args (%s) with error %s", t.config.FfmpegBinPath, args, err) + return nil, fmt.Errorf("failed getting transcoding progress (%s) with args (%s) with error %w", t.config.FfmpegBinPath, args, err) } } @@ -109,23 +119,36 @@ func (t *Transcoder) Start(opts transcoder.Options) (<-chan transcoder.Progress, // Start process err = cmd.Start() if err != nil { - return nil, fmt.Errorf("Failed starting transcoding (%s) with args (%s) with error %s", t.config.FfmpegBinPath, args, err) + return nil, fmt.Errorf("failed starting transcoding (%s) with args (%s) with error %w", t.config.FfmpegBinPath, args, err) } + out := make(chan transcoder.Progress) if t.config.ProgressEnabled && !t.config.Verbose { + done := make(chan struct{}) go func() { t.progress(stderrIn, out) + done <- struct{}{} + close(done) }() go func() { defer close(out) err = cmd.Wait() + if err != nil { + err = fmt.Errorf("failed to transcoding (%s) with args (%s) with error %w", t.config.FfmpegBinPath, args, err) + log.Println(err) + out <- &Progress{Error: err} + } + <-done }() } else { err = cmd.Wait() + if err != nil { + return nil, fmt.Errorf("failed to transcoding (%s) with args (%s) with error %w", t.config.FfmpegBinPath, args, err) + } } - return out, nil + return out, err } // Input ... @@ -141,8 +164,8 @@ func (t *Transcoder) Output(arg string) transcoder.Transcoder { } // InputPipe ... -func (t *Transcoder) InputPipe(w *io.WriteCloser, r *io.ReadCloser) transcoder.Transcoder { - if &t.input == nil { +func (t *Transcoder) InputPipe(w io.WriteCloser, r io.ReadCloser) transcoder.Transcoder { + if len(t.input) == 0 { t.inputPipeWriter = w t.inputPipeReader = r } @@ -150,8 +173,8 @@ func (t *Transcoder) InputPipe(w *io.WriteCloser, r *io.ReadCloser) transcoder.T } // OutputPipe ... -func (t *Transcoder) OutputPipe(w *io.WriteCloser, r *io.ReadCloser) transcoder.Transcoder { - if &t.output == nil { +func (t *Transcoder) OutputPipe(w io.WriteCloser, r io.ReadCloser) transcoder.Transcoder { + if len(t.output) == 0 { t.outputPipeWriter = w t.outputPipeReader = r } @@ -160,20 +183,20 @@ func (t *Transcoder) OutputPipe(w *io.WriteCloser, r *io.ReadCloser) transcoder. // WithOptions Sets the options object func (t *Transcoder) WithOptions(opts transcoder.Options) transcoder.Transcoder { - t.options = [][]string{opts.GetStrArguments()} + t.options = []transcoder.Options{opts} return t } // WithAdditionalOptions Appends an additional options object func (t *Transcoder) WithAdditionalOptions(opts transcoder.Options) transcoder.Transcoder { - t.options = append(t.options, opts.GetStrArguments()) + t.options = append(t.options, opts) return t } // WithContext is to be used on a Transcoder *before Starting* to // pass in a context.Context object that can be used to kill // a running transcoder process. Usage of this method is optional -func (t *Transcoder) WithContext(ctx *context.Context) transcoder.Transcoder { +func (t *Transcoder) WithContext(ctx context.Context) transcoder.Transcoder { t.commandContext = ctx return t } @@ -212,7 +235,7 @@ func (t *Transcoder) validate() error { // GetMetadata Returns metadata for the specified input file func (t *Transcoder) GetMetadata() (transcoder.Metadata, error) { - if t.config.FfprobeBinPath != "" { + if len(t.config.FfprobeBinPath) > 0 { var outb, errb bytes.Buffer input := t.input @@ -221,11 +244,25 @@ func (t *Transcoder) GetMetadata() (transcoder.Metadata, error) { input = "pipe:" } - args := []string{"-i", input, "-print_format", "json", "-show_format", "-show_streams", "-show_error"} + args := []string{ + "-i", input, + "-print_format", "json", + "-show_entries", "stream=:stream_tags=rotate", + "-show_format", + "-show_streams", + "-show_error", + } - cmd := exec.Command(t.config.FfprobeBinPath, args...) + var cmd *exec.Cmd + if t.commandContext == nil { + cmd = exec.Command(t.config.FfprobeBinPath, args...) + } else { + cmd = exec.CommandContext(t.commandContext, t.config.FfprobeBinPath, args...) + } cmd.Stdout = &outb cmd.Stderr = &errb + cmd.Env = append(t.config.Env, os.Environ()...) + cmd.Dir = t.config.Dir err := cmd.Run() if err != nil { @@ -234,7 +271,7 @@ func (t *Transcoder) GetMetadata() (transcoder.Metadata, error) { var metadata Metadata - if err = json.Unmarshal([]byte(outb.String()), &metadata); err != nil { + if err = json.Unmarshal(outb.Bytes(), &metadata); err != nil { return nil, err } @@ -246,6 +283,10 @@ func (t *Transcoder) GetMetadata() (transcoder.Metadata, error) { return nil, errors.New("ffprobe binary not found") } +var reEQ = regexp.MustCompile(`=\s+`) +var mgError = `Error ` +var mgFailed = `Conversion failed!` + // progress sends through given channel the transcoding status func (t *Transcoder) progress(stream io.ReadCloser, out chan transcoder.Progress) { @@ -276,16 +317,20 @@ func (t *Transcoder) progress(stream io.ReadCloser, out chan transcoder.Progress buf := make([]byte, 2) scanner.Buffer(buf, bufio.MaxScanTokenSize) + var isFailed bool + var errMessages []string + for scanner.Scan() { Progress := new(Progress) line := scanner.Text() - - if strings.Contains(line, "time=") && strings.Contains(line, "bitrate=") { - var re = regexp.MustCompile(`=\s+`) - st := re.ReplaceAllString(line, `=`) - + //println(`========>`, `[`+line+`]`) + if strings.HasPrefix(line, mgError) { + errMessages = append(errMessages, line) + } else if strings.HasPrefix(line, mgFailed) { + isFailed = true + } else if strings.Contains(line, "time=") && strings.Contains(line, "bitrate=") { + st := reEQ.ReplaceAllString(line, `=`) f := strings.Fields(st) - var framesProcessed string var currentTime string var currentBitrate string @@ -330,17 +375,18 @@ func (t *Transcoder) progress(stream io.ReadCloser, out chan transcoder.Progress out <- *Progress } } + if isFailed && len(errMessages) > 0 { + out <- &Progress{Error: errors.New(strings.Join(errMessages, "\n"))} + } } // closePipes Closes pipes if opened func (t *Transcoder) closePipes() { if t.inputPipeReader != nil { - ipr := *t.inputPipeReader - ipr.Close() + t.inputPipeReader.Close() } if t.outputPipeWriter != nil { - opr := *t.outputPipeWriter - opr.Close() + t.outputPipeWriter.Close() } } diff --git a/ffmpeg/metadata.go b/ffmpeg/metadata.go index 4e81910..000c82d 100644 --- a/ffmpeg/metadata.go +++ b/ffmpeg/metadata.go @@ -1,6 +1,6 @@ package ffmpeg -import "github.com/floostack/transcoder" +import "github.com/admpub/transcoder" // Metadata ... type Metadata struct { @@ -25,34 +25,36 @@ type Format struct { // Streams ... type Streams struct { Index int - ID string `json:"id"` - CodecName string `json:"codec_name"` - CodecLongName string `json:"codec_long_name"` - Profile string `json:"profile"` - CodecType string `json:"codec_type"` - CodecTimeBase string `json:"codec_time_base"` - CodecTagString string `json:"codec_tag_string"` - CodecTag string `json:"codec_tag"` - Width int `json:"width"` - Height int `json:"height"` - CodedWidth int `json:"coded_width"` - CodedHeight int `json:"coded_height"` - HasBFrames int `json:"has_b_frames"` - 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"` - BitRate string `json:"bit_rate"` + ID string `json:"id"` + CodecName string `json:"codec_name"` + CodecLongName string `json:"codec_long_name"` + Profile string `json:"profile"` + CodecType string `json:"codec_type"` + CodecTimeBase string `json:"codec_time_base"` + CodecTagString string `json:"codec_tag_string"` + CodecTag string `json:"codec_tag"` + Width int `json:"width"` + Height int `json:"height"` + CodedWidth int `json:"coded_width"` + CodedHeight int `json:"coded_height"` + HasBFrames int `json:"has_b_frames"` + 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"` + BitRate string `json:"bit_rate"` + Tags map[string]string `json:"tags"` + SideDataList []map[string]interface{} `json:"side_data_list"` } // Tags ... @@ -287,6 +289,14 @@ func (s Streams) GetBitRate() string { return s.BitRate } +func (s Streams) GetTags() map[string]string { + return s.Tags +} + +func (s Streams) GetSideDataList() []map[string]interface{} { + return s.SideDataList +} + //GetDefault ... func (d Disposition) GetDefault() int { return d.Default diff --git a/ffmpeg/options.go b/ffmpeg/options.go index 323d4b3..15d5f2d 100644 --- a/ffmpeg/options.go +++ b/ffmpeg/options.go @@ -77,34 +77,39 @@ func (opts Options) GetStrArguments() []string { for i := 0; i < f.NumField(); i++ { flag := f.Field(i).Tag.Get("flag") - value := v.Field(i).Interface() + rv := v.Field(i) + value := rv.Interface() - if !v.Field(i).IsNil() { + if !rv.IsNil() { if _, ok := value.(*bool); ok { values = append(values, flag) + continue } if vs, ok := value.(*string); ok { values = append(values, flag, *vs) + continue } if va, ok := value.([]string); ok { - for i := 0; i < len(va); i++ { item := va[i] values = append(values, flag, item) } + continue } if vm, ok := value.(map[string]interface{}); ok { for k, v := range vm { values = append(values, k, fmt.Sprintf("%v", v)) } + continue } - + if vi, ok := value.(*int); ok { values = append(values, flag, fmt.Sprintf("%d", *vi)) + continue } } diff --git a/ffmpeg/progress.go b/ffmpeg/progress.go index 62594c6..e305a5c 100644 --- a/ffmpeg/progress.go +++ b/ffmpeg/progress.go @@ -7,6 +7,7 @@ type Progress struct { CurrentBitrate string Progress float64 Speed string + Error error } // GetFramesProcessed ... @@ -33,3 +34,8 @@ func (p Progress) GetProgress() float64 { func (p Progress) GetSpeed() string { return p.Speed } + +// GetError ... +func (p Progress) GetError() error { + return p.Error +} diff --git a/go.mod b/go.mod index 4c9ade4..1e14137 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/floostack/transcoder +module github.com/admpub/transcoder go 1.13 diff --git a/metadata.go b/metadata.go index d03241d..89bd992 100644 --- a/metadata.go +++ b/metadata.go @@ -51,6 +51,8 @@ type Streams interface { GetDuration() string GetDisposition() Disposition GetBitRate() string + GetTags() map[string]string + GetSideDataList() []map[string]interface{} } // Tags ... diff --git a/progress.go b/progress.go index 4b7b060..8537680 100644 --- a/progress.go +++ b/progress.go @@ -7,4 +7,5 @@ type Progress interface { GetCurrentBitrate() string GetProgress() float64 GetSpeed() string + GetError() error } diff --git a/transcoder.go b/transcoder.go index c93bc4a..03ccaa7 100644 --- a/transcoder.go +++ b/transcoder.go @@ -9,11 +9,11 @@ import ( type Transcoder interface { Start(opts Options) (<-chan Progress, error) Input(i string) Transcoder - InputPipe(w *io.WriteCloser, r *io.ReadCloser) Transcoder + InputPipe(w io.WriteCloser, r io.ReadCloser) Transcoder Output(o string) Transcoder - OutputPipe(w *io.WriteCloser, r *io.ReadCloser) Transcoder + OutputPipe(w io.WriteCloser, r io.ReadCloser) Transcoder WithOptions(opts Options) Transcoder WithAdditionalOptions(opts Options) Transcoder - WithContext(ctx *context.Context) Transcoder + WithContext(ctx context.Context) Transcoder GetMetadata() (Metadata, error) }